mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(bookings): two-level clearance charges (port + misc) billed to customer with invoices
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Badge, Group, Loader, Paper, Text, Timeline } from "@mantine/core";
|
||||
import {
|
||||
CheckCircle2,
|
||||
CircleDot,
|
||||
FileText,
|
||||
MessageSquareWarning,
|
||||
Receipt,
|
||||
Send,
|
||||
Ship,
|
||||
Upload,
|
||||
UserCheck,
|
||||
} from "lucide-react";
|
||||
import type { Freight } from "@edr/types";
|
||||
|
||||
import { bookingsService } from "@/services/bookings.service";
|
||||
import { formatDateTime } from "@/lib/format";
|
||||
|
||||
/** Icon + color per action family; unknown actions fall back to a neutral dot. */
|
||||
function eventMeta(action: string): { icon: typeof Upload; color: string } {
|
||||
if (action === "DOC_APPROVED" || action.endsWith("_ACCEPTED") || action.endsWith("_FINALIZED") || action.endsWith("_CONFIRMED"))
|
||||
return { icon: CheckCircle2, color: "edr-green" };
|
||||
if (action === "DOC_QUERIED" || action.includes("CHANGE_REQUESTED") || action.includes("AMENDMENT"))
|
||||
return { icon: MessageSquareWarning, color: "red" };
|
||||
if (action.startsWith("CHARGE_"))
|
||||
return { icon: Receipt, color: action === "CHARGE_PAID" ? "edr-green" : "orange" };
|
||||
if (action.includes("TRANSIT_ASSIGNEE")) return { icon: UserCheck, color: "blue" };
|
||||
if (action.includes("ORDER")) return { icon: Ship, color: "blue" };
|
||||
if (action.includes("SENT")) return { icon: Send, color: "blue" };
|
||||
if (action.includes("UPLOAD") || action.includes("SUBMITTED"))
|
||||
return { icon: Upload, color: "blue" };
|
||||
if (action.includes("DOC")) return { icon: FileText, color: "gray" };
|
||||
return { icon: CircleDot, color: "gray" };
|
||||
}
|
||||
|
||||
const ACTOR_BADGE: Record<
|
||||
Freight.ClearanceHistoryEvent["actorType"],
|
||||
{ label: string; color: string }
|
||||
> = {
|
||||
STAFF: { label: "Staff", color: "blue" },
|
||||
CUSTOMER: { label: "Customer", color: "grape" },
|
||||
SYSTEM: { label: "System", color: "gray" },
|
||||
};
|
||||
|
||||
/**
|
||||
* Full per-booking clearance action trail: document reviews, phased workflow
|
||||
* steps (transit, declaration, duty, DO/RO, permits) and customer charges —
|
||||
* every event with who did it and when, newest first.
|
||||
*/
|
||||
export function ClearanceHistoryTab({ bookingId }: { bookingId: string }) {
|
||||
const { data: events, isLoading } = useQuery({
|
||||
queryKey: ["clearance-history", bookingId],
|
||||
queryFn: () => bookingsService.getClearanceHistory(bookingId),
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Group justify="center" py="xl" gap={10}>
|
||||
<Loader size="sm" color="edr-green" />
|
||||
<Text c="dimmed">Loading history…</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
if (!events || events.length === 0) {
|
||||
return (
|
||||
<Paper withBorder radius="md" p="lg">
|
||||
<Text size="sm" c="dimmed">
|
||||
No clearance actions recorded yet. Actions from now on — approvals,
|
||||
queries, workflow steps, charges — appear here automatically.
|
||||
</Text>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper withBorder radius="md" p="lg" maw={760}>
|
||||
<Timeline bulletSize={22} lineWidth={2} active={events.length - 1} color="gray">
|
||||
{events.map((ev) => {
|
||||
const meta = eventMeta(ev.action);
|
||||
const Icon = meta.icon;
|
||||
const actor = ACTOR_BADGE[ev.actorType];
|
||||
const note =
|
||||
typeof ev.metadata?.note === "string" ? ev.metadata.note : null;
|
||||
return (
|
||||
<Timeline.Item
|
||||
key={ev.id}
|
||||
color={meta.color}
|
||||
bullet={<Icon size={12} />}
|
||||
title={
|
||||
<Group gap={8} wrap="wrap">
|
||||
<Text fz="13px" fw={600} c="edr-text" lh={1.35}>
|
||||
{ev.label}
|
||||
</Text>
|
||||
<Badge size="xs" variant="light" color={actor.color} radius="sm">
|
||||
{actor.label}
|
||||
</Badge>
|
||||
</Group>
|
||||
}
|
||||
>
|
||||
<Text fz="11.5px" c="dimmed">
|
||||
{ev.actorName ? `${ev.actorName} · ` : ""}
|
||||
{formatDateTime(ev.at)}
|
||||
</Text>
|
||||
{note ? (
|
||||
<Text fz="12px" c="red.8" mt={2}>
|
||||
{note}
|
||||
</Text>
|
||||
) : null}
|
||||
</Timeline.Item>
|
||||
);
|
||||
})}
|
||||
</Timeline>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { Badge, Stack, Tabs, Text } from "@mantine/core";
|
||||
import { AlertTriangle, FileText, Receipt, Share2, ShieldAlert } from "lucide-react";
|
||||
import { AlertTriangle, FileText, History, Receipt, Share2, ShieldAlert } from "lucide-react";
|
||||
import type { Freight } from "@edr/types";
|
||||
|
||||
import { useAuth } from "@/auth/useAuth";
|
||||
@@ -10,6 +10,7 @@ import { AssignRiskCard } from "@/components/contracts/gl-actions/AssignRiskCard
|
||||
import { IncidentReportCard } from "@/components/contracts/gl-actions/IncidentReportCard";
|
||||
import { ClearanceUploadedDocumentsPanel } from "@/components/contracts/ClearanceUploadedDocumentsPanel";
|
||||
import { ClearanceChargesTab } from "@/components/contracts/ClearanceChargesTab";
|
||||
import { ClearanceHistoryTab } from "@/components/contracts/ClearanceHistoryTab";
|
||||
import { GlExchangePanel } from "@/components/contracts/GlExchangePanel";
|
||||
|
||||
export interface ClearanceOpsTabsProps {
|
||||
@@ -112,6 +113,11 @@ export function ClearanceOpsTabs({
|
||||
Customer charges
|
||||
</Tabs.Tab>
|
||||
) : null}
|
||||
{bookingId && showExchange ? (
|
||||
<Tabs.Tab value="history" leftSection={<History size={14} />}>
|
||||
History
|
||||
</Tabs.Tab>
|
||||
) : null}
|
||||
{showOpsTabs && canOps && riskMs ? (
|
||||
<Tabs.Tab value="risk" leftSection={<ShieldAlert size={14} />}>
|
||||
Risk assignment
|
||||
@@ -153,6 +159,12 @@ export function ClearanceOpsTabs({
|
||||
</Tabs.Panel>
|
||||
) : null}
|
||||
|
||||
{bookingId && showExchange ? (
|
||||
<Tabs.Panel value="history">
|
||||
<ClearanceHistoryTab bookingId={bookingId} />
|
||||
</Tabs.Panel>
|
||||
) : null}
|
||||
|
||||
{showOpsTabs && canOps && riskMs && bookingId ? (
|
||||
<Tabs.Panel value="risk">
|
||||
<SectionCard icon={ShieldAlert} title="Customs risk" accent="edr-green">
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
AlertTriangle,
|
||||
ClipboardList,
|
||||
FileText,
|
||||
History,
|
||||
Receipt,
|
||||
Share2,
|
||||
Upload,
|
||||
@@ -38,6 +39,7 @@ import { ContractClearanceReviewSection } from "@/components/contracts/ContractC
|
||||
import { ClearanceWorkflowFilesPanel } from "@/components/contracts/ClearanceWorkflowFilesPanel";
|
||||
import { GlExchangePanel } from "@/components/contracts/GlExchangePanel";
|
||||
import { ClearanceChargesTab } from "@/components/contracts/ClearanceChargesTab";
|
||||
import { ClearanceHistoryTab } from "@/components/contracts/ClearanceHistoryTab";
|
||||
import {
|
||||
GlClearanceUploadModal,
|
||||
type GlClearanceUploadKind,
|
||||
@@ -254,6 +256,11 @@ export default function GlClearanceDetailPage() {
|
||||
Customer charges
|
||||
</Tabs.Tab>
|
||||
) : null}
|
||||
{data.kind === "booking" ? (
|
||||
<Tabs.Tab value="history" leftSection={<History size={14} />}>
|
||||
History
|
||||
</Tabs.Tab>
|
||||
) : null}
|
||||
{incidentBookingId ? (
|
||||
<Tabs.Tab value="incidents" leftSection={<AlertTriangle size={14} />}>
|
||||
Incidents
|
||||
@@ -382,6 +389,12 @@ export default function GlClearanceDetailPage() {
|
||||
</Tabs.Panel>
|
||||
) : null}
|
||||
|
||||
{data.kind === "booking" ? (
|
||||
<Tabs.Panel value="history">
|
||||
<ClearanceHistoryTab bookingId={id!} />
|
||||
</Tabs.Panel>
|
||||
) : null}
|
||||
|
||||
{incidentBookingId ? (
|
||||
<Tabs.Panel value="incidents">
|
||||
<SectionCard icon={AlertTriangle} title="Incident reports" accent="edr-green">
|
||||
|
||||
@@ -432,6 +432,14 @@ export const bookingsService = {
|
||||
return unwrap(response.data) as Freight.ClearanceView;
|
||||
},
|
||||
|
||||
/** Clearance action history — reviews, workflow steps, charges (newest first). */
|
||||
getClearanceHistory: async (
|
||||
id: string,
|
||||
): Promise<Freight.ClearanceHistoryEvent[]> => {
|
||||
const response = await client.get(`/bookings/${id}/clearance/history`);
|
||||
return unwrap(response.data) as Freight.ClearanceHistoryEvent[];
|
||||
},
|
||||
|
||||
// ── Clearance charges (post-finalization customer billing) ──
|
||||
getClearanceCharges: async (id: string): Promise<Freight.ClearanceCharge[]> => {
|
||||
const response = await client.get(`/bookings/${id}/clearance/charges`);
|
||||
|
||||
Reference in New Issue
Block a user