mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 23:35:42 +00:00
284 lines
9.4 KiB
TypeScript
284 lines
9.4 KiB
TypeScript
import { useState } from "react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { useParams } from "react-router-dom";
|
|
import {
|
|
Alert,
|
|
Badge,
|
|
Box,
|
|
Button,
|
|
Grid,
|
|
Group,
|
|
Loader,
|
|
Stack,
|
|
Tabs,
|
|
Text,
|
|
ThemeIcon,
|
|
} from "@mantine/core";
|
|
import { AlertCircle, ClipboardList, FileText, Upload } from "lucide-react";
|
|
import type { Freight } from "@edr/types";
|
|
|
|
import { PageContainer } from "@/components/page/PageContainer";
|
|
import { PageHeader } from "@/components/page/PageHeader";
|
|
import { ClearanceReviewSection } from "@/components/bookings/detail/ClearanceReviewSection";
|
|
import { ContractClearanceReviewSection } from "@/components/contracts/ContractClearanceReviewSection";
|
|
import { ClearanceWorkflowFilesPanel } from "@/components/contracts/ClearanceWorkflowFilesPanel";
|
|
import {
|
|
GlClearanceUploadModal,
|
|
type GlClearanceUploadKind,
|
|
} from "@/components/contracts/GlClearanceUploadModal";
|
|
import { PhasedClearanceActionPanel } from "@/components/contracts/PhasedClearanceActionPanel";
|
|
import { findWorkflowFile } from "@/components/contracts/PhasedUploadedFileRow";
|
|
import { useFileViewer } from "@/hooks/useFileViewer";
|
|
import { useBookingMilestones } from "@/hooks/contracts/useContracts";
|
|
import { contractsService } from "@/services/contracts.service";
|
|
import { bookingsService } from "@/services/bookings.service";
|
|
import { downloadBookingFile } from "@/services/files.service";
|
|
|
|
type GlClearanceDetail =
|
|
| {
|
|
kind: "contract";
|
|
reference: string;
|
|
tradeDirection: string;
|
|
clearance: Freight.ContractClearanceView;
|
|
}
|
|
| {
|
|
kind: "booking";
|
|
reference: string;
|
|
tradeDirection: string;
|
|
clearance: Freight.ClearanceView;
|
|
};
|
|
|
|
async function loadGlClearanceDetail(id: string): Promise<GlClearanceDetail> {
|
|
try {
|
|
const [clearance, contract] = await Promise.all([
|
|
contractsService.getClearance(id),
|
|
contractsService.getById(id),
|
|
]);
|
|
return {
|
|
kind: "contract",
|
|
reference: contract.reference,
|
|
tradeDirection: contract.tradeDirection,
|
|
clearance,
|
|
};
|
|
} catch {
|
|
const [clearance, booking] = await Promise.all([
|
|
bookingsService.getClearance(id),
|
|
bookingsService.getById(id),
|
|
]);
|
|
return {
|
|
kind: "booking",
|
|
reference: booking.reference,
|
|
tradeDirection: booking.tradeDirection,
|
|
clearance,
|
|
};
|
|
}
|
|
}
|
|
|
|
/** Djibouti GL clearance detail — RO/DO upload and read-only upstream context. */
|
|
export default function GlClearanceDetailPage() {
|
|
const { id } = useParams<{ id: string }>();
|
|
const { view, viewer } = useFileViewer();
|
|
const [uploadKind, setUploadKind] = useState<GlClearanceUploadKind | null>(null);
|
|
|
|
const { data, isLoading, isError, refetch } = useQuery({
|
|
queryKey: ["gl-clearance-detail", id],
|
|
queryFn: () => loadGlClearanceDetail(id!),
|
|
enabled: Boolean(id),
|
|
});
|
|
|
|
const linkedBookingId =
|
|
data?.kind === "contract" ? (data.clearance.linkedBookingId ?? undefined) : undefined;
|
|
const { data: bookingMilestones, refetch: refetchBookingMilestones } =
|
|
useBookingMilestones(linkedBookingId);
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<PageContainer>
|
|
<Stack align="center" py={80}>
|
|
<Loader color="edr-green" />
|
|
<Text c="dimmed">Loading clearance…</Text>
|
|
</Stack>
|
|
</PageContainer>
|
|
);
|
|
}
|
|
|
|
if (isError || !data) {
|
|
return (
|
|
<PageContainer>
|
|
<Alert color="red" icon={<AlertCircle size={16} />}>
|
|
Could not load clearance for this item.
|
|
</Alert>
|
|
</PageContainer>
|
|
);
|
|
}
|
|
|
|
const backTo = "/dashboard/gl-djibouti/clearance";
|
|
const workflowFiles = data.clearance.workflowFiles ?? [];
|
|
const workflowFileCount = workflowFiles.filter((f) => f.file).length;
|
|
const isImport = data.tradeDirection === "IMPORT";
|
|
const hasDo = Boolean(findWorkflowFile(workflowFiles, "delivery_order"));
|
|
const hasRo = Boolean(findWorkflowFile(workflowFiles, "release_order"));
|
|
// DO upload is un-gated — Djibouti GL may attach it at any point, any file type.
|
|
const canUploadDo = isImport;
|
|
const vesselDepartureDate =
|
|
"vesselDepartureDate" in data.clearance
|
|
? (data.clearance.vesselDepartureDate ?? null)
|
|
: null;
|
|
|
|
return (
|
|
<PageContainer>
|
|
<Stack gap="lg">
|
|
<PageHeader
|
|
title={data.reference}
|
|
backTo={backTo}
|
|
breadcrumbs={[
|
|
{ label: "GL Djibouti Clearance", href: backTo },
|
|
{ label: data.reference },
|
|
]}
|
|
meta={
|
|
<Badge variant="light" color={isImport ? "edr-green" : "gray"} radius="sm">
|
|
{data.tradeDirection}
|
|
</Badge>
|
|
}
|
|
action={
|
|
<Group gap="sm">
|
|
{isImport ? (
|
|
<Button
|
|
color="edr-green"
|
|
leftSection={<Upload size={16} />}
|
|
disabled={!canUploadDo}
|
|
onClick={() => setUploadKind("do")}
|
|
>
|
|
{hasDo ? "Replace DO" : "Upload DO"}
|
|
</Button>
|
|
) : (
|
|
<Button
|
|
color="edr-green"
|
|
leftSection={<Upload size={16} />}
|
|
onClick={() => setUploadKind("ro")}
|
|
>
|
|
{hasRo ? "Replace RO" : "Upload RO"}
|
|
</Button>
|
|
)}
|
|
</Group>
|
|
}
|
|
/>
|
|
|
|
<Tabs defaultValue="workflow" keepMounted={false}>
|
|
<Tabs.List mb="md">
|
|
<Tabs.Tab value="workflow" leftSection={<ClipboardList size={14} />}>
|
|
Clearance workflow
|
|
</Tabs.Tab>
|
|
<Tabs.Tab
|
|
value="documents"
|
|
leftSection={<FileText size={14} />}
|
|
rightSection={
|
|
workflowFileCount > 0 ? (
|
|
<Badge size="xs" variant="light" color="edr-green" circle>
|
|
{workflowFileCount}
|
|
</Badge>
|
|
) : undefined
|
|
}
|
|
>
|
|
Customs documents (all steps)
|
|
</Tabs.Tab>
|
|
</Tabs.List>
|
|
|
|
<Tabs.Panel value="workflow">
|
|
<Grid>
|
|
<Grid.Col span={{ base: 12, lg: 7 }}>
|
|
{data.kind === "booking" ? (
|
|
<ClearanceReviewSection
|
|
bookingId={id!}
|
|
hideSummary
|
|
readOnly
|
|
phasedCustoms
|
|
/>
|
|
) : (
|
|
<ContractClearanceReviewSection
|
|
contractId={id!}
|
|
hideSummary
|
|
selfClear={false}
|
|
readOnly
|
|
phasedCustoms
|
|
/>
|
|
)}
|
|
</Grid.Col>
|
|
|
|
<Grid.Col span={{ base: 12, lg: 5 }}>
|
|
<PhasedClearanceActionPanel
|
|
contractId={data.kind === "contract" ? id : undefined}
|
|
bookingId={data.kind === "booking" ? id : linkedBookingId}
|
|
bookingCreated={data.kind === "booking" || Boolean(linkedBookingId)}
|
|
bookingMilestones={
|
|
data.kind === "booking"
|
|
? (data.clearance.milestones ?? [])
|
|
: (bookingMilestones ?? [])
|
|
}
|
|
clearance={data.clearance}
|
|
tradeDirection={data.tradeDirection}
|
|
workflowFiles={workflowFiles}
|
|
roleMode="DJ"
|
|
useUploadModals
|
|
onUploadDoRequest={() => setUploadKind("do")}
|
|
onUploadRoRequest={() => setUploadKind("ro")}
|
|
onChanged={() => {
|
|
void refetch();
|
|
void refetchBookingMilestones();
|
|
}}
|
|
onViewFile={view}
|
|
onDownloadFile={(f) => void downloadBookingFile(f.id, f.name)}
|
|
/>
|
|
</Grid.Col>
|
|
</Grid>
|
|
</Tabs.Panel>
|
|
|
|
<Tabs.Panel value="documents">
|
|
{workflowFiles.length > 0 ? (
|
|
<ClearanceWorkflowFilesPanel
|
|
files={workflowFiles}
|
|
title="Customs documents (all steps)"
|
|
onView={view}
|
|
onDownload={(f) => void downloadBookingFile(f.id, f.name)}
|
|
/>
|
|
) : (
|
|
<Box
|
|
py={48}
|
|
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={48}>
|
|
<FileText size={22} />
|
|
</ThemeIcon>
|
|
<Text size="sm" c="dimmed" maw={360}>
|
|
No customs workflow documents uploaded yet. Files from Ethiopia-side
|
|
clearance and your DO/RO uploads will appear here.
|
|
</Text>
|
|
</Stack>
|
|
</Box>
|
|
)}
|
|
</Tabs.Panel>
|
|
</Tabs>
|
|
</Stack>
|
|
|
|
<GlClearanceUploadModal
|
|
opened={uploadKind != null}
|
|
kind={uploadKind}
|
|
onClose={() => setUploadKind(null)}
|
|
entityId={id!}
|
|
isBooking={data.kind === "booking"}
|
|
workflowFiles={workflowFiles}
|
|
vesselDepartureDate={vesselDepartureDate}
|
|
onSuccess={() => void refetch()}
|
|
onPreview={view}
|
|
/>
|
|
{viewer}
|
|
</PageContainer>
|
|
);
|
|
}
|