mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 21:15:41 +00:00
Merge pull request #1387 from Tria-plc/freight_feature/usermanagement
fix issue
This commit is contained in:
@@ -55,9 +55,31 @@ interface WagonCancellation {
|
||||
reason?: string | null;
|
||||
rebookedAt?: string | null;
|
||||
createdAt: string;
|
||||
booking?: { id: string; reference: string; company?: { name: string } };
|
||||
booking?: {
|
||||
id: string;
|
||||
reference: string;
|
||||
customsClearingEnabled?: boolean;
|
||||
company?: { name: string };
|
||||
};
|
||||
rebookedBooking?: { id: string; reference: string };
|
||||
feeInvoice?: { invoiceNumber: string; status: string };
|
||||
cancelledQuantities?: {
|
||||
bySize?: Record<string, number>;
|
||||
units?: Array<{
|
||||
containerSize: string;
|
||||
containerNumber: string;
|
||||
sealNumber?: string | null;
|
||||
vgmTons: number;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
/** Editable rebook unit — prefilled from the cancelled snapshot. */
|
||||
interface RebookUnitDraft {
|
||||
containerSize: string;
|
||||
containerNumber: string;
|
||||
sealNumber: string;
|
||||
vgmTons: number | "";
|
||||
}
|
||||
|
||||
interface WagonCancellationListResponse {
|
||||
@@ -116,6 +138,49 @@ export default function WagonCancellationsPage() {
|
||||
const [from, setFrom] = useState<Date | null>(null);
|
||||
const [to, setTo] = useState<Date | null>(null);
|
||||
const [voiding, setVoiding] = useState<WagonCancellation | null>(null);
|
||||
// GL rebook of a customs (Path B) credit: pick the day; container number /
|
||||
// seal / VGM may be corrected. Non-customs credits are rebooked by the
|
||||
// customer from the portal.
|
||||
const canRebook = hasPermission(
|
||||
user,
|
||||
FREIGHT_PERMS.bookings.wagonCancellationRebook,
|
||||
);
|
||||
const [rebooking, setRebooking] = useState<WagonCancellation | null>(null);
|
||||
const [rebookDate, setRebookDate] = useState<Date | null>(null);
|
||||
const [rebookDrafts, setRebookDrafts] = useState<RebookUnitDraft[]>([]);
|
||||
const openRebook = (r: WagonCancellation) => {
|
||||
setRebooking(r);
|
||||
setRebookDate(null);
|
||||
setRebookDrafts(
|
||||
(r.cancelledQuantities?.units ?? []).map((u) => ({
|
||||
containerSize: u.containerSize,
|
||||
containerNumber: u.containerNumber,
|
||||
sealNumber: u.sealNumber ?? "",
|
||||
vgmTons: Number(u.vgmTons) || "",
|
||||
})),
|
||||
);
|
||||
};
|
||||
const rebookContainersPayload = () => {
|
||||
const bySize = new Map<string, RebookUnitDraft[]>();
|
||||
for (const d of rebookDrafts) {
|
||||
bySize.set(d.containerSize, [...(bySize.get(d.containerSize) ?? []), d]);
|
||||
}
|
||||
return [...bySize.entries()].map(([containerSize, units]) => ({
|
||||
containerSize,
|
||||
units: units.map((u) => ({
|
||||
containerNumber: u.containerNumber.trim(),
|
||||
...(u.sealNumber.trim() ? { sealNumber: u.sealNumber.trim() } : {}),
|
||||
...(u.vgmTons !== "" ? { vgmTons: Number(u.vgmTons) } : {}),
|
||||
})),
|
||||
}));
|
||||
};
|
||||
const rebook = useMutation({
|
||||
mutationFn: () =>
|
||||
api.post(`/bookings/wagon-cancellations/${rebooking!.id}/rebook`, {
|
||||
scheduledDate: toDayString(rebookDate!),
|
||||
...(rebookDrafts.length ? { containers: rebookContainersPayload() } : {}),
|
||||
}),
|
||||
});
|
||||
|
||||
const resetPage = () =>
|
||||
setPagination({ pageIndex: 0, pageSize: pagination.pageSize });
|
||||
@@ -234,18 +299,39 @@ export default function WagonCancellationsPage() {
|
||||
header: () => <span />,
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
if (r.status !== "FEE_PENDING" || !canVoid) return null;
|
||||
const showVoid = r.status === "FEE_PENDING" && canVoid;
|
||||
// Customs credits are GL's to rebook; non-customs ones the customer
|
||||
// rebooks from the portal.
|
||||
const showRebook =
|
||||
r.status === "CREDIT_AVAILABLE" &&
|
||||
canRebook &&
|
||||
Boolean(r.booking?.customsClearingEnabled) &&
|
||||
Number(r.creditAmount) > 0;
|
||||
if (!showVoid && !showRebook) return null;
|
||||
return (
|
||||
<Group justify="flex-end" wrap="nowrap">
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="subtle"
|
||||
color="red"
|
||||
onClick={() => setVoiding(r)}
|
||||
>
|
||||
Void
|
||||
</Button>
|
||||
{showRebook && (
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="green"
|
||||
onClick={() => openRebook(r)}
|
||||
>
|
||||
Rebook
|
||||
</Button>
|
||||
)}
|
||||
{showVoid && (
|
||||
<Button
|
||||
size="xs"
|
||||
radius="md"
|
||||
variant="subtle"
|
||||
color="red"
|
||||
onClick={() => setVoiding(r)}
|
||||
>
|
||||
Void
|
||||
</Button>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
},
|
||||
@@ -391,6 +477,117 @@ export default function WagonCancellationsPage() {
|
||||
</Stack>
|
||||
)}
|
||||
</Modal>
|
||||
<Modal
|
||||
opened={!!rebooking}
|
||||
onClose={() => setRebooking(null)}
|
||||
title="Rebook cancelled wagons"
|
||||
centered
|
||||
radius="md"
|
||||
>
|
||||
{rebooking && (
|
||||
<Stack gap="sm">
|
||||
<Text size="sm">
|
||||
{rebooking.booking?.reference ?? rebooking.bookingId} ·{" "}
|
||||
{rebooking.wagonsCancelled} wagon(s) · credit{" "}
|
||||
{formatMoney(rebooking.creditAmount, rebooking.feeCurrency, 2)}
|
||||
</Text>
|
||||
<DatePickerInput
|
||||
label="Shipment day"
|
||||
placeholder="Pick the day"
|
||||
value={rebookDate}
|
||||
onChange={(v) => setRebookDate(v ? new Date(v) : null)}
|
||||
radius="md"
|
||||
/>
|
||||
{rebookDrafts.length > 0 && (
|
||||
<Stack gap={6}>
|
||||
<Text size="xs" c="dimmed">
|
||||
Correct the container details if they changed — sizes and
|
||||
quantities stay as cancelled.
|
||||
</Text>
|
||||
{rebookDrafts.map((d, i) => (
|
||||
<Group key={i} gap={8} wrap="nowrap" align="flex-end">
|
||||
<TextInput
|
||||
label={`${d.containerSize} container`}
|
||||
value={d.containerNumber}
|
||||
onChange={(e) => {
|
||||
const v = e.currentTarget.value;
|
||||
setRebookDrafts((prev) =>
|
||||
prev.map((x, idx) =>
|
||||
idx === i ? { ...x, containerNumber: v } : x,
|
||||
),
|
||||
);
|
||||
}}
|
||||
size="xs"
|
||||
radius="md"
|
||||
style={{ flex: 1.4 }}
|
||||
/>
|
||||
<TextInput
|
||||
label="Seal no."
|
||||
value={d.sealNumber}
|
||||
onChange={(e) => {
|
||||
const v = e.currentTarget.value;
|
||||
setRebookDrafts((prev) =>
|
||||
prev.map((x, idx) =>
|
||||
idx === i ? { ...x, sealNumber: v } : x,
|
||||
),
|
||||
);
|
||||
}}
|
||||
size="xs"
|
||||
radius="md"
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<TextInput
|
||||
label="VGM (t)"
|
||||
type="number"
|
||||
value={d.vgmTons === "" ? "" : String(d.vgmTons)}
|
||||
onChange={(e) => {
|
||||
const raw = e.currentTarget.value;
|
||||
setRebookDrafts((prev) =>
|
||||
prev.map((x, idx) =>
|
||||
idx === i
|
||||
? { ...x, vgmTons: raw === "" ? "" : Number(raw) }
|
||||
: x,
|
||||
),
|
||||
);
|
||||
}}
|
||||
size="xs"
|
||||
radius="md"
|
||||
style={{ width: 90 }}
|
||||
/>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
<Group justify="flex-end" gap="sm">
|
||||
<Button
|
||||
variant="default"
|
||||
radius="md"
|
||||
onClick={() => setRebooking(null)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
<Button
|
||||
color="green"
|
||||
radius="md"
|
||||
disabled={!rebookDate}
|
||||
loading={rebook.isPending}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await rebook.mutateAsync();
|
||||
toast.success("Credit rebooked as a new paid booking");
|
||||
setRebooking(null);
|
||||
void refetch();
|
||||
} catch {
|
||||
// interceptor surfaces the reason
|
||||
}
|
||||
}}
|
||||
>
|
||||
Rebook
|
||||
</Button>
|
||||
</Group>
|
||||
</Stack>
|
||||
)}
|
||||
</Modal>
|
||||
</PageContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -95,12 +95,17 @@ function PayWindowCell({ deadline }: { deadline: string | null }) {
|
||||
);
|
||||
}
|
||||
|
||||
/** `invoices.type` of a wagon-cancellation fee — mirrors the API constant. */
|
||||
const WAGON_CANCEL_FEE_INVOICE_TYPE = "WAGON_CANCEL_FEE";
|
||||
|
||||
/**
|
||||
* "Confirm paid" for one row. Booking invoices are only confirmable while the
|
||||
* booking's pay window is open (the API refuses otherwise): no window yet →
|
||||
* no button; window closed → button disabled with the reason, and it flips
|
||||
* live the second the countdown hits zero. Non-booking invoices (warehouse,
|
||||
* clearance…) have no window and stay confirmable.
|
||||
* clearance…) have no window and stay confirmable — and so do
|
||||
* wagon-cancellation fees, which ride source=booking but are raised on an
|
||||
* already-paid booking whose window has closed.
|
||||
*/
|
||||
function ConfirmCell({
|
||||
row,
|
||||
@@ -109,10 +114,11 @@ function ConfirmCell({
|
||||
row: OfflineUsdInvoice;
|
||||
onConfirm: (row: OfflineUsdInvoice) => void;
|
||||
}) {
|
||||
const deadline = row.booking?.paymentDeadline ?? null;
|
||||
const feeInvoice = row.type === WAGON_CANCEL_FEE_INVOICE_TYPE;
|
||||
const deadline = feeInvoice ? null : (row.booking?.paymentDeadline ?? null);
|
||||
const now = useNow(deadline);
|
||||
|
||||
if (row.booking && !deadline) return null;
|
||||
if (row.booking && !deadline && !feeInvoice) return null;
|
||||
const closed = Boolean(deadline && new Date(deadline).getTime() <= now);
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user