train gate pass, Telebirr and Wafi

This commit is contained in:
hagiye
2026-07-01 16:23:52 +03:00
parent 38db9a4177
commit 14a00af341
41 changed files with 2807 additions and 151 deletions

View File

@@ -9,6 +9,8 @@ import {
RingProgress,
Stack,
Text,
Textarea,
TextInput,
ThemeIcon,
Title,
} from "@mantine/core";
@@ -88,6 +90,10 @@ export default function TrainScheduleV2DetailPage() {
const [previewResult, setPreviewResult] = useState<TrainSchedulePreviewResponse | null>(null);
const [containerPlacements, setContainerPlacements] = useState<ContainerPlacement[]>([]);
const [maintenanceOpen, setMaintenanceOpen] = useState(false);
const [gatepassSecuredAt, setGatepassSecuredAt] = useState("");
const [gatepassReference, setGatepassReference] = useState("");
const [gatepassFileUrl, setGatepassFileUrl] = useState("");
const [gatepassNotes, setGatepassNotes] = useState("");
const autoPreviewedRef = useRef(false);
const detailQuery = useQuery(
@@ -98,6 +104,42 @@ export default function TrainScheduleV2DetailPage() {
);
const schedule = detailQuery.data;
const freightType: FreightType | undefined = schedule?.freightType as FreightType | undefined;
const isDjiboutiPort = (value?: string | null) =>
["DJIBOUTI", "DORALEH", "DMP", "DCT", "NAGAD"].some((token) =>
(value ?? "").toUpperCase().includes(token),
);
const gatepassApplies = Boolean(
schedule &&
((schedule.direction === "IMPORT" &&
isDjiboutiPort(`${schedule.originStation?.code ?? ""} ${schedule.originStation?.label ?? ""}`)) ||
(schedule.direction === "EXPORT" &&
isDjiboutiPort(`${schedule.destinationStation?.code ?? ""} ${schedule.destinationStation?.label ?? ""}`))),
);
const gatepassQuery = useQuery({
queryKey: ["train-scheduling", "gatepass", scheduleId],
queryFn: () => trainSchedulingService.getImportDjiboutiOperation(scheduleId as string),
enabled: Boolean(scheduleId && gatepassApplies),
});
const secureGatepass = useMutation({
mutationFn: () =>
trainSchedulingService.grantImportDjiboutiGatepass(scheduleId as string, {
securedAt: gatepassSecuredAt ? new Date(gatepassSecuredAt).toISOString() : undefined,
reference: gatepassReference.trim() || undefined,
fileUrl: gatepassFileUrl.trim() || undefined,
notes: gatepassNotes.trim() || undefined,
}),
onSuccess: () => {
toast({ title: "Gate pass secured" });
void gatepassQuery.refetch();
},
onError: (error) => {
toast({
title: "Gate pass failed",
description: parseError(error, "Could not secure gate pass"),
variant: "destructive",
});
},
});
const eligibleFilters = useMemo(
() =>
@@ -133,6 +175,16 @@ export default function TrainScheduleV2DetailPage() {
: trainSchedulingService.downloadImportDjiboutiLoadListDocument(id),
});
useEffect(() => {
const operation = gatepassQuery.data;
if (!operation) return;
const secured = operation.gatepassSecuredAt ?? operation.gatepassGrantedAt;
setGatepassSecuredAt(secured ? new Date(secured).toISOString().slice(0, 16) : "");
setGatepassReference(operation.documents?.GATE_PASS?.reference ?? "");
setGatepassFileUrl(operation.documents?.GATE_PASS?.fileUrl ?? "");
setGatepassNotes(operation.documents?.GATE_PASS?.notes ?? operation.notes ?? "");
}, [gatepassQuery.data]);
const assignedIds = useMemo(
() => (schedule?.bookings ?? []).map((b) => b.id),
[schedule?.bookings],
@@ -899,6 +951,83 @@ export default function TrainScheduleV2DetailPage() {
]}
/>
{gatepassApplies ? (
<Paper radius="xl" p="lg" withBorder>
<Stack gap="md">
<Group justify="space-between" align="flex-start" wrap="wrap">
<Group gap="md" align="flex-start" wrap="nowrap">
<ThemeIcon
size={44}
radius="md"
variant="light"
color={gatepassQuery.data?.gatepassStatus === "SECURED" ? "edr-green" : "orange"}
>
<FileText size={22} />
</ThemeIcon>
<Stack gap={4}>
<Group gap="sm">
<Title order={4} fw={700}>
Djibouti Port gate pass
</Title>
<Badge
color={gatepassQuery.data?.gatepassStatus === "SECURED" ? "green" : "orange"}
variant="light"
>
{gatepassQuery.data?.gatepassStatus ?? "NOT_SECURED"}
</Badge>
</Group>
<Text size="sm" c="dimmed">
{schedule.direction === "IMPORT"
? "Secure before dispatch from Djibouti."
: "Secure after dispatch before Djibouti Port entry / unloading."}
</Text>
</Stack>
</Group>
{gatepassQuery.isLoading ? <Loader size="sm" /> : null}
</Group>
<Group align="flex-end" grow>
<TextInput
label="Secured date"
type="datetime-local"
value={gatepassSecuredAt}
onChange={(event) => setGatepassSecuredAt(event.currentTarget.value)}
/>
<TextInput
label="Document reference"
placeholder="Optional"
value={gatepassReference}
onChange={(event) => setGatepassReference(event.currentTarget.value)}
/>
<TextInput
label="Document URL"
placeholder="Optional upload/link"
value={gatepassFileUrl}
onChange={(event) => setGatepassFileUrl(event.currentTarget.value)}
/>
</Group>
<Textarea
label="Notes"
placeholder="Optional"
autosize
minRows={2}
value={gatepassNotes}
onChange={(event) => setGatepassNotes(event.currentTarget.value)}
/>
<Group justify="flex-end">
<Button
color="edr-green"
leftSection={<CheckCircle2 size={16} />}
loading={secureGatepass.isPending}
onClick={() => secureGatepass.mutate()}
>
Save as Secured
</Button>
</Group>
</Stack>
</Paper>
) : null}
<Paper radius="xl" p="lg">
<Stack gap="lg">
{/* Workflow header with ring progress */}