Enhance contract management by adding booking windows section and updating invoice logic for offloaded cargo

This commit is contained in:
Marshal
2026-07-07 09:49:12 +00:00
parent f4d28e77fa
commit 1c18fdbd52
8 changed files with 422 additions and 22 deletions

View File

@@ -646,7 +646,9 @@ function FinalInvoiceStep({
const invoice = clearance.finalInvoice ?? null;
const paid = invoice?.status === "PAID";
if (!clearance.offloaded && !invoice) {
// Export: OFFLOADED is a DJ doc milestone that may never be recorded, so the
// secured gate pass is enough to open invoicing. Sending an invoice is optional.
if (!clearance.offloaded && !clearance.gatepassGranted && !invoice) {
return (
<StepStatus
done={false}
@@ -740,7 +742,7 @@ function FinalInvoiceStep({
) : canDjAct && bookingId ? (
<>
<Text size="sm" c="dimmed">
Cargo offloaded send the final invoice to the customer.
Send the final invoice to the customer if post-arrival charges apply (optional).
</Text>
<Button
color="edr-green"

View File

@@ -21,7 +21,28 @@ import { CountdownTimer } from "@edr/ui-common";
import { useBookingWindowSocket } from "@/features/bookingWindows/useBookingWindowSocket";
import { api } from "@/services/api";
import type { StaffBookingWindow } from "@/types/trainScheduling";
/**
* The fields a window card needs. Structural so both `StaffBookingWindow`
* (all-lanes staff feed) and `BookingWindow` (contract-scoped feed, which
* carries no train number) satisfy it.
*/
interface WindowRow {
scheduleId: string;
trainNumber?: string | null;
direction: string | null;
windowPhase: string | null;
isOpenNow: boolean;
windowOpensAt: string | null;
windowClosesAt: string | null;
docReviewEndsAt: string | null;
paymentPhaseEndsAt: string | null;
bookingWindowStatus: string;
bookingCycleNo: number;
departureDate: string;
origin: string | null;
destination: string | null;
}
/** All window times are communicated in East Africa Time. */
const TZ = "Africa/Addis_Ababa";
@@ -46,7 +67,7 @@ function fmtTime(iso: string): string {
});
}
function windowLabel(w: StaffBookingWindow): string {
function windowLabel(w: WindowRow): string {
if (w.windowOpensAt && w.windowClosesAt) {
return `${fmtDay(w.windowOpensAt)} · ${fmtTime(w.windowOpensAt)} ${fmtTime(
w.windowClosesAt,
@@ -64,7 +85,7 @@ function windowLabel(w: StaffBookingWindow): string {
* between refetches announces what comes next rather than the bare "Expired".
*/
function phaseCountdown(
w: StaffBookingWindow,
w: WindowRow,
): { label: string; deadline: string; expiredText: string } | null {
switch (w.windowPhase) {
case "PRE_WINDOW":
@@ -105,7 +126,7 @@ function phaseCountdown(
}
/** Drop windows whose booking window (or the train itself) has already passed. */
function isPast(w: StaffBookingWindow): boolean {
function isPast(w: WindowRow): boolean {
const now = Date.now();
const closes = w.windowClosesAt ? new Date(w.windowClosesAt).getTime() : null;
const departs = w.departureDate ? new Date(w.departureDate).getTime() : null;
@@ -116,7 +137,7 @@ function isPast(w: StaffBookingWindow): boolean {
return false;
}
function WindowCard({ w }: { w: StaffBookingWindow }) {
function WindowCard({ w }: { w: WindowRow }) {
const cd = phaseCountdown(w);
const open = w.isOpenNow;
const isImport = w.direction === "IMPORT";
@@ -218,21 +239,45 @@ function WindowCard({ w }: { w: StaffBookingWindow }) {
);
}
interface GlUpcomingWindowsSectionProps {
/**
* Scope the card to one contract: only windows on that contract's routes
* (and therefore its import/export direction) are shown. Omit for the
* all-lanes staff feed on the clearance queue.
*/
contractId?: string;
}
/**
* All announced booking windows (import cycles + export FCFS) across every lane,
* shown to GL ET on the clearance queue as a paged carousel — three lanes per
* page, arrows to flip. Mirrors the customer's portal "Booking Windows" card.
* Hidden when nothing is pending.
* Announced booking windows (import cycles + export FCFS) as a paged carousel —
* three lanes per page, arrows to flip. Without `contractId` it shows every
* lane (GL ET clearance queue); with `contractId` it shows only the windows
* matching that contract's routes/direction (clearance detail page). Mirrors
* the customer's portal "Booking Windows" card. Hidden when nothing is pending.
*/
export function GlUpcomingWindowsSection() {
export function GlUpcomingWindowsSection({
contractId,
}: GlUpcomingWindowsSectionProps = {}) {
// Live pushes flip cards the moment the window engine transitions a phase;
// the 60s poll below stays only as a fallback.
useBookingWindowSocket();
const { data, isLoading } = useQuery(
api.trainScheduling.allBookingWindows.queryOptions({
const allLanes = useQuery({
...api.trainScheduling.allBookingWindows.queryOptions({
refetchInterval: 60_000,
}),
);
enabled: !contractId,
});
const contractLanes = useQuery({
...api.trainScheduling.contractBookingWindows.queryOptions({
input: { contractId: contractId ?? "" },
refetchInterval: 60_000,
}),
enabled: Boolean(contractId),
});
const data: WindowRow[] | undefined = contractId
? contractLanes.data
: allLanes.data;
const isLoading = contractId ? contractLanes.isLoading : allLanes.isLoading;
const [page, setPage] = useState(0);
const windows = useMemo(() => {
@@ -270,7 +315,9 @@ export function GlUpcomingWindowsSection() {
Booking windows
</Text>
<Text fz={13} c="dimmed">
Import and export booking windows across all lanes (EAT)
{contractId
? "Booking windows on this contract's routes (EAT)"
: "Import and export booking windows across all lanes (EAT)"}
</Text>
</Box>
</Group>