mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 21:08:12 +00:00
Merge pull request #458 from Tria-plc/freight_feature/usermanagement
refactor: remove gate pass granting logic from clearance services and UI
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
||||
Text,
|
||||
Textarea,
|
||||
} from "@mantine/core";
|
||||
import { DateInput, DateTimePicker } from "@mantine/dates";
|
||||
import { DateInput } from "@mantine/dates";
|
||||
import {
|
||||
AlertTriangle,
|
||||
CheckCircle2,
|
||||
@@ -397,15 +397,10 @@ export function ExportClearanceStepper({
|
||||
|
||||
<Stepper.Step
|
||||
label="Gate pass"
|
||||
description="GL Djibouti grants after arrival"
|
||||
description="Secured on the train schedule after arrival"
|
||||
icon={clearance.gatepassGranted ? <CheckCircle2 size={14} /> : <Truck size={14} />}
|
||||
>
|
||||
<GatepassStep
|
||||
bookingId={actionBookingId}
|
||||
clearance={clearance}
|
||||
canAct={showDj && canDj}
|
||||
onChanged={onChanged}
|
||||
/>
|
||||
<GatepassStep clearance={clearance} />
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step
|
||||
@@ -491,27 +486,19 @@ function ConfirmExportReleaseFallback({
|
||||
);
|
||||
}
|
||||
|
||||
function GatepassStep({
|
||||
bookingId,
|
||||
clearance,
|
||||
canAct,
|
||||
onChanged,
|
||||
}: {
|
||||
bookingId: string | null;
|
||||
clearance: ClearanceViewLike;
|
||||
canAct: boolean;
|
||||
onChanged?: () => void;
|
||||
}) {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [at, setAt] = useState<Date | null>(new Date());
|
||||
const [loading, setLoading] = useState(false);
|
||||
/**
|
||||
* Gate pass status, read-only. Secured on the train schedule's "Save as
|
||||
* Secured" action (train-scheduling-v2) — clearance no longer grants it directly.
|
||||
*/
|
||||
function GatepassStep({ clearance }: { clearance: ClearanceViewLike }) {
|
||||
const scheduleId = clearance.train?.scheduleId ?? null;
|
||||
|
||||
if (clearance.gatepassGranted) {
|
||||
return (
|
||||
<StepStatus
|
||||
done
|
||||
pendingLabel=""
|
||||
doneLabel={`Gate pass granted${
|
||||
doneLabel={`Gate pass secured${
|
||||
clearance.gatepassAt ? ` · ${new Date(clearance.gatepassAt).toLocaleString()}` : ""
|
||||
}`}
|
||||
/>
|
||||
@@ -526,68 +513,21 @@ function GatepassStep({
|
||||
done={false}
|
||||
pendingLabel={
|
||||
arrived
|
||||
? "Train arrived — GL Djibouti can grant the gate pass."
|
||||
? "Train arrived — secure the gate pass on the train schedule."
|
||||
: "Available once the train arrives at Djibouti."
|
||||
}
|
||||
doneLabel=""
|
||||
/>
|
||||
{canAct && bookingId ? (
|
||||
<>
|
||||
<Button
|
||||
color="edr-green"
|
||||
leftSection={<Truck size={16} />}
|
||||
disabled={!arrived}
|
||||
onClick={() => {
|
||||
setAt(new Date());
|
||||
setOpened(true);
|
||||
}}
|
||||
>
|
||||
Grant gate pass
|
||||
</Button>
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
title={<Text fw={700}>Grant gate pass</Text>}
|
||||
radius="md"
|
||||
size="sm"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<DateTimePicker
|
||||
label="Gate pass time"
|
||||
value={at}
|
||||
onChange={(v) => setAt(v ? new Date(v) : null)}
|
||||
required
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={() => setOpened(false)} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="edr-green"
|
||||
loading={loading}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await contractsService.grantGatepass(
|
||||
bookingId,
|
||||
(at ?? new Date()).toISOString(),
|
||||
);
|
||||
toast.success("Gate pass granted");
|
||||
setOpened(false);
|
||||
onChanged?.();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Grant
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</>
|
||||
{scheduleId ? (
|
||||
<Button
|
||||
component="a"
|
||||
href={`/dashboard/operations/train-scheduling-v2/${scheduleId}`}
|
||||
variant="light"
|
||||
color="edr-green"
|
||||
leftSection={<Truck size={16} />}
|
||||
>
|
||||
Secure gate pass on train schedule
|
||||
</Button>
|
||||
) : null}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
Badge,
|
||||
Button,
|
||||
Group,
|
||||
Modal,
|
||||
NumberInput,
|
||||
Paper,
|
||||
SegmentedControl,
|
||||
@@ -15,7 +14,6 @@ import {
|
||||
Text,
|
||||
TextInput,
|
||||
} from "@mantine/core";
|
||||
import { DateTimePicker } from "@mantine/dates";
|
||||
import { PhasedFileDropzone, PhasedMultiFileDropzone } from "@/components/contracts/PhasedFileDropzone";
|
||||
import {
|
||||
TransitPermitMultiUpload,
|
||||
@@ -536,17 +534,12 @@ export function PhasedClearanceActionPanel({
|
||||
|
||||
<Stepper.Step
|
||||
label="Gate pass"
|
||||
description="GL Djibouti grants after wagon allocation"
|
||||
description="Secured on the train schedule after wagon allocation"
|
||||
icon={
|
||||
clearance.gatepassGranted ? <CheckCircle2 size={14} /> : <Truck size={14} />
|
||||
}
|
||||
>
|
||||
<ImportGatepassStep
|
||||
bookingId={actionBookingId}
|
||||
clearance={clearance}
|
||||
canAct={showDj && canDj}
|
||||
onChanged={onChanged}
|
||||
/>
|
||||
<ImportGatepassStep clearance={clearance} />
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step
|
||||
@@ -816,28 +809,19 @@ function ImportT1Section({
|
||||
);
|
||||
}
|
||||
|
||||
/** GL DJ grants the import gate pass once wagons are allocated (captures time). */
|
||||
function ImportGatepassStep({
|
||||
bookingId,
|
||||
clearance,
|
||||
canAct,
|
||||
onChanged,
|
||||
}: {
|
||||
bookingId: string | null;
|
||||
clearance: ClearanceViewLike;
|
||||
canAct: boolean;
|
||||
onChanged?: () => void;
|
||||
}) {
|
||||
const [opened, setOpened] = useState(false);
|
||||
const [at, setAt] = useState<Date | null>(new Date());
|
||||
const [loading, setLoading] = useState(false);
|
||||
/**
|
||||
* Gate pass status, read-only. Secured on the train schedule's "Save as
|
||||
* Secured" action (train-scheduling-v2) — clearance no longer grants it directly.
|
||||
*/
|
||||
function ImportGatepassStep({ clearance }: { clearance: ClearanceViewLike }) {
|
||||
const scheduleId = clearance.train?.scheduleId ?? null;
|
||||
|
||||
if (clearance.gatepassGranted) {
|
||||
return (
|
||||
<StepStatus
|
||||
done
|
||||
pendingLabel=""
|
||||
doneLabel={`Gate pass granted${
|
||||
doneLabel={`Gate pass secured${
|
||||
clearance.gatepassAt ? ` · ${new Date(clearance.gatepassAt).toLocaleString()}` : ""
|
||||
}`}
|
||||
/>
|
||||
@@ -852,68 +836,21 @@ function ImportGatepassStep({
|
||||
done={false}
|
||||
pendingLabel={
|
||||
wagonAllocated
|
||||
? "Wagons allocated — GL Djibouti can grant the gate pass."
|
||||
? "Wagons allocated — secure the gate pass on the train schedule."
|
||||
: "Available once wagons are allocated."
|
||||
}
|
||||
doneLabel=""
|
||||
/>
|
||||
{canAct && bookingId ? (
|
||||
<>
|
||||
<Button
|
||||
color="edr-green"
|
||||
leftSection={<Truck size={16} />}
|
||||
disabled={!wagonAllocated}
|
||||
onClick={() => {
|
||||
setAt(new Date());
|
||||
setOpened(true);
|
||||
}}
|
||||
>
|
||||
Grant gate pass
|
||||
</Button>
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={() => setOpened(false)}
|
||||
title={<Text fw={700}>Grant gate pass</Text>}
|
||||
radius="md"
|
||||
size="sm"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<DateTimePicker
|
||||
label="Gate pass time"
|
||||
value={at}
|
||||
onChange={(v) => setAt(v ? new Date(v) : null)}
|
||||
required
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<Button variant="default" onClick={() => setOpened(false)} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="edr-green"
|
||||
loading={loading}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await contractsService.grantGatepass(
|
||||
bookingId,
|
||||
(at ?? new Date()).toISOString(),
|
||||
);
|
||||
toast.success("Gate pass granted");
|
||||
setOpened(false);
|
||||
onChanged?.();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Grant
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</>
|
||||
{scheduleId ? (
|
||||
<Button
|
||||
component="a"
|
||||
href={`/dashboard/operations/train-scheduling-v2/${scheduleId}`}
|
||||
variant="light"
|
||||
color="edr-green"
|
||||
leftSection={<Truck size={16} />}
|
||||
>
|
||||
Secure gate pass on train schedule
|
||||
</Button>
|
||||
) : null}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -70,7 +70,6 @@ export const QUERY_KEYS = {
|
||||
["contracts", "clearance-queue", region ?? "ET"] as const,
|
||||
clearanceHistory: (region?: string) =>
|
||||
["contracts", "clearance-history", region ?? "ET"] as const,
|
||||
djSchedules: ["contracts", "clearance-dj-schedules"] as const,
|
||||
milestones: (id: string) => ["contracts", "milestones", id] as const,
|
||||
capacity: (id: string) => ["contracts", "capacity", id] as const,
|
||||
bookingMilestones: (bookingId: string) =>
|
||||
|
||||
@@ -232,11 +232,6 @@ export const URL_CONSTANTS = {
|
||||
`/contracts/bookings/${bookingId}/t1-documents`,
|
||||
BOOKING_T1_CLOSE: (bookingId: string) =>
|
||||
`/contracts/bookings/${bookingId}/t1-close`,
|
||||
CLEARANCE_DJ_SCHEDULES: "/contracts/clearance/dj-schedules",
|
||||
CLEARANCE_SCHEDULE_GATEPASS: (scheduleId: string) =>
|
||||
`/contracts/clearance/schedules/${scheduleId}/gatepass`,
|
||||
BOOKING_GATEPASS: (bookingId: string) =>
|
||||
`/contracts/bookings/${bookingId}/gatepass`,
|
||||
BOOKING_FINAL_INVOICE: (bookingId: string) =>
|
||||
`/contracts/bookings/${bookingId}/final-invoice`,
|
||||
BOOKING_FINAL_INVOICE_CONFIRM: (bookingId: string) =>
|
||||
|
||||
@@ -68,15 +68,6 @@ export function useDjClearanceQueue(enabled = true) {
|
||||
});
|
||||
}
|
||||
|
||||
/** Train schedules carrying customs bookings — GL DJ gate-pass table. */
|
||||
export function useDjClearanceSchedules(enabled = true) {
|
||||
return useQuery({
|
||||
queryKey: QUERY_KEYS.CONTRACTS.djSchedules,
|
||||
queryFn: () => contractsService.getDjClearanceSchedules(),
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
/** Path A self-clearance queue (Operations reviews non-customs contracts). */
|
||||
export function useOpsClearanceQueue(enabled = true) {
|
||||
return useQuery({
|
||||
|
||||
@@ -1,326 +1,65 @@
|
||||
import { useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Card,
|
||||
Group,
|
||||
Loader,
|
||||
Modal,
|
||||
Stack,
|
||||
Tabs,
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import { DateTimePicker } from "@mantine/dates";
|
||||
import { ChevronRight, Ship, Train, Truck } from "lucide-react";
|
||||
import { DataTable, type ColumnDef } from "@edr/ui-common";
|
||||
import type { Freight } from "@edr/types";
|
||||
import toast from "react-hot-toast";
|
||||
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,
|
||||
useDjClearanceSchedules,
|
||||
} from "@/hooks/contracts/useContracts";
|
||||
import { contractsService } from "@/services/contracts.service";
|
||||
import { useDjClearanceQueue } from "@/hooks/contracts/useContracts";
|
||||
|
||||
export default function GlDjiboutiClearanceListPage() {
|
||||
const navigate = useNavigate();
|
||||
const { data: contractQueue, isLoading: contractsLoading } = useDjClearanceQueue();
|
||||
const schedulesQuery = useDjClearanceSchedules();
|
||||
|
||||
const contractItems = contractQueue?.items ?? [];
|
||||
const scheduleItems = schedulesQuery.data ?? [];
|
||||
|
||||
const [gatepassTarget, setGatepassTarget] =
|
||||
useState<Freight.DjClearanceSchedule | null>(null);
|
||||
const [gatepassAt, setGatepassAt] = useState<Date | null>(new Date());
|
||||
const [granting, setGranting] = useState(false);
|
||||
|
||||
const columns = useMemo<ColumnDef<Freight.DjClearanceSchedule>[]>(
|
||||
() => [
|
||||
{
|
||||
header: "Train",
|
||||
accessorKey: "trainNumber",
|
||||
cell: ({ row }) => (
|
||||
<Text size="sm" fw={700}>
|
||||
{row.original.trainNumber ?? "—"}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Route",
|
||||
id: "route",
|
||||
cell: ({ row }) => (
|
||||
<Text size="sm">
|
||||
{row.original.origin ?? "—"} → {row.original.destination ?? "—"}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Scheduled departure",
|
||||
id: "scheduled",
|
||||
cell: ({ row }) => (
|
||||
<Text size="sm">
|
||||
{row.original.scheduledDepartureDate
|
||||
? new Date(row.original.scheduledDepartureDate).toLocaleDateString()
|
||||
: "—"}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Departed",
|
||||
id: "departed",
|
||||
cell: ({ row }) => (
|
||||
<Text size="sm">
|
||||
{row.original.actualDepartureAt
|
||||
? new Date(row.original.actualDepartureAt).toLocaleString()
|
||||
: "—"}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Arrived",
|
||||
id: "arrived",
|
||||
cell: ({ row }) => (
|
||||
<Text size="sm">
|
||||
{row.original.actualArrivalAt
|
||||
? new Date(row.original.actualArrivalAt).toLocaleString()
|
||||
: "—"}
|
||||
</Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Status",
|
||||
accessorKey: "status",
|
||||
cell: ({ row }) => (
|
||||
<Badge variant="light" color={statusColor(row.original.status)} radius="sm">
|
||||
{row.original.status}
|
||||
</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
header: "Customs bookings",
|
||||
id: "customs",
|
||||
cell: ({ row }) => {
|
||||
const bookings = row.original.customsBookings;
|
||||
const directions = [...new Set(bookings.map((b) => b.tradeDirection))];
|
||||
return (
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<Badge variant="light" color="edr-green" radius="sm">
|
||||
{bookings.length}
|
||||
</Badge>
|
||||
{directions.map((d) => (
|
||||
<Badge key={d} variant="outline" color={d === "IMPORT" ? "edr-green" : "blue"} radius="sm">
|
||||
{d}
|
||||
</Badge>
|
||||
))}
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
header: "Gate pass",
|
||||
id: "gatepass",
|
||||
cell: ({ row }) => {
|
||||
const bookings = row.original.customsBookings;
|
||||
const allGranted =
|
||||
bookings.length > 0 && bookings.every((b) => b.gatepassGranted);
|
||||
const grantedAt = bookings.find((b) => b.gatepassAt)?.gatepassAt ?? null;
|
||||
if (allGranted) {
|
||||
return (
|
||||
<Badge variant="light" color="edr-green" radius="sm">
|
||||
Granted{grantedAt ? ` · ${new Date(grantedAt).toLocaleString()}` : ""}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
size="xs"
|
||||
color="edr-green"
|
||||
leftSection={<Truck size={14} />}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setGatepassAt(new Date());
|
||||
setGatepassTarget(row.original);
|
||||
}}
|
||||
>
|
||||
Gate pass
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
},
|
||||
],
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<PageHeader
|
||||
title="GL Djibouti — Clearance"
|
||||
subtitle="Customs contracts handed off to Djibouti GL, plus train schedules for gate-pass control."
|
||||
subtitle="Customs contracts handed off to Djibouti GL."
|
||||
/>
|
||||
<Tabs defaultValue="contracts" keepMounted={false}>
|
||||
<Tabs.List mb="md">
|
||||
<Tabs.Tab value="contracts">Contracts ({contractItems.length})</Tabs.Tab>
|
||||
<Tabs.Tab value="schedules" leftSection={<Train size={14} />}>
|
||||
Schedules ({scheduleItems.length})
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<Tabs.Panel value="contracts">
|
||||
{contractsLoading ? (
|
||||
<Group justify="center" py={60}>
|
||||
<Loader color="edr-green" />
|
||||
</Group>
|
||||
) : (
|
||||
<Stack gap="sm">
|
||||
{contractItems.length === 0 ? (
|
||||
<Text c="dimmed" ta="center" py="xl">
|
||||
No Djibouti customs contracts yet.
|
||||
</Text>
|
||||
) : (
|
||||
contractItems.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="edr-green">
|
||||
Contract
|
||||
</Badge>
|
||||
<ChevronRight size={18} className="text-muted-foreground" />
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
)}
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="schedules">
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={scheduleItems}
|
||||
status={
|
||||
schedulesQuery.isLoading
|
||||
? "loading"
|
||||
: schedulesQuery.isError
|
||||
? "error"
|
||||
: "success"
|
||||
}
|
||||
error={
|
||||
schedulesQuery.isError
|
||||
? {
|
||||
message: "Failed to load train schedules.",
|
||||
onRetry: () => void schedulesQuery.refetch(),
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
emptyMessage="No train schedules carry customs bookings yet."
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
|
||||
<Modal
|
||||
opened={gatepassTarget != null}
|
||||
onClose={() => setGatepassTarget(null)}
|
||||
title={
|
||||
<Group gap={8}>
|
||||
<Truck size={18} />
|
||||
<Text fw={700}>
|
||||
Gate pass — train {gatepassTarget?.trainNumber ?? ""}
|
||||
{contractsLoading ? (
|
||||
<Group justify="center" py={60}>
|
||||
<Loader color="edr-green" />
|
||||
</Group>
|
||||
) : (
|
||||
<Stack gap="sm">
|
||||
{contractItems.length === 0 ? (
|
||||
<Text c="dimmed" ta="center" py="xl">
|
||||
No Djibouti customs contracts yet.
|
||||
</Text>
|
||||
</Group>
|
||||
}
|
||||
radius="md"
|
||||
size="sm"
|
||||
>
|
||||
<Stack gap="md">
|
||||
<Text size="sm" c="dimmed">
|
||||
Grants the gate pass for all{" "}
|
||||
{gatepassTarget?.customsBookings.length ?? 0} customs booking
|
||||
{(gatepassTarget?.customsBookings.length ?? 0) === 1 ? "" : "s"} on this
|
||||
train.
|
||||
</Text>
|
||||
<DateTimePicker
|
||||
label="Gate pass time"
|
||||
value={gatepassAt}
|
||||
onChange={(v) => setGatepassAt(v ? new Date(v) : null)}
|
||||
required
|
||||
/>
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => setGatepassTarget(null)}
|
||||
disabled={granting}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
color="edr-green"
|
||||
loading={granting}
|
||||
leftSection={<Truck size={16} />}
|
||||
onClick={async () => {
|
||||
if (!gatepassTarget) return;
|
||||
setGranting(true);
|
||||
try {
|
||||
const result = await contractsService.grantScheduleGatepass(
|
||||
gatepassTarget.id,
|
||||
(gatepassAt ?? new Date()).toISOString(),
|
||||
);
|
||||
if (result.skipped.length > 0) {
|
||||
toast.error(
|
||||
`${result.granted} granted, ${result.skipped.length} skipped: ${result.skipped[0]?.error ?? ""}`,
|
||||
);
|
||||
} else {
|
||||
toast.success(
|
||||
`Gate pass granted for ${result.granted} booking${result.granted === 1 ? "" : "s"}`,
|
||||
);
|
||||
}
|
||||
setGatepassTarget(null);
|
||||
void schedulesQuery.refetch();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed");
|
||||
} finally {
|
||||
setGranting(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Grant gate pass
|
||||
</Button>
|
||||
</Group>
|
||||
) : (
|
||||
contractItems.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="edr-green">
|
||||
Contract
|
||||
</Badge>
|
||||
<ChevronRight size={18} className="text-muted-foreground" />
|
||||
</Group>
|
||||
</Group>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</Stack>
|
||||
</Modal>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
function statusColor(status: string): string {
|
||||
switch (status) {
|
||||
case "SCHEDULED":
|
||||
return "blue";
|
||||
case "DISPATCHED":
|
||||
return "yellow";
|
||||
case "ARRIVED":
|
||||
return "edr-green";
|
||||
default:
|
||||
return "gray";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,35 +391,6 @@ export const contractsService = {
|
||||
return unwrap(response.data) as Freight.ClearanceT1State;
|
||||
},
|
||||
|
||||
/** Train schedules carrying customs bookings — GL DJ gate-pass table. */
|
||||
getDjClearanceSchedules: async (): Promise<Freight.DjClearanceSchedule[]> => {
|
||||
const response = await client.get(C.CLEARANCE_DJ_SCHEDULES);
|
||||
return unwrap(response.data) as Freight.DjClearanceSchedule[];
|
||||
},
|
||||
|
||||
/** Gate pass for every customs booking on a train schedule (captures time). */
|
||||
grantScheduleGatepass: async (
|
||||
scheduleId: string,
|
||||
gatepassAt?: string,
|
||||
): Promise<{ granted: number; skipped: Array<{ bookingId: string; error: string }> }> => {
|
||||
const response = await client.post(C.CLEARANCE_SCHEDULE_GATEPASS(scheduleId), {
|
||||
gatepassAt,
|
||||
});
|
||||
return unwrap(response.data) as {
|
||||
granted: number;
|
||||
skipped: Array<{ bookingId: string; error: string }>;
|
||||
};
|
||||
},
|
||||
|
||||
/** Gate pass for a single customs booking (captures time). */
|
||||
grantGatepass: async (
|
||||
bookingId: string,
|
||||
gatepassAt?: string,
|
||||
): Promise<{ bookingId: string; gatepassAt: string }> => {
|
||||
const response = await client.post(C.BOOKING_GATEPASS(bookingId), { gatepassAt });
|
||||
return unwrap(response.data) as { bookingId: string; gatepassAt: string };
|
||||
},
|
||||
|
||||
/** GL DJ raises the post-offload final invoice (amount + invoice document). */
|
||||
sendFinalInvoice: async (
|
||||
bookingId: string,
|
||||
|
||||
Reference in New Issue
Block a user