mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
full change-booking flow for op revisions
This commit is contained in:
@@ -51,7 +51,12 @@ export function ClearanceCard({ booking }: { booking: Freight.IBooking }) {
|
||||
}
|
||||
|
||||
const summary =
|
||||
status === "CLEARANCE_READY" ? (
|
||||
status === "OPERATION_CHANGES_REQUESTED" ? (
|
||||
<Alert color="yellow" radius="md" icon={<Clock size={18} />}>
|
||||
Operations returned this order for changes. Update the booking details,
|
||||
pick a new shipment day and resubmit.
|
||||
</Alert>
|
||||
) : status === "CLEARANCE_READY" ? (
|
||||
<Alert color="teal" radius="md" icon={<CheckCircle2 size={18} />}>
|
||||
{`${
|
||||
booking.customsClearingEnabled
|
||||
@@ -103,9 +108,11 @@ export function ClearanceCard({ booking }: { booking: Freight.IBooking }) {
|
||||
{summary}
|
||||
|
||||
<Text fz="12.5px" c="dimmed" mt="sm">
|
||||
{isBookAction
|
||||
? "Use “Book” to enter the cargo details and schedule your shipment."
|
||||
: `Use “${action?.label ?? "the action button"}” to manage your ${docNoun}.`}
|
||||
{status === "OPERATION_CHANGES_REQUESTED"
|
||||
? "Use “Change booking” to update the details and pick a new shipment day."
|
||||
: isBookAction
|
||||
? "Use “Book” to enter the cargo details and schedule your shipment."
|
||||
: `Use “${action?.label ?? "the action button"}” to manage your ${docNoun}.`}
|
||||
</Text>
|
||||
|
||||
{!isBookAction && (
|
||||
|
||||
@@ -90,11 +90,21 @@ function actionByStatus(booking: ActionBooking): BookingNextAction | null {
|
||||
title: "Schedule your shipment",
|
||||
};
|
||||
case "OPERATION_CHANGES_REQUESTED":
|
||||
return {
|
||||
kind: "SCHEDULE_OPERATION",
|
||||
label: "Choose day & resubmit",
|
||||
title: "Resubmit your shipment",
|
||||
};
|
||||
// Contract bookings reopen the full completion form (cargo + shipment
|
||||
// day, prefilled from the booking) — same page as the initial booking.
|
||||
// Contract-less bookings keep the in-place day-picker modal.
|
||||
return booking.contractId
|
||||
? {
|
||||
kind: "BOOK",
|
||||
label: "Change booking",
|
||||
title: "Change your booking",
|
||||
to: `/contracts/${booking.contractId}/bookings/${booking.id}/complete`,
|
||||
}
|
||||
: {
|
||||
kind: "SCHEDULE_OPERATION",
|
||||
label: "Choose day & resubmit",
|
||||
title: "Resubmit your shipment",
|
||||
};
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
CheckCircle2,
|
||||
ChevronLeft,
|
||||
FileDown,
|
||||
FileText,
|
||||
FileUp,
|
||||
Flame,
|
||||
MapPin,
|
||||
@@ -59,6 +60,7 @@ import {
|
||||
contractsService,
|
||||
type ShipmentValidation,
|
||||
} from "@/services/contracts.service";
|
||||
import { downloadStoredFile } from "@/services/files.service";
|
||||
import {
|
||||
SelectField,
|
||||
StepCard,
|
||||
@@ -245,6 +247,127 @@ function bulkUnitOfMeasure(
|
||||
return hasPerItem ? "PER_ITEM" : "PER_TON";
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefill for a changes-requested resubmit: the booking's persisted cargo,
|
||||
* currency and route become the form's starting values so the customer edits
|
||||
* what exists instead of retyping it. The shipment day is deliberately left
|
||||
* empty — a new day must be picked.
|
||||
*/
|
||||
function mapBookingToShipmentValues(
|
||||
booking: Freight.IBooking,
|
||||
contract: Freight.IContract,
|
||||
): Partial<ShipmentFormInputValues> {
|
||||
const b = booking as unknown as {
|
||||
cargoFreeText?: string | null;
|
||||
contractRouteId?: string | null;
|
||||
cargoTotalWeightVgm?: number | string | null;
|
||||
bulkTotalWeightTons?: number | string | null;
|
||||
bulkHazardousQuantity?: number | string | null;
|
||||
bulkReeferQuantity?: number | string | null;
|
||||
bookingContainers?: Array<{
|
||||
quantity?: number;
|
||||
hazardousQuantity?: number | string | null;
|
||||
reeferQuantity?: number | string | null;
|
||||
returnQuantity?: number | string | null;
|
||||
containerType?: { sizeFt?: number | null } | null;
|
||||
units?: Array<{
|
||||
containerNumber?: string;
|
||||
sealNumber?: string | null;
|
||||
vgmTons?: number | string;
|
||||
isHazardous?: boolean;
|
||||
isReefer?: boolean;
|
||||
isReturn?: boolean;
|
||||
}>;
|
||||
}>;
|
||||
};
|
||||
const values: Partial<ShipmentFormInputValues> = {
|
||||
paymentCurrency: booking.paymentCurrency === "ETB" ? "ETB" : "USD",
|
||||
withReturn: booking.equipmentReturn === "WITH_RETURN",
|
||||
cargoDescription: b.cargoFreeText ?? "",
|
||||
...(b.contractRouteId ? { contractRouteId: b.contractRouteId } : {}),
|
||||
};
|
||||
if (contract.freightType === "CONTAINER") {
|
||||
const rows = b.bookingContainers ?? [];
|
||||
const lineFor = (size: "20ft" | "40ft") => {
|
||||
const bc = rows.find(
|
||||
(r) => (r.containerType?.sizeFt === 40 ? "40ft" : "20ft") === size,
|
||||
);
|
||||
return {
|
||||
containerSize: size,
|
||||
quantity: String(bc?.quantity ?? 0),
|
||||
hazardousQuantity: String(Number(bc?.hazardousQuantity ?? 0)),
|
||||
reeferQuantity: String(Number(bc?.reeferQuantity ?? 0)),
|
||||
returnQuantity: String(Number(bc?.returnQuantity ?? 0)),
|
||||
units: (bc?.units ?? []).map((u) => ({
|
||||
containerNumber: u.containerNumber ?? "",
|
||||
sealNumber: u.sealNumber ?? "",
|
||||
vgmTons: String(Number(u.vgmTons ?? 0)),
|
||||
isHazardous: Boolean(u.isHazardous),
|
||||
isReefer: Boolean(u.isReefer),
|
||||
isReturn: Boolean(u.isReturn),
|
||||
})),
|
||||
};
|
||||
};
|
||||
const sizes = (contract.cargoScope ?? [])
|
||||
.map((s) => s.containerSize)
|
||||
.filter((s): s is "20ft" | "40ft" => s === "20ft" || s === "40ft");
|
||||
values.containers = (sizes.length ? sizes : ["20ft", "40ft"]).map(lineFor);
|
||||
} else {
|
||||
const perItem = bulkUnitOfMeasure(contract) === "PER_ITEM";
|
||||
const amount = Number(b.cargoTotalWeightVgm ?? 0);
|
||||
if (perItem) {
|
||||
values.itemCount = amount ? String(amount) : "";
|
||||
values.cargoWeightTons =
|
||||
b.bulkTotalWeightTons != null
|
||||
? String(Number(b.bulkTotalWeightTons))
|
||||
: "";
|
||||
} else {
|
||||
values.cargoWeightTons = amount ? String(amount) : "";
|
||||
}
|
||||
values.bulkHazardousQuantity = String(Number(b.bulkHazardousQuantity ?? 0));
|
||||
values.bulkReeferQuantity = String(Number(b.bulkReeferQuantity ?? 0));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
/** Read-only list of the booking's already-uploaded documents (resubmit view). */
|
||||
function UploadedDocumentsCard({ booking }: { booking: Freight.IBooking }) {
|
||||
const files =
|
||||
(booking as unknown as { files?: Array<{ id: string; name: string }> })
|
||||
.files ?? [];
|
||||
if (!files.length) return null;
|
||||
return (
|
||||
<Paper withBorder radius="lg" p="lg">
|
||||
<Group gap={8} mb={4}>
|
||||
<FileText size={16} />
|
||||
<Text fw={700} fz="sm">
|
||||
Your uploaded documents
|
||||
</Text>
|
||||
</Group>
|
||||
<Text fz={12.5} c="dimmed" mb="sm">
|
||||
These stay attached to the booking — no need to upload them again.
|
||||
</Text>
|
||||
<Stack gap={6}>
|
||||
{files.map((f) => (
|
||||
<Group key={f.id} justify="space-between" wrap="nowrap">
|
||||
<Text fz={13} truncate>
|
||||
{f.name}
|
||||
</Text>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
radius="md"
|
||||
onClick={() => void downloadStoredFile(f.id, f.name)}
|
||||
aria-label={`Download ${f.name}`}
|
||||
>
|
||||
<FileDown size={15} />
|
||||
</ActionIcon>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
function NewShipmentBookingForm({
|
||||
contract,
|
||||
contractId,
|
||||
@@ -306,6 +429,34 @@ function NewShipmentBookingForm({
|
||||
: 0;
|
||||
const hasOdd20ft = ft20Total % 2 === 1;
|
||||
|
||||
// COMPLETION mode: fetch the booking — a changes-requested resubmit prefills
|
||||
// the form from it and shows the operations note + uploaded documents.
|
||||
const { data: completeBooking } = useQuery(
|
||||
api.bookings.get.queryOptions({
|
||||
input: { id: completeBookingId! },
|
||||
enabled: Boolean(completeBookingId),
|
||||
}),
|
||||
);
|
||||
const isResubmit = Boolean(
|
||||
completeBooking &&
|
||||
["OPERATION_CHANGES_REQUESTED", "EXPIRED"].includes(
|
||||
completeBooking.status as string,
|
||||
) &&
|
||||
(((completeBooking as unknown as { bookingContainers?: unknown[] })
|
||||
.bookingContainers?.length ?? 0) > 0 ||
|
||||
Number(completeBooking.cargoTotalWeightVgm ?? 0) > 0),
|
||||
);
|
||||
const prefilledRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (!isResubmit || prefilledRef.current || !completeBooking) return;
|
||||
prefilledRef.current = true;
|
||||
form.reset({
|
||||
...form.getValues(),
|
||||
...mapBookingToShipmentValues(completeBooking, contract),
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isResubmit]);
|
||||
|
||||
const submitMutation = useMutation({
|
||||
mutationFn: (dto: Freight.CreateBookingUnderContractDto) =>
|
||||
completeBookingId
|
||||
@@ -488,12 +639,16 @@ function NewShipmentBookingForm({
|
||||
style={{ letterSpacing: "-0.01em" }}
|
||||
>
|
||||
{completeBookingId
|
||||
? "Complete Your Booking"
|
||||
? isResubmit
|
||||
? "Change Your Booking"
|
||||
: "Complete Your Booking"
|
||||
: "New Shipment Booking"}
|
||||
</Title>
|
||||
<Text size="sm" c="edr-muted" mt={4}>
|
||||
{completeBookingId
|
||||
? `Clearance is finalized — enter the cargo details and shipment day to complete your booking under contract ${contract.reference}.`
|
||||
? isResubmit
|
||||
? `Update the details below and pick a new shipment day, then resubmit your booking under contract ${contract.reference}.`
|
||||
: `Clearance is finalized — enter the cargo details and shipment day to complete your booking under contract ${contract.reference}.`
|
||||
: `Book a shipment against contract ${contract.reference}.`}
|
||||
</Text>
|
||||
</Box>
|
||||
@@ -535,6 +690,16 @@ function NewShipmentBookingForm({
|
||||
|
||||
{/* Single-step form — all sections on one page. */}
|
||||
<Stack gap="lg" className="mx-auto max-w-4xl">
|
||||
{isResubmit && completeBooking?.latestChangeRequestNote && (
|
||||
<Alert
|
||||
color="yellow"
|
||||
icon={<AlertCircle size={16} />}
|
||||
radius="md"
|
||||
title="Operations requested changes"
|
||||
>
|
||||
<Text size="sm">{completeBooking.latestChangeRequestNote}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
<RouteStep form={form} contract={contract} routes={routes} />
|
||||
<CargoStep form={form} contract={contract} />
|
||||
{/* Legacy contracts only — WITH_RETURN contracts capture per-line
|
||||
@@ -547,6 +712,9 @@ function NewShipmentBookingForm({
|
||||
routes={routes}
|
||||
completeBookingId={completeBookingId ?? null}
|
||||
/>
|
||||
{isResubmit && completeBooking && (
|
||||
<UploadedDocumentsCard booking={completeBooking} />
|
||||
)}
|
||||
{/* Notes are captured when the booking is initiated — completing
|
||||
a bare booking does not re-ask for them. */}
|
||||
{!completeBookingId && <NotesSection form={form} />}
|
||||
@@ -594,7 +762,7 @@ function NewShipmentBookingForm({
|
||||
onClick={handleReview}
|
||||
disabled={hasOdd20ft}
|
||||
>
|
||||
Review price & book
|
||||
{isResubmit ? "Change booking" : "Review price & book"}
|
||||
</Button>
|
||||
</Box>
|
||||
</Tooltip>
|
||||
|
||||
Reference in New Issue
Block a user