feat(documents): implement document upload functionality for companies
- Added DocumentItem component to display uploaded documents in CompanyDetailsPage. - Introduced useUploadCompanyDocuments hook for handling document uploads. - Updated CreateCompany form to include document upload capabilities and validation for file types and sizes. - Enhanced API services to support batch document uploads and retrieval of document information. - Improved response handling in the file service for better error management and user feedback.
This commit is contained in:
@@ -17,7 +17,10 @@ export const API_ENDPOINTS = {
|
||||
},
|
||||
FILES: {
|
||||
UPLOAD: "files/upload",
|
||||
UPLOAD_BATCH: "files/upload/batch",
|
||||
DOWNLOAD: (fileId: string) => `files/${fileId}/download`,
|
||||
INFO: (fileId: string) => `files/${fileId}/info`,
|
||||
DELETE: (fileId: string) => `files/${fileId}`,
|
||||
},
|
||||
PROFILE: {
|
||||
READ: "profile",
|
||||
@@ -29,6 +32,7 @@ export const API_ENDPOINTS = {
|
||||
UPDATE: (id: string) => `companies/${id}`,
|
||||
DELETE: (id: string) => `companies/${id}`,
|
||||
DETAILS: (id: string) => `companies/${id}`,
|
||||
DOCUMENTS: (id: string) => `companies/${id}/documents`,
|
||||
CATEGORIES: {
|
||||
READ_ALL: "company-categories",
|
||||
CREATE: "company-categories",
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
TCompaniesResponse,
|
||||
TCompanyCategoryApiResponse,
|
||||
TCreateCompanyCategoryCredentials,
|
||||
UploadDocumentsData,
|
||||
} from "../types";
|
||||
|
||||
import { ICreateCompanyCredentials } from "../types";
|
||||
@@ -51,6 +52,45 @@ export const companiesService = {
|
||||
const response = await api.get(API_ENDPOINTS.COMPANIES.DETAILS(id));
|
||||
return response.data;
|
||||
},
|
||||
|
||||
/**
|
||||
* Uploads documents and links them to a company in one step. New file IDs are
|
||||
* appended to the company's existing `document_file_ids` (server-side), and
|
||||
* any per-file failures are returned in `data.failed`.
|
||||
*/
|
||||
uploadDocuments: async ({
|
||||
id,
|
||||
files,
|
||||
onProgress,
|
||||
}: {
|
||||
id: string;
|
||||
files: File[];
|
||||
onProgress?: (percent: number) => void;
|
||||
}): Promise<ApiResponse<UploadDocumentsData>> => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
files.forEach((file) => formData.append("files", file, file.name));
|
||||
|
||||
const response = await api.post(
|
||||
API_ENDPOINTS.COMPANIES.DOCUMENTS(id),
|
||||
formData,
|
||||
{
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (!progressEvent.total) return;
|
||||
onProgress?.(
|
||||
Math.round((progressEvent.loaded * 100) / progressEvent.total),
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("ERROR caught in Companies Services Upload Documents:", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
deleteCompany: async (id: string) => {
|
||||
try {
|
||||
return (await api.delete(API_ENDPOINTS.COMPANIES.DELETE(id))).data;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import api from "../axios";
|
||||
import { buildFileDownloadUrl } from "../config";
|
||||
import { API_ENDPOINTS } from "../endpoints";
|
||||
import { BatchUploadResult, FileInfo } from "../types";
|
||||
|
||||
interface IUploadFileParams {
|
||||
file: File;
|
||||
@@ -10,6 +11,23 @@ interface IUploadFileParams {
|
||||
onProgress?: (percent: number) => void;
|
||||
}
|
||||
|
||||
interface IUploadBatchParams {
|
||||
files: File[];
|
||||
category?: string;
|
||||
tags?: string[];
|
||||
description?: string;
|
||||
onProgress?: (percent: number) => void;
|
||||
}
|
||||
|
||||
const trackUploadProgress =
|
||||
(onProgress?: (percent: number) => void) =>
|
||||
(progressEvent: { loaded: number; total?: number }) => {
|
||||
if (!progressEvent.total) return;
|
||||
onProgress?.(
|
||||
Math.round((progressEvent.loaded * 100) / progressEvent.total),
|
||||
);
|
||||
};
|
||||
|
||||
export const fileService = {
|
||||
/**
|
||||
* Uploads a binary file to the shared filestore endpoint and returns the
|
||||
@@ -36,13 +54,7 @@ export const fileService = {
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
onUploadProgress: (progressEvent) => {
|
||||
if (!progressEvent.total) return;
|
||||
const percent = Math.round(
|
||||
(progressEvent.loaded * 100) / progressEvent.total,
|
||||
);
|
||||
onProgress?.(percent);
|
||||
},
|
||||
onUploadProgress: trackUploadProgress(onProgress),
|
||||
});
|
||||
|
||||
const fileId = response?.data?.file_id as string | undefined;
|
||||
@@ -56,4 +68,72 @@ export const fileService = {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Uploads multiple files in one request. Returns the batch body directly
|
||||
* (this endpoint is not wrapped in the `success`/`data` envelope).
|
||||
*/
|
||||
uploadBatch: async ({
|
||||
files,
|
||||
category,
|
||||
tags = [],
|
||||
description,
|
||||
onProgress,
|
||||
}: IUploadBatchParams): Promise<BatchUploadResult> => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
files.forEach((file) => formData.append("files", file, file.name));
|
||||
if (category) formData.append("category", category);
|
||||
tags.forEach((tag) => formData.append("tags", tag));
|
||||
if (description) formData.append("description", description);
|
||||
|
||||
const response = await api.post(
|
||||
API_ENDPOINTS.FILES.UPLOAD_BATCH,
|
||||
formData,
|
||||
{
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
onUploadProgress: trackUploadProgress(onProgress),
|
||||
},
|
||||
);
|
||||
|
||||
return response.data as BatchUploadResult;
|
||||
} catch (error) {
|
||||
console.error("ERROR caught in File Service Upload Batch", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/** Normalized metadata for a stored file, tolerant of envelope/key variations. */
|
||||
getFileInfo: async (fileId: string): Promise<FileInfo> => {
|
||||
try {
|
||||
const response = await api.get(API_ENDPOINTS.FILES.INFO(fileId));
|
||||
const info = response.data?.data ?? response.data ?? {};
|
||||
|
||||
return {
|
||||
file_id: fileId,
|
||||
filename: info.filename ?? info.name ?? "Document",
|
||||
content_type: info.content_type ?? info.contentType ?? "",
|
||||
size: Number(info.size ?? info.length ?? 0),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("ERROR caught in File Service Get File Info", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
|
||||
/** Downloads a stored file as a blob (caller saves it). */
|
||||
downloadFile: (fileId: string) =>
|
||||
api.get<Blob>(API_ENDPOINTS.FILES.DOWNLOAD(fileId), {
|
||||
responseType: "blob",
|
||||
}),
|
||||
|
||||
/** Permanently removes a file from storage. */
|
||||
deleteFile: async (fileId: string) => {
|
||||
try {
|
||||
return (await api.delete(API_ENDPOINTS.FILES.DELETE(fileId))).data;
|
||||
} catch (error) {
|
||||
console.error("ERROR caught in File Service Delete File", error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
@@ -47,6 +47,7 @@ export interface ICreateCompanyCredentials {
|
||||
address: IAddress;
|
||||
contact_person: IContactPerson;
|
||||
tags: ITags;
|
||||
document_file_ids?: string[];
|
||||
}
|
||||
|
||||
export const companySchema = z.object({
|
||||
@@ -90,6 +91,7 @@ export const companySchema = z.object({
|
||||
certifications: z.array(z.string()).optional(),
|
||||
cpv_codes: z.array(z.string()).optional(),
|
||||
}),
|
||||
document_file_ids: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export const companiesResponseSchema = z.object({
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/** A file that uploaded successfully via a batch / company-documents endpoint. */
|
||||
export interface UploadedFile {
|
||||
file_id: string;
|
||||
filename: string;
|
||||
content_type: string;
|
||||
size: number;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/** A file that failed to upload, surfaced so the user can retry it. */
|
||||
export interface FailedFile {
|
||||
filename: string;
|
||||
error: string;
|
||||
code?: string;
|
||||
}
|
||||
|
||||
/** `data` payload of `POST /companies/:id/documents` (wrapped in ApiResponse). */
|
||||
export interface UploadDocumentsData {
|
||||
document_file_ids: string[];
|
||||
uploaded: UploadedFile[];
|
||||
failed?: FailedFile[];
|
||||
}
|
||||
|
||||
/** Body of `POST /files/upload/batch` — returned directly, not wrapped. */
|
||||
export interface BatchUploadResult {
|
||||
uploaded: UploadedFile[];
|
||||
failed?: FailedFile[];
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/** Normalized metadata for a stored file (`GET /files/:id/info`). */
|
||||
export interface FileInfo {
|
||||
file_id: string;
|
||||
filename: string;
|
||||
content_type: string;
|
||||
size: number;
|
||||
}
|
||||
@@ -47,6 +47,7 @@ export interface ICreateCompanyCredentials {
|
||||
address: IAddress;
|
||||
contact_person?: IContactPerson;
|
||||
tags: ITags;
|
||||
document_file_ids?: string[];
|
||||
}
|
||||
|
||||
export const companySchema = z.object({
|
||||
@@ -92,6 +93,7 @@ export const companySchema = z.object({
|
||||
certifications: z.array(z.string()).or(z.null()).optional(),
|
||||
cpv_codes: z.array(z.string()).or(z.null()).optional(),
|
||||
}),
|
||||
document_file_ids: z.array(z.string()).or(z.null()).optional(),
|
||||
});
|
||||
|
||||
export const companiesResponseSchema = z.object({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./CompanyCategories";
|
||||
export * from "./CompanyDocuments";
|
||||
export * from "./Customers";
|
||||
export * from "./Factory";
|
||||
export * from "./Posts";
|
||||
|
||||
Reference in New Issue
Block a user