mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 22:18:12 +00:00
fix schule issue and contianer type issue
This commit is contained in:
@@ -197,20 +197,20 @@ const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
|
||||
FREIGHT_PERMS.contracts.clearanceEtActions,
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Shipment Requests",
|
||||
href: "/dashboard/shipment-requests",
|
||||
icon: <Send />,
|
||||
permission: FREIGHT_PERMS.contracts.createBooking,
|
||||
},
|
||||
// {
|
||||
// label: "Shipment Requests",
|
||||
// href: "/dashboard/shipment-requests",
|
||||
// icon: <Send />,
|
||||
// permission: FREIGHT_PERMS.contracts.createBooking,
|
||||
// },
|
||||
// Operations Path A queue: per-booking self-clearance review for
|
||||
// GENERAL non-customs booking instances (and legacy self-clear bookings).
|
||||
{
|
||||
label: "Self-Clearance Review",
|
||||
href: "/dashboard/contracts/ops-clearance",
|
||||
icon: <ShieldCheck />,
|
||||
permission: FREIGHT_PERMS.contracts.opsClearanceReview,
|
||||
},
|
||||
// {
|
||||
// label: "Self-Clearance Review",
|
||||
// href: "/dashboard/contracts/ops-clearance",
|
||||
// icon: <ShieldCheck />,
|
||||
// permission: FREIGHT_PERMS.contracts.opsClearanceReview,
|
||||
// },
|
||||
{
|
||||
label: "GL Djibouti Clearance",
|
||||
href: "/dashboard/gl-djibouti/clearance",
|
||||
|
||||
@@ -65,7 +65,10 @@ export default function AdjustConsistModal({
|
||||
}
|
||||
}, [opened]);
|
||||
|
||||
// Live projection: gross = cargo + tare of (consist − trims + adds).
|
||||
// Live projection: gross = cargo + tare of (consist − trims + adds), plus
|
||||
// the schedule's wagon-slot picture — the consist IS the booking capacity
|
||||
// (weight/length only bind while assembling the consist), so trims/adds
|
||||
// move the FULL line in real time.
|
||||
const projection = useMemo(() => {
|
||||
if (!data) return null;
|
||||
const removed = new Set(removeIds);
|
||||
@@ -79,8 +82,11 @@ export default function AdjustConsistModal({
|
||||
const tare = keptTare + addedWagons.reduce((s, w) => s + tareOf(w), 0);
|
||||
const length = keptLength + addedWagons.reduce((s, w) => s + lengthOf(w), 0);
|
||||
const gross = round2(data.totals.cargoTons + tare);
|
||||
const wagonCount = data.totals.wagonCount - removeIds.length + addIds.length;
|
||||
const cap = data.scheduleCapacity;
|
||||
const freeSlots = cap ? wagonCount - cap.allocatedWagons : null;
|
||||
return {
|
||||
wagonCount: data.totals.wagonCount - removeIds.length + addIds.length,
|
||||
wagonCount,
|
||||
tare: round2(tare),
|
||||
gross,
|
||||
length: round2(length),
|
||||
@@ -93,16 +99,33 @@ export default function AdjustConsistModal({
|
||||
overWeight: data.limits.pullCapTons > 0 && gross > data.limits.pullCapTons,
|
||||
overLength:
|
||||
data.limits.lengthCapMeters > 0 && length > data.limits.lengthCapMeters,
|
||||
slots:
|
||||
cap && freeSlots != null
|
||||
? {
|
||||
allocated: cap.allocatedWagons,
|
||||
free: freeSlots,
|
||||
pct:
|
||||
wagonCount > 0
|
||||
? Math.round((cap.allocatedWagons / wagonCount) * 100)
|
||||
: null,
|
||||
isFullNow: cap.bookingWindowStatus === "FULL",
|
||||
willBeFull: freeSlots <= 0,
|
||||
overAllocated: freeSlots < 0,
|
||||
willReopen: cap.bookingWindowStatus === "FULL" && freeSlots > 0,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}, [data, removeIds, addIds]);
|
||||
|
||||
const hasChanges = removeIds.length > 0 || addIds.length > 0;
|
||||
|
||||
const toggle = (setter: typeof setRemoveIds) => (id: string, checked: boolean) =>
|
||||
setter((prev) => (checked ? [...prev, id] : prev.filter((x) => x !== id)));
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!removeIds.length && !addIds.length) return;
|
||||
try {
|
||||
await adjust.mutateAsync({
|
||||
const result = await adjust.mutateAsync({
|
||||
scheduleId,
|
||||
payload: {
|
||||
...(addIds.length ? { addWagonIds: addIds } : {}),
|
||||
@@ -114,6 +137,18 @@ export default function AdjustConsistModal({
|
||||
removeIds.length && addIds.length ? ", " : ""
|
||||
}${addIds.length ? `${addIds.length} added` : ""}`,
|
||||
});
|
||||
// Schedule-impact warnings from the API: window reopened / now FULL /
|
||||
// consist trimmed below what bookings already hold.
|
||||
for (const warning of result.warnings ?? []) {
|
||||
toast({
|
||||
title: "Schedule capacity",
|
||||
description: warning,
|
||||
duration: 8000,
|
||||
...(warning.includes("over capacity")
|
||||
? { variant: "destructive" as const }
|
||||
: {}),
|
||||
});
|
||||
}
|
||||
setRemoveIds([]);
|
||||
setAddIds([]);
|
||||
} catch (err) {
|
||||
@@ -169,8 +204,57 @@ export default function AdjustConsistModal({
|
||||
over={projection?.overLength ?? false}
|
||||
/>
|
||||
</Grid.Col>
|
||||
{projection?.slots ? (
|
||||
<Grid.Col span={12}>
|
||||
<LimitGauge
|
||||
label="Booking slots — the consist is the schedule's capacity"
|
||||
detail={`${projection.slots.allocated} of ${projection.wagonCount} projected wagon slot(s) held by bookings${
|
||||
projection.slots.free > 0
|
||||
? ` — ${projection.slots.free} free`
|
||||
: projection.slots.free === 0
|
||||
? " — none free (FULL)"
|
||||
: ""
|
||||
}`}
|
||||
pct={projection.slots.pct}
|
||||
over={projection.slots.overAllocated}
|
||||
/>
|
||||
</Grid.Col>
|
||||
) : null}
|
||||
</Grid>
|
||||
|
||||
{projection?.slots?.isFullNow && !hasChanges ? (
|
||||
<Alert color="yellow" icon={<AlertTriangle size={16} />}>
|
||||
This schedule is FULL — all {projection.wagonCount} wagon slots are
|
||||
taken. You can still edit the train: coupling wagons adds capacity
|
||||
and reopens booking; trimming free wagons keeps it FULL.
|
||||
</Alert>
|
||||
) : null}
|
||||
{hasChanges && projection?.slots?.overAllocated ? (
|
||||
<Alert color="red" icon={<AlertTriangle size={16} />}>
|
||||
This change leaves {-projection.slots.free} booked wagon(s) without
|
||||
a slot — bookings already hold {projection.slots.allocated} of the{" "}
|
||||
{projection.wagonCount} remaining. You can apply it, but couple
|
||||
wagons back or free bookings before departure.
|
||||
</Alert>
|
||||
) : null}
|
||||
{hasChanges &&
|
||||
projection?.slots &&
|
||||
!projection.slots.overAllocated &&
|
||||
projection.slots.willBeFull &&
|
||||
!projection.slots.isFullNow ? (
|
||||
<Alert color="yellow" icon={<AlertTriangle size={16} />}>
|
||||
This change takes the last free wagon slot — the schedule becomes
|
||||
FULL and stops accepting bookings.
|
||||
</Alert>
|
||||
) : null}
|
||||
{hasChanges && projection?.slots?.willReopen ? (
|
||||
<Alert color="blue" icon={<AlertTriangle size={16} />}>
|
||||
This schedule is currently FULL — applying frees{" "}
|
||||
{projection.slots.free} wagon slot(s) and reopens its booking
|
||||
window.
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<Grid gap="md">
|
||||
<Grid.Col span={{ base: 12, md: 6 }}>
|
||||
<Stack gap="xs">
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { ContainerUnitRow } from '@/types/trainScheduling';
|
||||
function makeUnits(containerType: string, sizeFt: number, quantity: number): ContainerUnitRow[] {
|
||||
const units: ContainerUnitRow[] = [];
|
||||
const containersPerWagon = sizeFt >= 40 ? 1 : 2;
|
||||
const wagonsPerUnit = sizeFt >= 40 ? 1 : 0.5;
|
||||
|
||||
for (let i = 0; i < quantity; i++) {
|
||||
units.push({
|
||||
@@ -18,7 +17,6 @@ function makeUnits(containerType: string, sizeFt: number, quantity: number): Con
|
||||
label: `${containerType} ${i + 1}/${quantity}`,
|
||||
grossWeightTons: 25,
|
||||
sizeFt,
|
||||
wagonsPerUnit,
|
||||
containersPerWagon,
|
||||
teuSlots: sizeFt >= 40 ? 2 : 1,
|
||||
});
|
||||
|
||||
@@ -61,7 +61,6 @@ interface RefContainerType {
|
||||
name: string;
|
||||
code: string;
|
||||
is_reefer?: boolean;
|
||||
wagons_per_unit?: number;
|
||||
}
|
||||
interface RefContainerGroup {
|
||||
size: string;
|
||||
|
||||
@@ -384,7 +384,7 @@ export const RULE_ENGINE_RESOURCES: RuleEngineResourceConfig[] = [
|
||||
label: "Max wagon count",
|
||||
type: "number",
|
||||
required: true,
|
||||
description: "Ceiling per type: WAGON 50 · CURRENCY 35 · CUSTOMS 15",
|
||||
description: "No upper limit — must be at least the min wagon count",
|
||||
},
|
||||
{ name: "scorePoints", label: "Score points", type: "number", required: true },
|
||||
{ name: "isActive", label: "Active", type: "boolean" },
|
||||
|
||||
@@ -1,20 +1,14 @@
|
||||
/**
|
||||
* Client mirror of the backend's contiguous-range rules for priority configs
|
||||
* (see PriorityConfigsService.assertNoRangeCollision): ranges per type — per
|
||||
* currency for CURRENCY — run 1..cap with no gaps and no overlaps, so the next
|
||||
* range always starts at the lowest uncovered wagon count. The backend
|
||||
* re-validates on submit AND on approval; this only drives the form prefill.
|
||||
* currency for CURRENCY — run from 1 with no gaps and no overlaps, so the next
|
||||
* range always starts at the lowest uncovered wagon count. There is no upper
|
||||
* ceiling. The backend re-validates on submit AND on approval; this only
|
||||
* drives the form prefill.
|
||||
*/
|
||||
|
||||
export type PriorityRuleType = "WAGON" | "CURRENCY" | "CUSTOMS";
|
||||
|
||||
/** Hard ceiling of each type's chain — keep in sync with the API's RANGE_CAPS. */
|
||||
export const PRIORITY_RANGE_CAPS: Record<PriorityRuleType, number> = {
|
||||
WAGON: 50,
|
||||
CURRENCY: 35,
|
||||
CUSTOMS: 15,
|
||||
};
|
||||
|
||||
export interface PriorityRangeRule {
|
||||
id?: unknown;
|
||||
type?: unknown;
|
||||
@@ -23,10 +17,13 @@ export interface PriorityRangeRule {
|
||||
maxWagonCount?: unknown;
|
||||
}
|
||||
|
||||
const PRIORITY_RULE_TYPES: PriorityRuleType[] = ["WAGON", "CURRENCY", "CUSTOMS"];
|
||||
|
||||
/**
|
||||
* Where the next range for `type` (+`currency`) must start, excluding
|
||||
* `excludeId` (the rule being edited). Null when the chain already covers
|
||||
* 1..cap — no further rule fits.
|
||||
* `excludeId` (the rule being edited). Null only when `type` is not yet a
|
||||
* known priority rule type — the chain itself is unbounded, so a next start
|
||||
* always exists.
|
||||
*/
|
||||
export function nextPriorityRangeStart(
|
||||
rules: PriorityRangeRule[],
|
||||
@@ -34,8 +31,7 @@ export function nextPriorityRangeStart(
|
||||
currency: string | null | undefined,
|
||||
excludeId?: string,
|
||||
): number | null {
|
||||
const cap = PRIORITY_RANGE_CAPS[type as PriorityRuleType];
|
||||
if (!cap) return null;
|
||||
if (!PRIORITY_RULE_TYPES.includes(type as PriorityRuleType)) return null;
|
||||
|
||||
const scoped = rules
|
||||
.filter(
|
||||
@@ -56,5 +52,5 @@ export function nextPriorityRangeStart(
|
||||
if (r.min > next) break; // gap before this rule — fill it first
|
||||
next = Math.max(next, r.max + 1);
|
||||
}
|
||||
return next > cap ? null : next;
|
||||
return next;
|
||||
}
|
||||
|
||||
@@ -185,6 +185,7 @@ import { trainService, type Train } from "./trains.service";
|
||||
import {
|
||||
trainBuilderService,
|
||||
type AdjustConsistPayload,
|
||||
type AdjustConsistResult,
|
||||
type AvailableTrain,
|
||||
type BuildTrainPayload,
|
||||
type BuiltTrainListFilters,
|
||||
@@ -338,7 +339,7 @@ export const api = {
|
||||
|
||||
adjustConsist: endpoint<
|
||||
{ scheduleId: string; payload: AdjustConsistPayload },
|
||||
ScheduleConsist
|
||||
AdjustConsistResult
|
||||
>(
|
||||
"train-scheduling",
|
||||
"adjust-consist",
|
||||
|
||||
@@ -235,6 +235,18 @@ export interface ScheduleConsist {
|
||||
occurredAt: string;
|
||||
}>;
|
||||
editable: boolean;
|
||||
/**
|
||||
* Wagon-slot picture of the schedule: the consist IS the booking capacity
|
||||
* (weight/length only bind while building the consist), so the dialog can
|
||||
* project FULL / reopen / over-allocation live. Null on legacy schedules.
|
||||
*/
|
||||
scheduleCapacity: {
|
||||
maxWagons: number;
|
||||
allocatedWagons: number;
|
||||
remainingSlots: number;
|
||||
overAllocatedBy: number;
|
||||
bookingWindowStatus: string | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface AdjustConsistPayload {
|
||||
@@ -242,6 +254,9 @@ export interface AdjustConsistPayload {
|
||||
removeWagonIds?: string[];
|
||||
}
|
||||
|
||||
/** Adjust response: fresh consist + schedule-impact warnings to surface. */
|
||||
export type AdjustConsistResult = ScheduleConsist & { warnings: string[] };
|
||||
|
||||
export const trainBuilderService = {
|
||||
list: (filters: BuiltTrainListFilters = {}) =>
|
||||
apiClient.get<BuiltTrainListResponse>(`${BASE}${toQuery(filters)}`),
|
||||
@@ -275,7 +290,7 @@ export const trainBuilderService = {
|
||||
apiClient.get<ScheduleConsist>(`/train-scheduling/schedules/${scheduleId}/consist`),
|
||||
/** Permanently trim/add wagons on the schedule's built train. */
|
||||
adjustConsist: (scheduleId: string, payload: AdjustConsistPayload) =>
|
||||
apiClient.post<ScheduleConsist>(
|
||||
apiClient.post<AdjustConsistResult>(
|
||||
`/train-scheduling/schedules/${scheduleId}/adjust-consist`,
|
||||
payload,
|
||||
),
|
||||
|
||||
@@ -72,7 +72,6 @@ export interface ContainerUnitRow {
|
||||
label: string;
|
||||
grossWeightTons: number;
|
||||
sizeFt?: number;
|
||||
wagonsPerUnit?: number;
|
||||
containersPerWagon?: number;
|
||||
teuSlots?: number;
|
||||
containerNumber?: string | null;
|
||||
|
||||
Reference in New Issue
Block a user