import type { Freight } from "@edr/types"; import { Alert, Badge, Button, Group, Paper, Stack, Text, TextInput, Textarea, } from "@mantine/core"; import { useMutation } from "@tanstack/react-query"; import { CheckCircle2, Clock, Send, UserCheck } from "lucide-react"; import { useState } from "react"; import toast from "react-hot-toast"; import { contractsService } from "@/services/contracts.service"; export interface TransitAssigneePanelProps { contractId: string; transitAssignee: Freight.ContractClearanceView["transitAssignee"]; /** * ET asks and waits; DJ answers with a name. The same state renders from both * desks — only the action on offer differs. */ side: "ET" | "DJ"; /** Hide the action (read-only audit view, or the user lacks the permission). */ readOnly?: boolean; onChanged?: () => void; } const fmt = (iso?: string | null) => iso ? new Date(iso).toLocaleString("en-GB", { day: "numeric", month: "short", hour: "2-digit", minute: "2-digit", hour12: false, }) : "—"; /** * The transit-assignee handshake that gates the customs declaration. * * GL Ethiopia cannot file a declaration until Djibouti says who will handle the * shipment on their side, so ET raises the ask here and Djibouti answers with a * name (free text — the officer is not a platform user). Djibouti can send a * different name later; the newest one wins and Ethiopia is notified again. */ export function TransitAssigneePanel({ contractId, transitAssignee, side, readOnly = false, onChanged, }: TransitAssigneePanelProps) { const [note, setNote] = useState(""); const [assignee, setAssignee] = useState(transitAssignee?.name ?? ""); const [changing, setChanging] = useState(false); const request = useMutation({ mutationFn: () => contractsService.requestTransitAssignee(contractId, note.trim()), onSuccess: () => { toast.success("Request sent to GL Djibouti"); setNote(""); onChanged?.(); }, }); const assign = useMutation({ mutationFn: () => contractsService.assignTransitAssignee(contractId, assignee.trim()), onSuccess: () => { toast.success("Transit assignee sent to GL Ethiopia"); setChanging(false); onChanged?.(); }, }); const requested = Boolean(transitAssignee?.requestedAt); const assigned = Boolean(transitAssignee?.name); // Settled state — both desks see the same line; DJ keeps a way to change it. if (assigned && !changing) { return ( } title="Transit assignee confirmed" > {transitAssignee!.name} {" "} will handle this shipment in transit · assigned{" "} {fmt(transitAssignee!.assignedAt)} {side === "DJ" && !readOnly ? ( ) : null} ); } // Djibouti's desk: answer the ask. if (side === "DJ") { if (!requested) { return ( }> GL Ethiopia has not requested a transit assignee for this clearance yet. ); } return ( {changing ? "Change the transit assignee" : "Assign a transit officer"} Requested {fmt(transitAssignee?.requestedAt)} {transitAssignee?.requestNote ? ( GL Ethiopia: {transitAssignee.requestNote} ) : null} setAssignee(e.currentTarget.value)} disabled={readOnly} /> {changing ? ( ) : null} ); } // Ethiopia's desk: raise the ask, then wait. if (requested) { return ( } title="Waiting for GL Djibouti" > Requested {fmt(transitAssignee?.requestedAt)}. The customs declaration opens once Djibouti names the transit officer. {!readOnly ? ( ) : null} ); } return ( Request a transit assignee GL Djibouti must name the officer handling this shipment in transit before the customs declaration can be filed.