Merge branch 'dev' into freight/nati-2

This commit is contained in:
Nathnael
2026-08-07 08:09:34 +00:00
79 changed files with 7273 additions and 802 deletions

View File

@@ -1,6 +1,6 @@
import type { LucideIcon } from "lucide-react";
import type { ReactNode } from "react";
import { Hash, Package, Ship, Weight, Clock } from "lucide-react";
import { Hash, Package, Ship, Weight, Clock, TrainFront } from "lucide-react";
import { Group, Stack, Text, Divider } from "@mantine/core";
import { cargoTonsAndItems } from "@/utils/cargoWeight";
@@ -50,6 +50,21 @@ export function BookingFactsCard({ booking }: BookingFactsCardProps) {
},
{ icon: Clock, label: "Last Updated", value: formatDate(booking.updatedAt) },
];
// Allocated train facts — only once the booking rides a schedule.
if (booking.trainSchedule?.trainNumber || booking.trainSchedule?.reference) {
facts.splice(1, 0, {
icon: TrainFront,
label: "Allocated Train",
value: [
booking.trainSchedule.trainNumber
? `Train ${booking.trainSchedule.trainNumber}`
: null,
booking.trainSchedule.reference ?? null,
]
.filter(Boolean)
.join(" · "),
});
}
return (
<SectionCard icon={Hash} title="Booking Details" accent="cyan">

View File

@@ -144,4 +144,10 @@ export interface BookingDetailView {
bookingContainers?: BookingContainerView[];
reviewNotes?: BookingReviewNoteView[];
files?: BookingFileView[];
/** The allocated train, present once the booking is placed on a schedule. */
trainSchedule?: {
trainNumber: string | null;
reference: string | null;
scheduledDepartureDate: string | null;
} | null;
}

View File

@@ -1,6 +1,6 @@
import { Alert, Button, Group, Paper, Stack, Text } from "@mantine/core";
import { DateInput } from "@mantine/dates";
import { AlertTriangle, Send } from "lucide-react";
import { AlertTriangle, Pencil, Send } from "lucide-react";
import { useState } from "react";
import { Link } from "react-router-dom";
import toast from "react-hot-toast";
@@ -16,6 +16,9 @@ export interface BookingChangesRequestedAlertProps {
scheduledDate?: string | null;
/** GL Ethiopia owns customs bookings, so only they get the resubmit control. */
canResubmit: boolean;
/** Completion-form route for editing the cargo before resubmitting —
* rendered only for resubmit-capable users when provided. */
editHref?: string;
onResubmitted?: () => void;
}
@@ -33,6 +36,7 @@ export function BookingChangesRequestedAlert({
note,
scheduledDate,
canResubmit,
editHref,
onResubmitted,
}: BookingChangesRequestedAlertProps) {
const [day, setDay] = useState<Date | null>(
@@ -122,6 +126,18 @@ export function BookingChangesRequestedAlert({
>
Resubmit to Operations
</Button>
{editHref ? (
<Button
component={Link}
to={editHref}
variant="default"
radius="md"
size="sm"
leftSection={<Pencil size={15} />}
>
Edit cargo & resubmit
</Button>
) : null}
</Group>
) : null}
</Stack>

View File

@@ -225,7 +225,13 @@ export function ExportClearanceStepper({
<Stepper.Step
label="Request transit assignee"
description="Ask GL Djibouti to name the officer handling this shipment"
// Description is the only part of a passed step that stays visible,
// so it carries the assigned officer's name for GL Ethiopia.
description={
clearance.transitAssignee?.name
? `Transit assignee: ${clearance.transitAssignee.name}`
: "Ask GL Djibouti to name the officer handling this shipment"
}
icon={
clearance.transitAssignee?.name ? (
<CheckCircle2 size={14} />

View File

@@ -961,7 +961,7 @@ export default function GlCreateBookingForm() {
// modal falls back to the contract unit-rate estimate while it loads.
const validateShipmentMutation = useMutation({
mutationFn: (dto: Freight.CreateBookingUnderContractDto) =>
contractsService.validateShipment(id ?? "", dto),
contractsService.validateShipment(id ?? "", dto, completeBookingId),
});
const validation = validateShipmentMutation.data ?? null;

View File

@@ -39,6 +39,8 @@ interface WindowRow {
windowClosesAt: string | null;
docReviewEndsAt: string | null;
paymentPhaseEndsAt: string | null;
/** End of the payment drain tail — pending payments may settle until then. */
paymentDrainEndsAt?: string | null;
bookingWindowStatus: string;
bookingCycleNo: number;
departureDate: string;
@@ -94,16 +96,29 @@ const COUNTDOWN_TEXT: Partial<
PRE_WINDOW: { label: "Opens in", expiredText: "Opening now…" },
OPEN: { label: "Closes in", expiredText: "Review starting…" },
DOC_REVIEW: { label: "Doc review ends in", expiredText: "Payment starting…" },
PAYMENT: { label: "Payment ends in", expiredText: "Closing…" },
PAYMENT: { label: "Payment ends in", expiredText: "Finalizing…" },
};
function phaseCountdown(
w: WindowRow,
): { label: string; deadline: string; expiredText: string } | null {
function phaseCountdown(w: WindowRow): {
label: string;
deadline: string;
expiredText: string;
graceDeadline?: string | null;
graceLabel?: string;
} | null {
const state = bookingWindowUiState(w);
const text = COUNTDOWN_TEXT[state.kind];
if (!state.countdownTo || !text) return null;
return { ...text, deadline: state.countdownTo };
// Once the pay deadline lapses, pending payments still settle during the
// drain tail — count it down as "processing" instead of a stale "closing".
const grace =
state.kind === "PAYMENT" && w.paymentDrainEndsAt
? {
graceDeadline: w.paymentDrainEndsAt,
graceLabel: "Processing payments — closes in",
}
: undefined;
return { ...text, deadline: state.countdownTo, ...grace };
}
/** Badge label + Mantine color per UI state — same state the countdown uses. */
@@ -225,6 +240,8 @@ function WindowCard({ w }: { w: WindowRow }) {
deadline={cd.deadline}
label={cd.label}
expiredText={cd.expiredText}
graceDeadline={cd.graceDeadline}
graceLabel={cd.graceLabel}
size="xs"
/>
</Box>

View File

@@ -357,7 +357,14 @@ export function PhasedClearanceActionPanel({
<Stepper.Step
label="Request transit assignee"
description="Ask GL Djibouti to name the officer handling this shipment"
// Once the flow moves past this step its content collapses — the
// description is the only slot that stays visible, so it carries
// the assigned officer's name for GL Ethiopia.
description={
clearance.transitAssignee?.name
? `Transit assignee: ${clearance.transitAssignee.name}`
: "Ask GL Djibouti to name the officer handling this shipment"
}
icon={
clearance.transitAssignee?.name ? (
<CheckCircle2 size={14} />