diff --git a/apps/edr-freight-web/portal/src/hooks/useFileViewer.tsx b/apps/edr-freight-web/portal/src/hooks/useFileViewer.tsx
index 4ca24476c..c00777ef7 100644
--- a/apps/edr-freight-web/portal/src/hooks/useFileViewer.tsx
+++ b/apps/edr-freight-web/portal/src/hooks/useFileViewer.tsx
@@ -1,24 +1,3 @@
-import { useCallback, useState } from "react";
-import { FileViewerModal, type ViewableFile } from "@edr/ui-common";
-
-/**
- * Drives a single shared {@link FileViewerModal} for a page. Call `view(file)`
- * from any file row to open the document inline (pdf / image / video / office /
- * text); render `viewer` once near the page root.
- *
- * const { view, viewer } = useFileViewer();
- *
- * {viewer}
- */
-export function useFileViewer() {
- const [file, setFile] = useState(null);
-
- const view = useCallback((f: ViewableFile) => setFile(f), []);
- const close = useCallback(() => setFile(null), []);
-
- const viewer = (
-
- );
-
- return { view, close, viewer };
-}
+// Re-export of the shared hook, now living in @edr/ui-common. Kept so existing
+// `@/hooks/useFileViewer` imports keep working.
+export { useFileViewer } from "@edr/ui-common";
diff --git a/apps/edr-freight-web/portal/src/pages/settings/TabDocuments.tsx b/apps/edr-freight-web/portal/src/pages/settings/TabDocuments.tsx
index 4d5c4b759..6fbcbe911 100644
--- a/apps/edr-freight-web/portal/src/pages/settings/TabDocuments.tsx
+++ b/apps/edr-freight-web/portal/src/pages/settings/TabDocuments.tsx
@@ -4,10 +4,12 @@ import { getMinFiles } from "@/types/fileUploadSettings";
import type { ProfileResponse } from "@/types/profile";
import { SmartFileInput } from "@edr/ui-common";
import {
+ Anchor,
Button,
Card,
Center,
Group,
+ Stack,
Text,
Title,
} from "@mantine/core";
@@ -17,10 +19,19 @@ import {
CheckCircle2,
FileCheck,
Loader2,
+ Paperclip,
UploadCloud,
XCircle,
} from "lucide-react";
-import { useState } from "react";
+import { useMemo, useState } from "react";
+
+const ROLE_LABELS: Record = {
+ importer: "Importer",
+ exporter: "Exporter",
+ freight_forwarder: "Freight Forwarder",
+ dj_freight_forwarder: "DJ Freight Forwarder",
+ transporter: "Transporter",
+};
interface TabDocumentsProps {
profile: ProfileResponse;
@@ -28,21 +39,59 @@ interface TabDocumentsProps {
onContinue?: () => void;
}
-export default function TabDocuments({ profile, mode = "edit", onContinue }: TabDocumentsProps) {
+function documentSettingCode(nationality: string | null | undefined): string {
+ return nationality === "foreign"
+ ? "company_onboarding_documents_foreign"
+ : "company_onboarding_documents_ethiopian";
+}
+
+export default function TabDocuments({
+ profile,
+ mode = "edit",
+ onContinue,
+}: TabDocumentsProps) {
const queryClient = useQueryClient();
- const [documentFiles, setDocumentFiles] = useState>({});
+ const [documentFiles, setDocumentFiles] = useState<
+ Record
+ >({});
const docSettingQuery = useQuery(
api.fileUploadSettings.getByCode.queryOptions({
- input: { code: "customer_file_documents" },
+ input: { code: documentSettingCode(profile.nationality) },
}),
);
+ const docsQuery = useQuery(
+ api.companies.documents.queryOptions({
+ input: { companyId: profile.companyId },
+ }),
+ );
+
+ const uploadedKeys = useMemo(
+ () => (docsQuery.data ?? []).map((d) => d.code),
+ [docsQuery.data],
+ );
+
+ const existingFilesByKey = useMemo(() => {
+ const map: Record =
+ {};
+ for (const doc of docsQuery.data ?? []) {
+ (map[doc.code] ??= []).push({
+ name: doc.name,
+ url: doc.url,
+ size: doc.size,
+ });
+ }
+ return map;
+ }, [docsQuery.data]);
+
const docUploadMutation = useMutation({
mutationFn: (files: Record) =>
companiesService.uploadDocuments(profile.companyId, files),
onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: api.companies.getProfile.queryKey() });
+ queryClient.invalidateQueries({
+ queryKey: api.companies.getProfile.queryKey(),
+ });
},
});
@@ -73,6 +122,7 @@ export default function TabDocuments({ profile, mode = "edit", onContinue }: Tab
for (const field of docSettingQuery.data?.fields ?? []) {
const min = getMinFiles(field);
if (min <= 0) continue;
+ if (uploadedKeys.includes(field.fileKey)) continue;
const v = documentFiles[field.fileKey];
const count = Array.isArray(v) ? v.length : v ? 1 : 0;
if (count < min) {
@@ -82,94 +132,141 @@ export default function TabDocuments({ profile, mode = "edit", onContinue }: Tab
return errs;
};
+ const licenseProfiles = profile.companyProfiles.filter(
+ (p) => p.licenseFiles && p.licenseFiles.length > 0,
+ );
+
return (
-
-
-
- Documents
-
-
- Upload and manage required business documents
-
-
- {docSettingQuery.isLoading ? (
-
-
-
- ) : !docSettingQuery.data ? (
-
- No document requirements configured for your account.
+ <>
+
+
+
+ Documents
+
+
+ Upload and manage required business documents
- ) : (
-
- )}
- {docSettingQuery.data && (
-
-
- {docUploadMutation.isSuccess && (
-
-
-
- {mode === "onboarding" ? "Saved successfully" : "Documents uploaded successfully"}
-
-
- )}
- {docUploadMutation.isError && (
-
-
- Upload failed
-
+ {docSettingQuery.isLoading ? (
+
+
+
+ ) : !docSettingQuery.data ? (
+
+ No document requirements configured for your account.
+
+ ) : (
+
+ )}
+
+ {docSettingQuery.data && (
+
+
+ {docUploadMutation.isSuccess && (
+
+
+
+ {mode === "onboarding"
+ ? "Saved successfully"
+ : "Documents uploaded successfully"}
+
+
+ )}
+ {docUploadMutation.isError && (
+
+
+
+ Upload failed
+
+
+ )}
+
+ {mode === "onboarding" ? (
+ }
+ loading={docUploadMutation.isPending}
+ onClick={() => {
+ const validationErrors = validateRequired();
+ if (Object.keys(validationErrors).length > 0) {
+ setFieldErrors(validationErrors);
+ return;
+ }
+ if (hasFiles) {
+ docUploadMutation.mutate(documentFiles, {
+ onSuccess: () => onContinue?.(),
+ });
+ } else {
+ onContinue?.();
+ }
+ }}
+ >
+ Continue
+
+ ) : (
+ }
+ loading={docUploadMutation.isPending}
+ disabled={!hasFiles}
+ onClick={() => {
+ if (!hasFiles) return;
+ docUploadMutation.mutate(documentFiles);
+ }}
+ >
+ Upload Documents
+
)}
- {mode === "onboarding" ? (
- }
- loading={docUploadMutation.isPending}
- onClick={() => {
- const validationErrors = validateRequired();
- if (Object.keys(validationErrors).length > 0) {
- setFieldErrors(validationErrors);
- return;
- }
- if (hasFiles) {
- docUploadMutation.mutate(documentFiles, {
- onSuccess: () => onContinue?.(),
- });
- } else {
- onContinue?.();
- }
- }}
- >
- Continue
-
- ) : (
- }
- loading={docUploadMutation.isPending}
- disabled={!hasFiles}
- onClick={() => {
- if (!hasFiles) return;
- docUploadMutation.mutate(documentFiles);
- }}
- >
- Upload Documents
-
- )}
-
+ )}
+
+
+ {licenseProfiles.length > 0 && (
+
+
+
+ Business licenses
+
+
+ License documents uploaded per operational profile
+
+
+
+ {licenseProfiles.map((p) => (
+
+
+ {ROLE_LABELS[p.type] ?? p.type} ยท {p.reference}
+
+ {p.licenseFiles.map((f) => (
+
+
+
+ {f.name}
+
+
+ ))}
+
+ ))}
+
+
)}
-
+ >
);
}
diff --git a/apps/edr-freight-web/portal/src/services/api.ts b/apps/edr-freight-web/portal/src/services/api.ts
index a09bd4b4e..1ba83ec30 100644
--- a/apps/edr-freight-web/portal/src/services/api.ts
+++ b/apps/edr-freight-web/portal/src/services/api.ts
@@ -53,6 +53,7 @@ import {
UpdateDropdownSettingDto,
} from "@/types/dropdownSettings";
import type {
+ CompanyDocument,
CompanyInfoResponse,
CompanyNationality,
CompanyProfileResponse,
@@ -158,7 +159,11 @@ export const api = {
createCompanyProfile: endpoint<
{ type: ProfileTypeValue; businessLicense?: string },
CompanyProfileResponse
- >("companies", "createCompanyProfile", companiesService.createCompanyProfile),
+ >(
+ "companies",
+ "createCompanyProfile",
+ companiesService.createCompanyProfile,
+ ),
startOnboarding: endpoint<
{
@@ -192,6 +197,12 @@ export const api = {
"onboardingRequirements",
companiesService.getOnboardingRequirements,
),
+
+ documents: endpoint<{ companyId: string }, CompanyDocument[]>(
+ "companies",
+ "documents",
+ ({ companyId }) => companiesService.getDocuments(companyId),
+ ),
},
bookings: {
@@ -228,7 +239,8 @@ export const api = {
downloadHandoverDocument: endpoint<{ inventoryId: string }, Blob>(
"bookings",
"downloadHandoverDocument",
- ({ inventoryId }) => bookingsService.downloadHandoverDocument(inventoryId),
+ ({ inventoryId }) =>
+ bookingsService.downloadHandoverDocument(inventoryId),
),
create: endpoint<
@@ -312,11 +324,8 @@ export const api = {
proceedToOperation: endpoint<
{ id: string; scheduledDate: string },
Freight.IBooking
- >(
- "bookings",
- "proceedToOperation",
- ({ id, scheduledDate }) =>
- bookingsService.proceedToOperation(id, scheduledDate),
+ >("bookings", "proceedToOperation", ({ id, scheduledDate }) =>
+ bookingsService.proceedToOperation(id, scheduledDate),
),
checkPayment: endpoint<{ orderId: string }, { status: string }>(
@@ -354,10 +363,11 @@ export const api = {
bookingsService.getAvailableDays({ originYardId, destinationYardId }),
),
- getAvailableDaysForCargo: endpoint(
- "train-scheduling",
- "availableDaysForCargo",
- (input) => bookingsService.getAvailableDaysForCargo(input),
+ getAvailableDaysForCargo: endpoint<
+ Freight.AvailableDaysForCargoQuery,
+ string[]
+ >("train-scheduling", "availableDaysForCargo", (input) =>
+ bookingsService.getAvailableDaysForCargo(input),
),
getMyBookingWindows: endpoint(
diff --git a/apps/edr-freight-web/portal/src/services/companies.service.ts b/apps/edr-freight-web/portal/src/services/companies.service.ts
index d326811b8..d3f584170 100644
--- a/apps/edr-freight-web/portal/src/services/companies.service.ts
+++ b/apps/edr-freight-web/portal/src/services/companies.service.ts
@@ -82,6 +82,18 @@ export interface CompanyInfoResponse {
company: CompanyResponse;
}
+/** A single company-level document uploaded against a `file_upload_settings` field. */
+export interface CompanyDocument {
+ id: string;
+ name: string;
+ /** The `fileKey` of the setting field it was uploaded against. */
+ code: string;
+ mimeType: string;
+ size: number;
+ uploadedAt: string;
+ url: string;
+}
+
/** A single onboarding document field, as resolved and described by the backend. */
export interface OnboardingDocumentField {
fileKey: string;
@@ -124,7 +136,12 @@ export interface OnboardingRequirements {
}
export interface CompanyProfileInput {
- type: "importer" | "exporter" | "freight_forwarder" | "dj_freight_forwarder" | "transporter";
+ type:
+ | "importer"
+ | "exporter"
+ | "freight_forwarder"
+ | "dj_freight_forwarder"
+ | "transporter";
businessLicense?: string;
}
@@ -180,7 +197,9 @@ export const companiesService = {
}
},
- create: async (payload: CreateCompanyPayload): Promise => {
+ create: async (
+ payload: CreateCompanyPayload,
+ ): Promise => {
const response = await client.post>(
URL_CONSTANTS.COMPANIES_API.CREATE,
payload,
@@ -195,7 +214,9 @@ export const companiesService = {
return unwrap(response.data);
},
- updateProfile: async (payload: UpdateProfilePayload): Promise => {
+ updateProfile: async (
+ payload: UpdateProfilePayload,
+ ): Promise => {
const response = await client.patch>(
URL_CONSTANTS.COMPANIES_API.PROFILE,
payload,
@@ -293,7 +314,18 @@ export const companiesService = {
formData.append(fieldName, fileOrFiles);
}
}
- await client.post(URL_CONSTANTS.COMPANIES_API.DOCUMENTS(companyId), formData);
+ await client.post(
+ URL_CONSTANTS.COMPANIES_API.DOCUMENTS(companyId),
+ formData,
+ );
+ },
+
+ /** List documents already uploaded for a company (settings-driven, by fileKey). */
+ getDocuments: async (companyId: string): Promise => {
+ const response = await client.get>(
+ URL_CONSTANTS.COMPANIES_API.DOCUMENTS(companyId),
+ );
+ return unwrap(response.data);
},
/** Upload business-license document(s) for a company profile (multi-file). */