Files
edr-platform/apps/edr-freight-web/backoffice/src/components/contracts/ClearanceUploadedDocumentsPanel.tsx
2026-07-02 12:06:17 +03:00

206 lines
5.6 KiB
TypeScript

import { useMemo } from "react";
import { Badge, Box, Stack, Tabs, Text, ThemeIcon } from "@mantine/core";
import { FileText, Receipt, Ship, Truck } from "lucide-react";
import type { Freight } from "@edr/types";
import { SectionCard } from "@/components/bookings/detail/SectionCard";
import { PhasedUploadedFileRow } from "@/components/contracts/PhasedUploadedFileRow";
type TabValue = Freight.ClearanceWorkflowFileCategory;
type TabConfig = {
value: TabValue;
label: string;
icon: typeof FileText;
emptyHint: string;
};
function tabConfigForTradeDirection(tradeDirection: string): TabConfig[] {
if (tradeDirection === "EXPORT") {
return [
{
value: "declaration",
label: "Declaration",
icon: FileText,
emptyHint: "No declaration uploaded yet.",
},
{
value: "djibouti",
label: "Release order",
icon: Ship,
emptyHint: "No release order uploaded yet.",
},
{
value: "transit",
label: "Transit Permit",
icon: Truck,
emptyHint: "No transit permit uploaded yet.",
},
];
}
return [
{
value: "declaration",
label: "Declaration",
icon: FileText,
emptyHint: "No declaration uploaded yet.",
},
{
value: "duty",
label: "Duty notice",
icon: Receipt,
emptyHint: "No duty notice or payment slip uploaded yet.",
},
{
value: "transit",
label: "Transit permit",
icon: Truck,
emptyHint: "No transit permit uploaded yet.",
},
];
}
function subtitleForTradeDirection(tradeDirection: string): string {
return tradeDirection === "EXPORT"
? "Declaration, release order, and transit permit files for this clearance."
: "Declaration, duty notice, and transit permit files for this clearance.";
}
function footerHintForTradeDirection(tradeDirection: string): string {
return tradeDirection === "EXPORT"
? "Files appear here once GL uploads the declaration and release order, and after booking when the transit permit is uploaded."
: "Files appear here once GL Ethiopia uploads declaration, duty notice, or transit permit documents.";
}
export interface ClearanceUploadedDocumentsPanelProps {
files: Freight.ClearanceWorkflowFile[];
tradeDirection?: string;
onView: (file: { name: string; url: string }) => void;
onDownload?: (file: { id: string; name: string }) => void;
}
export function ClearanceUploadedDocumentsPanel({
files,
tradeDirection = "IMPORT",
onView,
onDownload,
}: ClearanceUploadedDocumentsPanelProps) {
const tabConfig = useMemo(
() => tabConfigForTradeDirection(tradeDirection),
[tradeDirection],
);
const isExport = tradeDirection === "EXPORT";
const visibleFiles = useMemo(
() =>
isExport ? files.filter((f) => f.category !== "duty") : files,
[files, isExport],
);
const uploadedCount = visibleFiles.filter((f) => f.file).length;
const defaultTab =
tabConfig.find((tab) =>
visibleFiles.some((f) => f.category === tab.value && f.file),
)?.value ?? tabConfig[0]?.value ?? "declaration";
return (
<SectionCard
icon={FileText}
title="Uploaded customs documents"
subtitle={subtitleForTradeDirection(tradeDirection)}
accent="edr-green"
>
<Tabs defaultValue={defaultTab} keepMounted={false}>
<Tabs.List mb="md">
{tabConfig.map((tab) => {
const count = visibleFiles.filter(
(f) => f.category === tab.value && f.file,
).length;
const Icon = tab.icon;
return (
<Tabs.Tab
key={tab.value}
value={tab.value}
leftSection={<Icon size={14} />}
rightSection={
count > 0 ? (
<Badge size="xs" variant="light" color="edr-green" circle>
{count}
</Badge>
) : undefined
}
>
{tab.label}
</Tabs.Tab>
);
})}
</Tabs.List>
{tabConfig.map((tab) => {
const items = visibleFiles.filter(
(f) => f.category === tab.value && f.file,
);
const Icon = tab.icon;
return (
<Tabs.Panel key={tab.value} value={tab.value}>
{items.length > 0 ? (
<Stack gap="sm">
{items.map((item) => (
<PhasedUploadedFileRow
key={item.code}
label={item.label}
file={item.file!}
onView={onView}
onDownload={onDownload}
/>
))}
</Stack>
) : (
<EmptyTabState icon={Icon} hint={tab.emptyHint} />
)}
</Tabs.Panel>
);
})}
</Tabs>
{uploadedCount === 0 ? (
<Text size="xs" c="dimmed" mt="md">
{footerHintForTradeDirection(tradeDirection)}
</Text>
) : null}
</SectionCard>
);
}
function EmptyTabState({
icon: Icon,
hint,
}: {
icon: typeof FileText;
hint: string;
}) {
return (
<Box
py={40}
style={{
borderRadius: 12,
border: "1px dashed var(--mantine-color-gray-4)",
background: "var(--mantine-color-gray-0)",
textAlign: "center",
}}
>
<Stack gap={8} align="center">
<ThemeIcon variant="light" color="gray" radius="xl" size={44}>
<Icon size={20} />
</ThemeIcon>
<Text size="sm" c="dimmed" maw={320}>
{hint}
</Text>
</Stack>
</Box>
);
}