mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 02:28:18 +00:00
fix issues
This commit is contained in:
@@ -178,12 +178,12 @@ 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,
|
||||
},
|
||||
{
|
||||
label: "GL Djibouti Clearance",
|
||||
href: "/dashboard/gl-djibouti/clearance",
|
||||
|
||||
@@ -10,7 +10,10 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
const { toast } = useToast();
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [form, setForm] = useState<Partial<TrainSchedulingGlobalRules>>({});
|
||||
// Fields hold raw NumberInput values (number | string) while editing; coerced to Number on save.
|
||||
const [form, setForm] = useState<
|
||||
Partial<Record<keyof TrainSchedulingGlobalRules, number | string>>
|
||||
>({});
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
@@ -65,7 +68,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
description="Sum of all wagon lengths must not exceed this"
|
||||
value={form.maxTrainLengthMeters ?? ""}
|
||||
onChange={(value) =>
|
||||
setForm((current) => ({ ...current, maxTrainLengthMeters: Number(value) }))
|
||||
setForm((current) => ({ ...current, maxTrainLengthMeters: value }))
|
||||
}
|
||||
min={1}
|
||||
disabled={loading}
|
||||
@@ -75,7 +78,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
description="Total container and bulk cargo weight must not exceed this"
|
||||
value={form.maxTrainWeightTons ?? ""}
|
||||
onChange={(value) =>
|
||||
setForm((current) => ({ ...current, maxTrainWeightTons: Number(value) }))
|
||||
setForm((current) => ({ ...current, maxTrainWeightTons: value }))
|
||||
}
|
||||
min={1}
|
||||
disabled={loading}
|
||||
@@ -84,7 +87,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
label="Max wagons per train"
|
||||
value={form.maxWagonsPerTrain ?? ""}
|
||||
onChange={(value) =>
|
||||
setForm((current) => ({ ...current, maxWagonsPerTrain: Number(value) }))
|
||||
setForm((current) => ({ ...current, maxWagonsPerTrain: value }))
|
||||
}
|
||||
min={1}
|
||||
disabled={loading}
|
||||
@@ -96,7 +99,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
onChange={(value) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
max20ftContainerWeightTons: Number(value),
|
||||
max20ftContainerWeightTons: value,
|
||||
}))
|
||||
}
|
||||
min={0.001}
|
||||
@@ -109,7 +112,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
onChange={(value) =>
|
||||
setForm((current) => ({
|
||||
...current,
|
||||
max20ftPairWeightDiffTons: Number(value),
|
||||
max20ftPairWeightDiffTons: value,
|
||||
}))
|
||||
}
|
||||
min={0}
|
||||
@@ -129,7 +132,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
description="The single booking day opens this many days before departure"
|
||||
value={form.importWindowLeadDays ?? ""}
|
||||
onChange={(value) =>
|
||||
setForm((current) => ({ ...current, importWindowLeadDays: Number(value) }))
|
||||
setForm((current) => ({ ...current, importWindowLeadDays: value }))
|
||||
}
|
||||
min={0}
|
||||
disabled={loading}
|
||||
@@ -139,7 +142,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
description="Export bookings are accepted first-come-first-serve starting this many hours before departure"
|
||||
value={form.exportBookingLeadHours ?? ""}
|
||||
onChange={(value) =>
|
||||
setForm((current) => ({ ...current, exportBookingLeadHours: Number(value) }))
|
||||
setForm((current) => ({ ...current, exportBookingLeadHours: value }))
|
||||
}
|
||||
min={1}
|
||||
disabled={loading}
|
||||
@@ -149,7 +152,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
description="Local hour the import window opens on its booking day (e.g. 8 = 08:00)"
|
||||
value={form.windowOpenHour ?? ""}
|
||||
onChange={(value) =>
|
||||
setForm((current) => ({ ...current, windowOpenHour: Number(value) }))
|
||||
setForm((current) => ({ ...current, windowOpenHour: value }))
|
||||
}
|
||||
min={0}
|
||||
max={23}
|
||||
@@ -159,7 +162,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
label="Window duration (hours)"
|
||||
value={form.windowDurationHours ?? ""}
|
||||
onChange={(value) =>
|
||||
setForm((current) => ({ ...current, windowDurationHours: Number(value) }))
|
||||
setForm((current) => ({ ...current, windowDurationHours: value }))
|
||||
}
|
||||
min={0.25}
|
||||
max={12}
|
||||
@@ -171,7 +174,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
description="Max staff time to accept booking documents after the window closes"
|
||||
value={form.docReviewMinutes ?? ""}
|
||||
onChange={(value) =>
|
||||
setForm((current) => ({ ...current, docReviewMinutes: Number(value) }))
|
||||
setForm((current) => ({ ...current, docReviewMinutes: value }))
|
||||
}
|
||||
min={0}
|
||||
disabled={loading}
|
||||
@@ -181,7 +184,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
description="Time a selected customer has to pay before the slot expires"
|
||||
value={form.paymentWindowMinutes ?? ""}
|
||||
onChange={(value) =>
|
||||
setForm((current) => ({ ...current, paymentWindowMinutes: Number(value) }))
|
||||
setForm((current) => ({ ...current, paymentWindowMinutes: value }))
|
||||
}
|
||||
min={1}
|
||||
disabled={loading}
|
||||
@@ -191,7 +194,7 @@ export default function TrainSchedulingGlobalRulesPage() {
|
||||
description="Delay after window close before reopening when the train is not full (90 = 11:00 close → 12:30 reopen)"
|
||||
value={form.reopenDelayMinutes ?? ""}
|
||||
onChange={(value) =>
|
||||
setForm((current) => ({ ...current, reopenDelayMinutes: Number(value) }))
|
||||
setForm((current) => ({ ...current, reopenDelayMinutes: value }))
|
||||
}
|
||||
min={1}
|
||||
disabled={loading}
|
||||
|
||||
@@ -147,6 +147,31 @@ async function searchPlaces(
|
||||
return found;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the address label for a picked place.
|
||||
*
|
||||
* For an establishment / POI (e.g. "Bole Medhanialem") Google's
|
||||
* `formatted_address` is the *postal* address, which for many Ethiopian places
|
||||
* collapses to just the city ("Addis Ababa, Ethiopia") — so taking it verbatim
|
||||
* silently replaces the specific place the user picked with a broad city. The
|
||||
* place `name` carries the specific label, so we lead with it and only append
|
||||
* the formatted address for context when it doesn't already contain the name.
|
||||
* Falls back to the prediction's own description (what the user saw and clicked).
|
||||
*/
|
||||
function placeDisplayName(
|
||||
place: google.maps.places.PlaceResult | null,
|
||||
prediction: PlacePrediction,
|
||||
): string {
|
||||
const name = place?.name?.trim();
|
||||
const formatted = place?.formatted_address?.trim();
|
||||
if (name && formatted) {
|
||||
return formatted.toLowerCase().includes(name.toLowerCase())
|
||||
? formatted
|
||||
: `${name}, ${formatted}`;
|
||||
}
|
||||
return name || formatted || prediction.displayName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a picked prediction to its coordinates via Place Details. Runs once
|
||||
* per selection (closes the Autocomplete session), so billing stays on the
|
||||
@@ -174,10 +199,7 @@ async function resolvePrediction(
|
||||
return;
|
||||
}
|
||||
resolve({
|
||||
displayName:
|
||||
place?.formatted_address ||
|
||||
place?.name ||
|
||||
prediction.displayName,
|
||||
displayName: placeDisplayName(place, prediction),
|
||||
lat: loc.lat(),
|
||||
lng: loc.lng(),
|
||||
});
|
||||
|
||||
@@ -371,16 +371,41 @@ function PriceConfirmModal({
|
||||
onConfirm: () => void;
|
||||
onReject: () => void;
|
||||
}) {
|
||||
const total = useMemo(
|
||||
const baseTotal = useMemo(
|
||||
() => (values ? computeShipmentTotal(contract, values) : null),
|
||||
[contract, values],
|
||||
);
|
||||
|
||||
const overweightLines = validation?.overweightLines ?? [];
|
||||
const overweightSurchargeAmount = validation?.overweightSurchargeAmount ?? 0;
|
||||
const pairingErrors = validation?.pairingErrors ?? [];
|
||||
const hasPairingBlock = pairingErrors.length > 0;
|
||||
const confirmDisabled = loading || validationLoading || hasPairingBlock;
|
||||
|
||||
// The contract's frozen unit rates (computeShipmentTotal) don't carry an
|
||||
// overweight line — that surcharge only exists in the live rule engine. Fold
|
||||
// the real amount from validateShipment into the displayed total so the
|
||||
// customer sees the actual charge the overweight warning refers to, not just
|
||||
// the warning text.
|
||||
const total = useMemo(() => {
|
||||
if (!baseTotal) return null;
|
||||
if (!(overweightSurchargeAmount > 0)) return baseTotal;
|
||||
return {
|
||||
...baseTotal,
|
||||
lines: [
|
||||
...baseTotal.lines,
|
||||
{
|
||||
label: "Overweight surcharge",
|
||||
unitPrice: overweightSurchargeAmount,
|
||||
unit: "flat" as const,
|
||||
quantity: 1,
|
||||
amount: overweightSurchargeAmount,
|
||||
},
|
||||
],
|
||||
total: baseTotal.total + overweightSurchargeAmount,
|
||||
};
|
||||
}, [baseTotal, overweightSurchargeAmount]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
opened={Boolean(values)}
|
||||
@@ -456,8 +481,11 @@ function PriceConfirmModal({
|
||||
</Text>
|
||||
))}
|
||||
<Text fz="xs" c="#9A5B00" mt={2}>
|
||||
An overweight surcharge applies. You can still submit, or go
|
||||
back and adjust weights.
|
||||
{overweightSurchargeAmount > 0
|
||||
? `An overweight surcharge of ${overweightSurchargeAmount.toLocaleString()} ${
|
||||
validation?.currency ?? total?.currency ?? ""
|
||||
} applies (included in the total below). You can still submit, or go back and adjust weights.`
|
||||
: "An overweight surcharge applies. You can still submit, or go back and adjust weights."}
|
||||
</Text>
|
||||
</Stack>
|
||||
</Alert>
|
||||
|
||||
@@ -46,9 +46,13 @@ export interface OverweightLine {
|
||||
* `overweightLines` are WARNINGS only (an overweight surcharge applies — the
|
||||
* customer may still submit); `pairingErrors` are HARD BLOCKS (20ft containers
|
||||
* that cannot be balanced onto wagons) and must prevent booking.
|
||||
* `overweightSurchargeAmount` is the real overweight charge (same rate the
|
||||
* booking is billed at on submit) so the confirm-modal total can include it.
|
||||
*/
|
||||
export interface ShipmentValidation {
|
||||
overweightLines: OverweightLine[];
|
||||
overweightSurchargeAmount: number;
|
||||
currency: string | null;
|
||||
pairingErrors: string[];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user