mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 13:05:44 +00:00
add CUSTOMS type to priority configs and update related logic
This commit is contained in:
@@ -150,16 +150,19 @@ export default function GlCreateBookingForm() {
|
||||
[bookingWindows],
|
||||
);
|
||||
|
||||
// Soonest future window across all routes, used for the "next window" notice.
|
||||
// Next future window across all routes, used for the "next window" notice —
|
||||
// the train dispatching soonest among those not yet open, matching the
|
||||
// departure-date ordering of the window cards.
|
||||
const nextWindow = useMemo(() => {
|
||||
const now = Date.now();
|
||||
return (bookingWindows ?? [])
|
||||
.filter((w) => w.windowOpensAt && new Date(w.windowOpensAt).getTime() > now)
|
||||
.sort(
|
||||
(a, b) =>
|
||||
new Date(a.windowOpensAt!).getTime() -
|
||||
new Date(b.windowOpensAt!).getTime(),
|
||||
)[0];
|
||||
.sort((a, b) => {
|
||||
const da = a.departureDate ? new Date(a.departureDate).getTime() : Infinity;
|
||||
const db = b.departureDate ? new Date(b.departureDate).getTime() : Infinity;
|
||||
if (da !== db) return da - db;
|
||||
return new Date(a.windowOpensAt!).getTime() - new Date(b.windowOpensAt!).getTime();
|
||||
})[0];
|
||||
}, [bookingWindows]);
|
||||
|
||||
const [scheduledDate, setScheduledDate] = useState("");
|
||||
|
||||
@@ -125,16 +125,15 @@ function phaseCountdown(
|
||||
}
|
||||
}
|
||||
|
||||
/** Drop windows whose booking window (or the train itself) has already passed. */
|
||||
/**
|
||||
* Drop windows the SERVER considers finished — keyed off windowPhase, never the
|
||||
* client clock. The server query already excludes terminal / departed rows;
|
||||
* comparing `Date.now()` here only re-introduced clock skew that made a card
|
||||
* vanish and reappear on refresh. Trust the server phase (live-patched over the
|
||||
* socket) instead.
|
||||
*/
|
||||
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;
|
||||
// Still live while in a post-close staff phase (doc review / payment).
|
||||
if (w.windowPhase === "DOC_REVIEW" || w.windowPhase === "PAYMENT") return false;
|
||||
if (departs != null && departs <= now) return true;
|
||||
if (closes != null && closes <= now) return true;
|
||||
return false;
|
||||
return w.windowPhase === "DONE" || w.windowPhase === "CLOSED_FOR_DAY";
|
||||
}
|
||||
|
||||
function WindowCard({ w }: { w: WindowRow }) {
|
||||
@@ -286,13 +285,13 @@ export function GlUpcomingWindowsSection({
|
||||
);
|
||||
// Canceled schedules are retired to windowPhase='DONE' server-side, so the
|
||||
// guard above already excludes them; they never reach the upcoming list.
|
||||
// Open lanes first, then by opening time.
|
||||
// Order by the train's dispatch (departure) date, nearest first. Open-now
|
||||
// breaks ties on the same departure.
|
||||
return rows.sort((a, b) => {
|
||||
const openDiff = Number(b.isOpenNow) - Number(a.isOpenNow);
|
||||
if (openDiff !== 0) return openDiff;
|
||||
const at = a.windowOpensAt ? new Date(a.windowOpensAt).getTime() : Infinity;
|
||||
const bt = b.windowOpensAt ? new Date(b.windowOpensAt).getTime() : Infinity;
|
||||
return at - bt;
|
||||
const da = a.departureDate ? new Date(a.departureDate).getTime() : Infinity;
|
||||
const db = b.departureDate ? new Date(b.departureDate).getTime() : Infinity;
|
||||
if (da !== db) return da - db;
|
||||
return Number(b.isOpenNow) - Number(a.isOpenNow);
|
||||
});
|
||||
}, [data]);
|
||||
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -187,6 +187,7 @@ const CURRENCIES = [
|
||||
const PRIORITY_CONFIG_TYPES = [
|
||||
{ label: "Wagon count", value: "WAGON" },
|
||||
{ label: "Payment currency", value: "CURRENCY" },
|
||||
{ label: "Customs clearance", value: "CUSTOMS" },
|
||||
];
|
||||
|
||||
const codeColumn = (key: string, header = "Code"): ResourceColumn => ({
|
||||
@@ -310,7 +311,7 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
|
||||
slug: "priority-configs",
|
||||
label: "Priority Rules",
|
||||
category: "rules",
|
||||
subtitle: "Wagon-count and payment-currency scoring rules",
|
||||
subtitle: "Wagon-count, payment-currency, and customs scoring rules",
|
||||
searchPlaceholder: "Search priority rules...",
|
||||
orderConfig: { field: "displayOrder", label: "Display order" },
|
||||
columns: [
|
||||
@@ -337,7 +338,7 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
|
||||
optional: true,
|
||||
options: [{ label: "None", value: RULE_ENGINE_SELECT_NONE }, ...CURRENCIES],
|
||||
placeholder: "Select a currency",
|
||||
hideWhen: { field: "type", equals: ["WAGON"] },
|
||||
hideWhen: { field: "type", equals: ["WAGON", "CUSTOMS"] },
|
||||
},
|
||||
{ name: "minWagonCount", label: "Min wagon count", type: "number", required: true },
|
||||
{ name: "maxWagonCount", label: "Max wagon count", type: "number", required: true },
|
||||
@@ -357,7 +358,6 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
|
||||
codeColumn("code"),
|
||||
{ id: "serviceName", header: "Service name", accessorKey: "serviceName" },
|
||||
{ id: "displayOrder", header: "#", accessorKey: "displayOrder", format: "number" },
|
||||
{ id: "priorityBonusPoints", header: "Bonus pts", accessorKey: "priorityBonusPoints", format: "number" },
|
||||
activeColumn,
|
||||
],
|
||||
formFields: [
|
||||
@@ -367,7 +367,6 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
|
||||
{ name: "includesFirstMile", label: "Includes first mile", type: "boolean" },
|
||||
{ name: "includesLastMile", label: "Includes last mile", type: "boolean" },
|
||||
{ name: "includesCustoms", label: "Includes customs", type: "boolean" },
|
||||
{ name: "priorityBonusPoints", label: "Priority bonus points", type: "number" },
|
||||
{ name: "isActive", label: "Active", type: "boolean" },
|
||||
],
|
||||
},
|
||||
|
||||
@@ -207,7 +207,7 @@ export interface BookingDetail {
|
||||
company?: BookingNamedRef & Partial<BookingCompany>;
|
||||
originYard?: BookingNamedRef;
|
||||
destinationYard?: BookingNamedRef;
|
||||
serviceType?: BookingNamedRef & { code?: string; priorityBonusPoints?: number; includesCustoms?: boolean; includesFirstMile?: boolean; includesLastMile?: boolean };
|
||||
serviceType?: BookingNamedRef & { code?: string; includesCustoms?: boolean; includesFirstMile?: boolean; includesLastMile?: boolean };
|
||||
cargoType?: BookingNamedRef;
|
||||
shippingLine?: BookingNamedRef;
|
||||
bookingContainers?: BookingContainerLine[];
|
||||
@@ -239,7 +239,6 @@ export interface BookingListRow {
|
||||
priorityScore: number;
|
||||
schedulingStatus?: string;
|
||||
serviceTypeLabel?: string;
|
||||
serviceTypeBonus?: number;
|
||||
trainScheduleId?: string | null;
|
||||
isGovernment?: boolean;
|
||||
governmentInstitution?: string | null;
|
||||
|
||||
Reference in New Issue
Block a user