Merge pull request #701 from Tria-plc/freight_feature/usermanagement

Freight feature/usermanagement
This commit is contained in:
marshal
2026-07-15 13:32:37 +03:00
committed by GitHub
43 changed files with 1900 additions and 312 deletions

View File

@@ -6,7 +6,7 @@ import {
ChevronLeft,
ChevronRight,
} from "lucide-react";
import { CountdownTimer } from "@edr/ui-common";
import { CountdownTimer, bookingWindowUiState } from "@edr/ui-common";
import type { MyBookingWindow } from "@/services/bookings.service";
import { windowRouteStops } from "@/pages/contracts/booking-window";
import { Card } from "./Card";
@@ -50,54 +50,34 @@ function windowLabel(w: MyBookingWindow): string {
}
/**
* The countdown for whichever phase the window is currently in. Phases run:
* pre-window (opens at windowOpensAt) → open (closes at windowClosesAt) →
* document review (docReviewEndsAt) → payment (paymentPhaseEndsAt).
* The countdown for the window's UI state (shared with the status badge via
* `bookingWindowUiState`, so the two can never contradict — a FULL train shows
* no ticking "closes in" under a non-open badge).
*
* `label` describes the deadline being counted down to; `expiredText` names the
* NEXT step so that when a deadline lapses between the 60s refetches the row
* announces what comes next ("Booking opening now…", "Review starting…") rather
* than the bare word "Expired". Returns null when no phase is timing down.
*/
const COUNTDOWN_TEXT: Partial<
Record<
ReturnType<typeof bookingWindowUiState>["kind"],
{ label: string; expiredText: string }
>
> = {
PRE_WINDOW: { label: "Booking opens in", expiredText: "Booking opening now…" },
OPEN: { label: "Window closes in", expiredText: "Document review starting…" },
DOC_REVIEW: { label: "Document review ends in", expiredText: "Payment starting…" },
PAYMENT: { label: "Payment due in", expiredText: "Payment window closing…" },
};
function phaseCountdown(
w: MyBookingWindow,
): { label: string; deadline: string; expiredText: string } | null {
switch (w.windowPhase) {
case "PRE_WINDOW":
if (w.windowOpensAt)
return {
label: "Booking opens in",
deadline: w.windowOpensAt,
expiredText: "Booking opening now…",
};
return null;
case "OPEN":
if (w.windowClosesAt)
return {
label: "Window closes in",
deadline: w.windowClosesAt,
expiredText: "Document review starting…",
};
return null;
case "DOC_REVIEW":
if (w.docReviewEndsAt)
return {
label: "Document review ends in",
deadline: w.docReviewEndsAt,
expiredText: "Payment starting…",
};
return null;
case "PAYMENT":
if (w.paymentPhaseEndsAt)
return {
label: "Payment due in",
deadline: w.paymentPhaseEndsAt,
expiredText: "Payment window closing…",
};
return null;
default:
return null;
}
const state = bookingWindowUiState(w);
const text = COUNTDOWN_TEXT[state.kind];
if (!state.countdownTo || !text) return null;
return { ...text, deadline: state.countdownTo };
}
function Pill({
@@ -147,19 +127,47 @@ function DirectionBadge({ direction }: { direction: MyBookingWindow["direction"]
}
function StatusBadge({ window: w }: { window: MyBookingWindow }) {
if (w.isOpenNow) {
return (
<Pill bg="#ECF6F1" color="#0A6F4D" border="#CDEBDD">
Open now
</Pill>
);
}
if (w.windowPhase === "PRE_WINDOW" && w.windowOpensAt) {
return (
<Pill bg="#FEF6E6" color="#B07D14">
Opens at {fmtTime(w.windowOpensAt)} EAT
</Pill>
);
const state = bookingWindowUiState(w);
switch (state.kind) {
case "OPEN":
return (
<Pill bg="#ECF6F1" color="#0A6F4D" border="#CDEBDD">
Open now
</Pill>
);
case "FULL":
return (
<Pill bg="#FDECEA" color="#B3261E" border="#F6C9C4">
Train full
</Pill>
);
case "PRE_WINDOW":
if (w.windowOpensAt) {
return (
<Pill bg="#FEF6E6" color="#B07D14">
Opens at {fmtTime(w.windowOpensAt)} EAT
</Pill>
);
}
break;
case "DOC_REVIEW":
return (
<Pill bg="#EAF1FB" color="#2E5B96">
Document review
</Pill>
);
case "PAYMENT":
return (
<Pill bg="#EAF1FB" color="#2E5B96">
Payment window
</Pill>
);
case "CLOSED":
return (
<Pill bg="#F1F5F9" color={MUTED}>
Closed
</Pill>
);
}
return (
<Pill bg="#F1F5F9" color={MUTED}>

View File

@@ -18,7 +18,8 @@ import {
ChevronRight,
Clock,
} from "lucide-react";
import { CountdownTimer } from "@edr/ui-common";
import { CountdownTimer, bookingWindowUiState } from "@edr/ui-common";
import type { BookingWindowUiKind } from "@edr/ui-common";
import type { MyBookingWindow } from "@/services/bookings.service";
import {
@@ -90,50 +91,29 @@ function windowLabel(w: MyBookingWindow): string {
}
/**
* The countdown for whichever phase the window is currently in, mirroring the
* home dashboard's Booking Windows card. `expiredText` names the NEXT step so a
* deadline that lapses between refetches announces what comes next rather than
* the bare "Expired".
* The countdown for the window's UI state, mirroring the home dashboard's
* Booking Windows card. Derived from the SAME state as the badge
* (`bookingWindowUiState`) so they can never contradict — a full train shows
* no ticking countdown. `expiredText` names the NEXT step so a deadline that
* lapses between refetches announces what comes next rather than the bare
* "Expired".
*/
const COUNTDOWN_TEXT: Partial<
Record<BookingWindowUiKind, { label: string; expiredText: string }>
> = {
PRE_WINDOW: { label: "Booking opens in", expiredText: "Booking opening now…" },
OPEN: { label: "Window closes in", expiredText: "Document review starting…" },
DOC_REVIEW: { label: "Document review ends in", expiredText: "Payment starting…" },
PAYMENT: { label: "Payment due in", expiredText: "Payment window closing…" },
};
function phaseCountdown(
w: MyBookingWindow,
): { label: string; deadline: string; expiredText: string } | null {
switch (w.windowPhase) {
case "PRE_WINDOW":
return w.windowOpensAt
? {
label: "Booking opens in",
deadline: w.windowOpensAt,
expiredText: "Booking opening now…",
}
: null;
case "OPEN":
return w.windowClosesAt
? {
label: "Window closes in",
deadline: w.windowClosesAt,
expiredText: "Document review starting…",
}
: null;
case "DOC_REVIEW":
return w.docReviewEndsAt
? {
label: "Document review ends in",
deadline: w.docReviewEndsAt,
expiredText: "Payment starting…",
}
: null;
case "PAYMENT":
return w.paymentPhaseEndsAt
? {
label: "Payment due in",
deadline: w.paymentPhaseEndsAt,
expiredText: "Payment window closing…",
}
: null;
default:
return null;
}
const state = bookingWindowUiState(w);
const text = COUNTDOWN_TEXT[state.kind];
if (!state.countdownTo || !text) return null;
return { ...text, deadline: state.countdownTo };
}
/**
@@ -148,9 +128,21 @@ function isPast(w: MyBookingWindow): boolean {
return w.windowPhase === "DONE" || w.windowPhase === "CLOSED_FOR_DAY";
}
/** Badge label + Mantine color per UI state — same state the countdown uses. */
const KIND_BADGE: Record<BookingWindowUiKind, { label: string; color: string }> = {
OPEN: { label: "Open now", color: "edr-green" },
FULL: { label: "Train full", color: "red" },
PRE_WINDOW: { label: "Opens soon", color: "yellow" },
DOC_REVIEW: { label: "Document review", color: "gray" },
PAYMENT: { label: "Payment due", color: "gray" },
CLOSED: { label: "Closed", color: "gray" },
};
function WindowCard({ w }: { w: MyBookingWindow }) {
const cd = phaseCountdown(w);
const open = w.isOpenNow;
const state = bookingWindowUiState(w);
const badge = KIND_BADGE[state.kind];
const open = state.isBookable;
const isImport = w.direction === "IMPORT";
return (
@@ -183,13 +175,11 @@ function WindowCard({ w }: { w: MyBookingWindow }) {
)}
<Badge
variant={open ? "filled" : "light"}
color={open ? "edr-green" : "gray"}
color={badge.color}
radius="sm"
size="sm"
>
{open
? "Open now"
: windowPhaseLabel(w.windowPhase ?? w.bookingWindowStatus)}
{badge.label}
</Badge>
</Group>