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}
}
loading={assign.isPending}
disabled={readOnly || !assignee.trim()}
onClick={() => assign.mutate()}
>
Send assignment
);
}
// 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.
);
}
export default TransitAssigneePanel;