mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 15:18:11 +00:00
Merge pull request #1006 from Tria-plc/freight_feature/usermanagement
Add ContractCourtBadge component and integrate into contract pages
This commit is contained in:
@@ -99,16 +99,20 @@ export function ContractMilestonesTimeline({
|
||||
});
|
||||
}
|
||||
|
||||
// Every acted approval step, not just hazardous ones — this is the one
|
||||
// place the approval-time record shows up in the page's main content
|
||||
// (the sidebar's ContractApprovalStepsCard has the same times, but only
|
||||
// there, and only while the chain is still actionable).
|
||||
for (const step of contract.approvalSteps ?? []) {
|
||||
if (!(step.requiredRole in HAZARDOUS_APPROVAL_ROLE_PERMISSION)) continue;
|
||||
if (!step.actedAt) continue;
|
||||
const hazard = step.requiredRole in HAZARDOUS_APPROVAL_ROLE_PERMISSION;
|
||||
items.push({
|
||||
key: `hazard-${step.id}`,
|
||||
key: `step-${step.id}`,
|
||||
at: step.actedAt,
|
||||
title: CONTRACT_APPROVAL_ROLE_LABELS[step.requiredRole] ?? step.requiredRole,
|
||||
detail: step.status === "REJECTED" ? "Rejected" : "Approved",
|
||||
color: step.status === "REJECTED" ? "red" : "orange",
|
||||
icon: Flame,
|
||||
title: `${CONTRACT_APPROVAL_ROLE_LABELS[step.requiredRole] ?? step.requiredRole} ${step.status === "REJECTED" ? "rejected" : "approved"}`,
|
||||
detail: step.note ?? undefined,
|
||||
color: step.status === "REJECTED" ? "red" : hazard ? "orange" : "edr-green",
|
||||
icon: hazard ? Flame : ShieldCheck,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Badge, Group } from "@mantine/core";
|
||||
import { Repeat } from "lucide-react";
|
||||
import { Building2, Repeat, UserRound } from "lucide-react";
|
||||
|
||||
import {
|
||||
CONTRACT_STATUS_COLOR,
|
||||
CONTRACT_STATUS_STYLES,
|
||||
contractCourt,
|
||||
} from "@/features/contracts/contract-status.config";
|
||||
|
||||
interface ContractStatusBadgeProps {
|
||||
@@ -69,3 +70,42 @@ export function ContractStatusBadge({
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
/** Whose court the contract sits in: customer, EDR, or nobody ("—"). */
|
||||
export function ContractCourtBadge({ status }: { status: string }) {
|
||||
const court = contractCourt(status);
|
||||
if (!court) {
|
||||
return (
|
||||
<span className="text-sm text-muted-foreground" title="No party is awaited">
|
||||
—
|
||||
</span>
|
||||
);
|
||||
}
|
||||
const isCustomer = court === "customer";
|
||||
return (
|
||||
<Badge
|
||||
color={isCustomer ? "orange" : "edr-green"}
|
||||
variant="light"
|
||||
size="sm"
|
||||
radius="md"
|
||||
tt="uppercase"
|
||||
fw={600}
|
||||
leftSection={
|
||||
isCustomer ? <UserRound size={12} /> : <Building2 size={12} />
|
||||
}
|
||||
title={
|
||||
isCustomer
|
||||
? "Waiting on the customer to act"
|
||||
: "Waiting on EDR staff to act"
|
||||
}
|
||||
style={{
|
||||
fontSize: "0.7rem",
|
||||
letterSpacing: "0.05em",
|
||||
display: "inline-flex",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{isCustomer ? "With customer" : "With EDR"}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -34,12 +34,13 @@ import type {
|
||||
} from "@/types/trainScheduling";
|
||||
import { WindowPhasePill } from "./batchVisuals";
|
||||
import { ForecastPanel } from "./ForecastPanel";
|
||||
import { forecastIsLive } from "./batchForecast";
|
||||
import { forecastIsLive, rankBookings } from "./batchForecast";
|
||||
|
||||
/**
|
||||
* Priority Tracking tab — live, glanceable ranking of every booking on this
|
||||
* schedule in the exact order the batch engine boards them (government first,
|
||||
* then rule-engine priority score, then oldest). Bookings above the train's
|
||||
* then window cycle — bookings compete only within their own cycle — then
|
||||
* rule-engine priority score, then oldest). Bookings above the train's
|
||||
* wagon-capacity line render as "selected" (green), below it as the waiting
|
||||
* list; during the PAYMENT phase selected bookings show a live pay-window
|
||||
* countdown. Purely presentational — data comes from the batch-board detail
|
||||
@@ -286,19 +287,11 @@ export const PriorityTrackingTab = memo(function PriorityTrackingTab({
|
||||
);
|
||||
const showForecast = forecastAvailable && view === "forecast";
|
||||
|
||||
// Rank exactly as the batch engine does: government first, then priority score
|
||||
// desc, then oldest (fullyExecutedAt / selectedForBatchAt as the tiebreak the
|
||||
// backend uses). The board already returns them in this order, but re-sort
|
||||
// defensively so the tab is correct even if the source order ever changes.
|
||||
const ranked = useMemo(() => {
|
||||
const time = (b: BatchBoardBookingDetail) =>
|
||||
b.fullyExecutedAt ? new Date(b.fullyExecutedAt).getTime() : Number.MAX_SAFE_INTEGER;
|
||||
return [...bookings].sort((a, b) => {
|
||||
if (a.isGovernment !== b.isGovernment) return a.isGovernment ? -1 : 1;
|
||||
if (b.priorityScore !== a.priorityScore) return b.priorityScore - a.priorityScore;
|
||||
return time(a) - time(b);
|
||||
});
|
||||
}, [bookings]);
|
||||
// Rank exactly as the batch engine does: government first, then window cycle
|
||||
// (bookings only compete within the cycle they arrived in — an earlier cycle
|
||||
// boards before a later one regardless of score), then priority desc, then
|
||||
// oldest. Shared with the forecast sim so both views agree.
|
||||
const ranked = useMemo(() => rankBookings(bookings), [bookings]);
|
||||
|
||||
const scoreMax = useMemo(() => maxScore(ranked), [ranked]);
|
||||
// Wagon-slot cap from the board DTO (derived from train length and the
|
||||
@@ -395,7 +388,8 @@ export const PriorityTrackingTab = memo(function PriorityTrackingTab({
|
||||
<Stack gap={2}>
|
||||
<Text fw={700}>Priority ranking</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
Government first, then rule-engine score, then earliest booked.
|
||||
Government first, then booking window (earlier cycles board
|
||||
first), then rule-engine score, then earliest booked.
|
||||
</Text>
|
||||
</Stack>
|
||||
</Group>
|
||||
|
||||
@@ -61,7 +61,12 @@ export interface ForecastResult {
|
||||
full: boolean;
|
||||
}
|
||||
|
||||
/** Engine rank order: government first, then priority desc, then oldest booked. */
|
||||
/**
|
||||
* Engine rank order: government first, then window cycle asc (bookings compete
|
||||
* only within the cycle they arrived in — earlier cycles board first no matter
|
||||
* the score; pending-contract rows sink last), then priority desc, then oldest
|
||||
* booked.
|
||||
*/
|
||||
export function rankBookings(
|
||||
bookings: BatchBoardBookingDetail[],
|
||||
): BatchBoardBookingDetail[] {
|
||||
@@ -69,8 +74,11 @@ export function rankBookings(
|
||||
b.fullyExecutedAt
|
||||
? new Date(b.fullyExecutedAt).getTime()
|
||||
: Number.MAX_SAFE_INTEGER;
|
||||
const cycle = (b: BatchBoardBookingDetail) =>
|
||||
b.windowCycleNo ?? Number.MAX_SAFE_INTEGER;
|
||||
return [...bookings].sort((a, b) => {
|
||||
if (a.isGovernment !== b.isGovernment) return a.isGovernment ? -1 : 1;
|
||||
if (cycle(a) !== cycle(b)) return cycle(a) - cycle(b);
|
||||
if (b.priorityScore !== a.priorityScore)
|
||||
return b.priorityScore - a.priorityScore;
|
||||
return time(a) - time(b);
|
||||
|
||||
Reference in New Issue
Block a user