add CUSTOMS type to priority configs and update related logic

This commit is contained in:
Marshal
2026-07-07 16:42:57 +00:00
parent f300600bfa
commit b57840c907
26 changed files with 352 additions and 120 deletions

View File

@@ -15,11 +15,56 @@ import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
// prefix — strip a trailing `/api` if the base URL carries one.
const SOCKET_ORIGIN = String(API_BASE_URL ?? "").replace(/\/api\/?$/, "");
// The two carousel window lists share the MyBookingWindow-shaped row and can be
// patched in place. The batch board is a richer, differently-shaped view, so it
// stays on a (debounced) invalidate.
const WINDOW_ACTIONS = new Set(["all-booking-windows", "contractBookingWindows"]);
/** Shape shared by both carousel window lists (all-lanes + contract-scoped). */
interface WindowRow {
scheduleId: string;
windowPhase: string | null;
isOpenNow: boolean;
windowOpensAt: string | null;
windowClosesAt: string | null;
docReviewEndsAt: string | null;
paymentPhaseEndsAt: string | null;
bookingWindowStatus: string;
bookingCycleNo: number;
departureDate: string;
}
function isWindowKey(key: readonly unknown[]): boolean {
return key[0] === "train-scheduling" && WINDOW_ACTIONS.has(String(key[1]));
}
/**
* Subscribes to live booking-window pushes for staff. Every phase transition
* the window engine applies invalidates the GL windows carousel and the batch
* board, so both flip the moment the backend does — polling stays only as a
* fallback.
* Fold a server phase push onto a cached window row, recomputing isOpenNow the
* same way the server does (phase OPEN + status OPEN) so live-patched state can
* never disagree with a fresh REST fetch on refresh.
*/
function applyEvent<T extends WindowRow>(row: T, event: BookingWindowPhaseEvent): T {
return {
...row,
windowPhase: event.phase,
bookingWindowStatus: event.bookingWindowStatus ?? row.bookingWindowStatus,
bookingCycleNo: event.bookingCycleNo,
isOpenNow: event.phase === "OPEN" && event.bookingWindowStatus === "OPEN",
windowOpensAt: event.windowOpensAt,
windowClosesAt: event.windowClosesAt,
docReviewEndsAt: event.docReviewEndsAt,
paymentPhaseEndsAt: event.paymentPhaseEndsAt,
departureDate: event.scheduledDepartureDate ?? row.departureDate,
};
}
/**
* Subscribes to live booking-window pushes for staff. A phase transition carries
* the schedule's full new state; we fold it straight into the carousel window
* lists with setQueriesData rather than invalidating — same rationale as the
* portal hook (no per-push refetch storm; live + refreshed state agree, killing
* the refresh-jump). The batch board is a different-shaped view, so it keeps a
* debounced invalidate, as do pushes for schedules not present in any list.
*/
export function useBookingWindowSocket(enabled: boolean = true) {
const qc = useQueryClient();
@@ -47,19 +92,53 @@ export function useBookingWindowSocket(enabled: boolean = true) {
console.debug("[booking-windows] socket disconnected:", reason),
);
socket.on(
BOOKING_WINDOW_WS_EVENTS.PHASE,
(_event: BookingWindowPhaseEvent) => {
qc.invalidateQueries({
queryKey: ["train-scheduling", "all-booking-windows"],
});
qc.invalidateQueries({
// Coalesce the batch-board refresh (and the unknown-schedule fallback) so a
// burst of pushes triggers at most one invalidation per window.
let refetchTimer: ReturnType<typeof setTimeout> | null = null;
const scheduleRefetch = (includeWindowLists: boolean) => {
if (refetchTimer) return;
refetchTimer = setTimeout(() => {
refetchTimer = null;
void qc.invalidateQueries({
queryKey: QUERY_KEYS.TRAIN_SCHEDULING.batchBoard(),
});
if (includeWindowLists) {
void qc.invalidateQueries({
predicate: (q) => isWindowKey(q.queryKey),
});
}
}, 800);
};
socket.on(
BOOKING_WINDOW_WS_EVENTS.PHASE,
(event: BookingWindowPhaseEvent) => {
let patchedSomewhere = false;
qc.setQueriesData<WindowRow[]>(
{ predicate: (q) => isWindowKey(q.queryKey) },
(rows) => {
if (!rows) return rows;
let changed = false;
const next = rows.map((row) => {
if (row.scheduleId !== event.scheduleId) return row;
changed = true;
patchedSomewhere = true;
return applyEvent(row, event);
});
return changed ? next : rows;
},
);
// Always refresh the batch board (different shape, not patched). When the
// schedule wasn't in any window list either, refresh those too so a newly
// announced window surfaces. Both debounced — no per-push stampede.
scheduleRefetch(!patchedSomewhere);
},
);
return () => {
if (refetchTimer) clearTimeout(refetchTimer);
socket.off();
socket.disconnect();
};

View File

@@ -39,7 +39,6 @@ export function toBookingListRow(booking: BookingDetail): BookingListRow {
booking.serviceType?.label ??
booking.serviceType?.name ??
booking.serviceType?.code,
serviceTypeBonus: booking.serviceType?.priorityBonusPoints ?? 0,
trainScheduleId: booking.trainScheduleId ?? null,
isGovernment: booking.isGovernment ?? false,
governmentInstitution: booking.governmentInstitution ?? null,