mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
add contract clearance detail page and enhance contract requests filtering and ui fix
This commit is contained in:
@@ -596,11 +596,40 @@ export class ContractBookingService {
|
||||
if (!booking || booking.contractId !== contract.id) {
|
||||
throw new NotFoundException(`Booking ${bookingId} not found on this contract`);
|
||||
}
|
||||
if (!['CLEARANCE_READY', 'OPERATION_CHANGES_REQUESTED'].includes(booking.status)) {
|
||||
if (
|
||||
!['CLEARANCE_READY', 'OPERATION_CHANGES_REQUESTED', 'EXPIRED'].includes(
|
||||
booking.status,
|
||||
)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
'Clearance must be finalized before the booking can be completed.',
|
||||
);
|
||||
}
|
||||
// An unpaid booking that expired at train dispatch keeps its finished
|
||||
// per-booking clearance — GL rebooks it onto a new shipment day instead of
|
||||
// forcing the customer through a new shipment request + clearance fee.
|
||||
if (booking.status === 'EXPIRED') {
|
||||
// Only a booking that completed once (it has a price, so its clearance
|
||||
// finished and cargo is persisted) can be rebooked after expiry.
|
||||
if (!(Number(booking.totalAmount) > 0)) {
|
||||
throw new BadRequestException(
|
||||
'Only a previously completed booking can be rebooked after it expires.',
|
||||
);
|
||||
}
|
||||
// Expiry released the booking's contract-capacity hold; if the payload
|
||||
// re-states the cargo, make sure the released share is still free.
|
||||
if (dto.containers?.length || dto.bulkLines?.length) {
|
||||
await this.assertWithinQuantityCap(contract, dto);
|
||||
}
|
||||
// Drop the departed train's link and fall into the day-only resubmit
|
||||
// path below — same machinery as OPERATION_CHANGES_REQUESTED.
|
||||
await this.bookingsRepository.update(booking.id, {
|
||||
status: 'OPERATION_CHANGES_REQUESTED',
|
||||
trainScheduleId: null,
|
||||
} as never);
|
||||
booking.status = 'OPERATION_CHANGES_REQUESTED';
|
||||
booking.trainScheduleId = null;
|
||||
}
|
||||
// Path B: only GL Ethiopia completes a customs instance — the customer
|
||||
// never enters shipment data on a customs contract.
|
||||
if (contract.customsClearingEnabled) {
|
||||
|
||||
@@ -407,6 +407,9 @@ export default function GlCreateBookingForm() {
|
||||
|
||||
useEffect(() => {
|
||||
if (!bookingRequest || prefilled) return;
|
||||
// A rebook (?copyFrom=) seeds from the expired booking's real cargo —
|
||||
// richer than the request's bare quantities. Let that seed win the race.
|
||||
if (copyFromParam) return;
|
||||
setPrefilled(true);
|
||||
const lines = bookingRequest.requestedLines ?? {};
|
||||
if (lines.containers?.length) {
|
||||
@@ -448,13 +451,27 @@ export default function GlCreateBookingForm() {
|
||||
setContainerLines(
|
||||
lines.map((c) => {
|
||||
const qty = Math.max(1, c.quantity);
|
||||
// Carry the persisted per-unit details (numbers, seals, VGM, handling)
|
||||
// when the source booking has them — a rebooked EXPIRED booking does,
|
||||
// and its cargo is fixed server-side anyway.
|
||||
const units: UnitDraft[] =
|
||||
c.units?.length === qty
|
||||
? c.units.map((u) => ({
|
||||
containerNumber: u.containerNumber ?? "",
|
||||
sealNumber: u.sealNumber ?? "",
|
||||
vgmTons: u.vgmTons != null ? String(u.vgmTons) : "",
|
||||
isHazardous: Boolean(u.isHazardous),
|
||||
isReefer: Boolean(u.isReefer),
|
||||
isReturn: Boolean(u.isReturn),
|
||||
}))
|
||||
: Array.from({ length: qty }, emptyUnit);
|
||||
return {
|
||||
containerSize: String(c.containerType?.sizeFt ?? ""),
|
||||
quantity: String(qty),
|
||||
hazardousQuantity: "0",
|
||||
reeferQuantity: "0",
|
||||
returnQuantity: "0",
|
||||
units: Array.from({ length: qty }, emptyUnit),
|
||||
hazardousQuantity: String(units.filter((u) => u.isHazardous).length),
|
||||
reeferQuantity: String(units.filter((u) => u.isReefer).length),
|
||||
returnQuantity: String(units.filter((u) => u.isReturn).length),
|
||||
units,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
Clock,
|
||||
PackageCheck,
|
||||
PackagePlus,
|
||||
RotateCcw,
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
import type { Freight } from "@edr/types";
|
||||
@@ -110,6 +111,16 @@ export default function DocumentClearanceDetailPage() {
|
||||
hasPermission(user, FREIGHT_PERMS.contracts.createBooking) &&
|
||||
!isDjiboutiGl(user);
|
||||
|
||||
// The completed booking expired unpaid at train dispatch. Its per-booking
|
||||
// clearance is finished, so GL rebooks it onto a new day — the customer never
|
||||
// re-requests the shipment or pays the clearance fee again.
|
||||
const canRebookExpired =
|
||||
booking?.status === "EXPIRED" &&
|
||||
Boolean(booking?.contractId) &&
|
||||
Number(booking?.totalAmount ?? 0) > 0 &&
|
||||
hasPermission(user, FREIGHT_PERMS.contracts.createBooking) &&
|
||||
!isDjiboutiGl(user);
|
||||
|
||||
const docsPhaseComplete =
|
||||
clearance?.milestones?.some(
|
||||
(m) => m.milestoneCode === "DOCUMENTS_APPROVED" && m.status === "COMPLETED",
|
||||
@@ -194,6 +205,19 @@ export default function DocumentClearanceDetailPage() {
|
||||
>
|
||||
Create booking
|
||||
</Button>
|
||||
) : canRebookExpired ? (
|
||||
<Button
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
leftSection={<RotateCcw size={16} />}
|
||||
onClick={() =>
|
||||
navigate(
|
||||
`/dashboard/contracts/${booking!.contractId}/bookings/${id}/complete?copyFrom=${id}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
Rebook shipment
|
||||
</Button>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -727,8 +727,12 @@ export default function ContractClearanceListPage() {
|
||||
)
|
||||
}
|
||||
onRebook={(row) =>
|
||||
// Re-complete the SAME expired booking (new day, same finished
|
||||
// per-booking clearance) — a fresh create-booking would spawn a
|
||||
// new instance and force the customer through clearance + fee
|
||||
// again.
|
||||
navigate(
|
||||
`/dashboard/contracts/${row.contractId}/create-booking?copyFrom=${row.id}`,
|
||||
`/dashboard/contracts/${row.contractId}/bookings/${row.id}/complete?copyFrom=${row.id}`,
|
||||
)
|
||||
}
|
||||
onViewContract={(contractId) =>
|
||||
|
||||
@@ -79,6 +79,8 @@ export interface BookingContainerUnit {
|
||||
vgmTons: number;
|
||||
isHazardous?: boolean;
|
||||
isReefer?: boolean;
|
||||
/** This container ships back empty after unloading (equipment return). */
|
||||
isReturn?: boolean;
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user