mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 12:18:11 +00:00
Enhance clearance milestone management and introduce new contract actions
- Added new milestones for 'Transit Permit Uploaded' and 'Export Transport Document Issued' in the clearance milestone catalog. - Implemented methods in ClearanceMilestoneService to skip milestones and complete them with metadata. - Updated ContractBookingService to check boundary conditions before booking creation. - Introduced new endpoints in ContractsController for uploading customs declarations, advising duty, and handling various document uploads. - Enhanced the UI to support new clearance actions and display relevant components based on milestone statuses.
This commit is contained in:
@@ -28,6 +28,9 @@ import { PageContainer } from "@/components/page/PageContainer";
|
||||
import { PageHeader } from "@/components/page/PageHeader";
|
||||
import { SectionCard } from "@/components/bookings/detail/SectionCard";
|
||||
import { ContractClearanceReviewSection } from "@/components/contracts/ContractClearanceReviewSection";
|
||||
import { ClearancePhaseStepper } from "@/components/contracts/ClearancePhaseStepper";
|
||||
import { PhasedClearanceActionPanel } from "@/components/contracts/PhasedClearanceActionPanel";
|
||||
import { ClearanceMilestoneTimeline } from "@/components/contracts/ClearanceMilestoneTimeline";
|
||||
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
|
||||
import { contractsService } from "@/services/contracts.service";
|
||||
import { useContractDetail } from "@/hooks/contracts/useContracts";
|
||||
@@ -40,6 +43,7 @@ export default function ContractClearanceDetailPage() {
|
||||
data: clearance,
|
||||
isLoading,
|
||||
isError,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: QUERY_KEYS.CONTRACTS.clearance(id ?? ""),
|
||||
queryFn: () => contractsService.getClearance(id!),
|
||||
@@ -61,7 +65,7 @@ export default function ContractClearanceDetailPage() {
|
||||
const reference = contract?.reference ?? "Clearance";
|
||||
// Customs (Path B) hub. The customer always creates the booking in the portal
|
||||
// after GL finalizes clearance — there is no GL "Create booking" action here.
|
||||
const ready = clearance?.clearanceStatus === "CLEARANCE_READY_FOR_BOOKING";
|
||||
const ready = clearance?.bookingReady ?? clearance?.clearanceStatus === "CLEARANCE_READY_FOR_BOOKING";
|
||||
const clearanceReadOnly = Boolean(
|
||||
contract?.status &&
|
||||
[
|
||||
@@ -152,15 +156,23 @@ export default function ContractClearanceDetailPage() {
|
||||
|
||||
<ClearanceHero contract={contract} stats={stats} />
|
||||
|
||||
{contract?.contractKind === "ONE_TIME" && contract.customsClearingEnabled ? (
|
||||
<Paper withBorder radius="md" p="lg">
|
||||
<ClearancePhaseStepper
|
||||
clearance={clearance}
|
||||
tradeDirection={contract.tradeDirection}
|
||||
/>
|
||||
</Paper>
|
||||
) : null}
|
||||
|
||||
{ready ? (
|
||||
<Alert
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
icon={<PackageCheck size={16} />}
|
||||
title="Clearance finalized"
|
||||
title="Clearance complete"
|
||||
>
|
||||
Customs clearance is complete. The customer can now create the
|
||||
shipment booking from the portal — no further action is needed here.
|
||||
Pre-booking clearance is complete. GL Ethiopia can create the shipment booking.
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
@@ -171,55 +183,81 @@ export default function ContractClearanceDetailPage() {
|
||||
hideSummary
|
||||
selfClear={false}
|
||||
readOnly={clearanceReadOnly}
|
||||
onChanged={() => void refetch()}
|
||||
/>
|
||||
</Grid.Col>
|
||||
|
||||
<Grid.Col span={{ base: 12, lg: 4 }}>
|
||||
<Box style={{ position: "sticky", top: 24 }}>
|
||||
<SectionCard
|
||||
icon={PackageCheck}
|
||||
title="Review progress"
|
||||
accent="edr-green"
|
||||
>
|
||||
<Stack align="center" gap="sm">
|
||||
<RingProgress
|
||||
size={140}
|
||||
thickness={12}
|
||||
roundCaps
|
||||
sections={[{ value: stats.pct, color: "edr-green" }]}
|
||||
label={
|
||||
<Stack gap={0} align="center">
|
||||
<Text fw={800} fz={26} lh={1}>
|
||||
{stats.pct}%
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
approved
|
||||
</Text>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<Group gap="lg" justify="center">
|
||||
<ProgressStat
|
||||
color="edr-green"
|
||||
label="Approved"
|
||||
value={stats.approved}
|
||||
/>
|
||||
<ProgressStat
|
||||
color="red"
|
||||
label="Queried"
|
||||
value={stats.queried}
|
||||
/>
|
||||
<ProgressStat
|
||||
color="gray"
|
||||
label="Pending"
|
||||
value={stats.pending}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
</SectionCard>
|
||||
</Box>
|
||||
<Stack gap="md">
|
||||
{contract?.contractKind === "ONE_TIME" && contract.customsClearingEnabled ? (
|
||||
<PhasedClearanceActionPanel
|
||||
contractId={id!}
|
||||
clearance={clearance}
|
||||
tradeDirection={contract.tradeDirection}
|
||||
roleMode="ALL"
|
||||
onChanged={() => void refetch()}
|
||||
bookingCreateHref={
|
||||
ready ? `/dashboard/contracts/${id}/create-booking` : undefined
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Box style={{ position: "sticky", top: 24 }}>
|
||||
<SectionCard
|
||||
icon={PackageCheck}
|
||||
title="Review progress"
|
||||
accent="edr-green"
|
||||
>
|
||||
<Stack align="center" gap="sm">
|
||||
<RingProgress
|
||||
size={140}
|
||||
thickness={12}
|
||||
roundCaps
|
||||
sections={[{ value: stats.pct, color: "edr-green" }]}
|
||||
label={
|
||||
<Stack gap={0} align="center">
|
||||
<Text fw={800} fz={26} lh={1}>
|
||||
{stats.pct}%
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
approved
|
||||
</Text>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
<Group gap="lg" justify="center">
|
||||
<ProgressStat
|
||||
color="edr-green"
|
||||
label="Approved"
|
||||
value={stats.approved}
|
||||
/>
|
||||
<ProgressStat
|
||||
color="red"
|
||||
label="Queried"
|
||||
value={stats.queried}
|
||||
/>
|
||||
<ProgressStat
|
||||
color="gray"
|
||||
label="Pending"
|
||||
value={stats.pending}
|
||||
/>
|
||||
</Group>
|
||||
</Stack>
|
||||
</SectionCard>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
{clearance.milestones && clearance.milestones.length > 0 ? (
|
||||
<ClearanceMilestoneTimeline
|
||||
milestones={
|
||||
clearance.milestones as Parameters<
|
||||
typeof ClearanceMilestoneTimeline
|
||||
>[0]["milestones"]
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
</Stack>
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { Alert, Grid, Loader, Paper, Stack, Text } from "@mantine/core";
|
||||
import { AlertCircle } from "lucide-react";
|
||||
|
||||
import { PageContainer } from "@/components/page/PageContainer";
|
||||
import { PageHeader } from "@/components/page/PageHeader";
|
||||
import { SectionCard } from "@/components/bookings/detail/SectionCard";
|
||||
import { ContractClearanceReviewSection } from "@/components/contracts/ContractClearanceReviewSection";
|
||||
import { ClearancePhaseStepper } from "@/components/contracts/ClearancePhaseStepper";
|
||||
import { PhasedClearanceActionPanel } from "@/components/contracts/PhasedClearanceActionPanel";
|
||||
import { ClearanceMilestoneTimeline } from "@/components/contracts/ClearanceMilestoneTimeline";
|
||||
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
|
||||
import { contractsService } from "@/services/contracts.service";
|
||||
import { useContractDetail } from "@/hooks/contracts/useContracts";
|
||||
|
||||
export default function GlClearanceDetailPage({
|
||||
backTo,
|
||||
breadcrumbsLabel,
|
||||
roleMode,
|
||||
}: {
|
||||
backTo: string;
|
||||
breadcrumbsLabel: string;
|
||||
roleMode: "ET" | "DJ";
|
||||
}) {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const { data: contract } = useContractDetail(id);
|
||||
const {
|
||||
data: clearance,
|
||||
isLoading,
|
||||
isError,
|
||||
refetch,
|
||||
} = useQuery({
|
||||
queryKey: QUERY_KEYS.CONTRACTS.clearance(id ?? ""),
|
||||
queryFn: () => contractsService.getClearance(id!),
|
||||
enabled: Boolean(id),
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<Stack align="center" py={80}>
|
||||
<Loader color="edr-green" />
|
||||
<Text c="dimmed">Loading clearance…</Text>
|
||||
</Stack>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (isError || !clearance || !contract) {
|
||||
return (
|
||||
<PageContainer>
|
||||
<Alert color="red" icon={<AlertCircle size={16} />}>
|
||||
Could not load clearance for this contract.
|
||||
</Alert>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const bookingHref =
|
||||
roleMode === "ET" ? `/dashboard/contracts/${id}/create-booking` : undefined;
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Stack gap="lg">
|
||||
<PageHeader
|
||||
title={contract.reference}
|
||||
backTo={backTo}
|
||||
breadcrumbs={[
|
||||
{ label: breadcrumbsLabel, href: backTo },
|
||||
{ label: contract.reference },
|
||||
]}
|
||||
/>
|
||||
|
||||
<Paper withBorder radius="md" p="lg">
|
||||
<ClearancePhaseStepper
|
||||
clearance={clearance}
|
||||
tradeDirection={contract.tradeDirection}
|
||||
/>
|
||||
</Paper>
|
||||
|
||||
<Grid>
|
||||
<Grid.Col span={{ base: 12, lg: roleMode === "DJ" ? 12 : 7 }}>
|
||||
{roleMode === "ET" ? (
|
||||
<ContractClearanceReviewSection
|
||||
contractId={id!}
|
||||
hideSummary
|
||||
selfClear={false}
|
||||
readOnly={false}
|
||||
onChanged={() => void refetch()}
|
||||
/>
|
||||
) : (
|
||||
<SectionCard title="Contract context" accent="edr-green">
|
||||
<Text size="sm" c="dimmed" mb="sm">
|
||||
Review upstream status before uploading Djibouti documents.
|
||||
</Text>
|
||||
<ContractClearanceReviewSection
|
||||
contractId={id!}
|
||||
hideSummary
|
||||
selfClear={false}
|
||||
readOnly
|
||||
/>
|
||||
</SectionCard>
|
||||
)}
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{ base: 12, lg: roleMode === "DJ" ? 12 : 5 }}>
|
||||
<PhasedClearanceActionPanel
|
||||
contractId={id!}
|
||||
clearance={clearance}
|
||||
tradeDirection={contract.tradeDirection}
|
||||
roleMode={roleMode}
|
||||
onChanged={() => void refetch()}
|
||||
bookingCreateHref={bookingHref}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
{clearance.milestones && clearance.milestones.length > 0 ? (
|
||||
<ClearanceMilestoneTimeline
|
||||
milestones={clearance.milestones as Parameters<typeof ClearanceMilestoneTimeline>[0]["milestones"]}
|
||||
/>
|
||||
) : null}
|
||||
</Stack>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Badge, Card, Group, Loader, Stack, Text } from "@mantine/core";
|
||||
import { ChevronRight, Ship } from "lucide-react";
|
||||
|
||||
import { PageContainer } from "@/components/page/PageContainer";
|
||||
import { PageHeader } from "@/components/page/PageHeader";
|
||||
import { useDjClearanceQueue } from "@/hooks/contracts/useContracts";
|
||||
|
||||
export default function GlDjiboutiClearanceListPage() {
|
||||
const navigate = useNavigate();
|
||||
const { data, isLoading } = useDjClearanceQueue();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
title="GL Djibouti — Clearance"
|
||||
subtitle="Contracts awaiting Djibouti GL action (DO / RO)."
|
||||
/>
|
||||
{isLoading ? (
|
||||
<Group justify="center" py={60}>
|
||||
<Loader color="edr-green" />
|
||||
</Group>
|
||||
) : (
|
||||
<Stack gap="sm">
|
||||
{(data?.items ?? []).length === 0 ? (
|
||||
<Text c="dimmed" ta="center" py="xl">
|
||||
No contracts need Djibouti GL action right now.
|
||||
</Text>
|
||||
) : (
|
||||
(data?.items ?? []).map((c) => (
|
||||
<Card
|
||||
key={c.id}
|
||||
withBorder
|
||||
radius="md"
|
||||
padding="md"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => navigate(`/dashboard/gl-djibouti/clearance/${c.id}`)}
|
||||
>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group gap="sm">
|
||||
<Ship size={18} className="text-[color:var(--freight-brand)]" />
|
||||
<div>
|
||||
<Text fw={700}>{c.reference}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{c.tradeDirection} · {c.status}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Badge variant="light" color="cyan">
|
||||
{c.tradeDirection}
|
||||
</Badge>
|
||||
<ChevronRight size={18} className="text-muted-foreground" />
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Badge, Card, Group, Loader, Stack, Text } from "@mantine/core";
|
||||
import { ChevronRight, Flag } from "lucide-react";
|
||||
|
||||
import { PageContainer } from "@/components/page/PageContainer";
|
||||
import { PageHeader } from "@/components/page/PageHeader";
|
||||
import { useEtClearanceQueue } from "@/hooks/contracts/useContracts";
|
||||
|
||||
export default function GlEthiopiaClearanceListPage() {
|
||||
const navigate = useNavigate();
|
||||
const { data, isLoading } = useEtClearanceQueue();
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
title="GL Ethiopia — Clearance"
|
||||
subtitle="Contracts awaiting Ethiopia GL action in the phased clearance workflow."
|
||||
/>
|
||||
{isLoading ? (
|
||||
<Group justify="center" py={60}>
|
||||
<Loader color="edr-green" />
|
||||
</Group>
|
||||
) : (
|
||||
<Stack gap="sm">
|
||||
{(data?.items ?? []).length === 0 ? (
|
||||
<Text c="dimmed" ta="center" py="xl">
|
||||
No contracts need ET GL action right now.
|
||||
</Text>
|
||||
) : (
|
||||
(data?.items ?? []).map((c) => (
|
||||
<Card
|
||||
key={c.id}
|
||||
withBorder
|
||||
radius="md"
|
||||
padding="md"
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => navigate(`/dashboard/gl-ethiopia/clearance/${c.id}`)}
|
||||
>
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Group gap="sm">
|
||||
<Flag size={18} className="text-[color:var(--freight-brand)]" />
|
||||
<div>
|
||||
<Text fw={700}>{c.reference}</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
{c.tradeDirection} · {c.status}
|
||||
</Text>
|
||||
</div>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Badge variant="light" color="edr-green">
|
||||
{c.tradeDirection}
|
||||
</Badge>
|
||||
<ChevronRight size={18} className="text-muted-foreground" />
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user