mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 03:38:17 +00:00
train
This commit is contained in:
@@ -1,4 +1,11 @@
|
||||
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
|
||||
import {
|
||||
Fragment,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
ActionIcon,
|
||||
@@ -75,6 +82,8 @@ interface ClearanceRow {
|
||||
freightType: string;
|
||||
originLabel: string;
|
||||
destinationLabel: string;
|
||||
/** Full ordered corridor across the contract's route legs (origin → … → destination). */
|
||||
routeStops: string[];
|
||||
contractKind: string;
|
||||
serviceTypeName: string;
|
||||
customs: boolean;
|
||||
@@ -93,6 +102,25 @@ function yardLabel(
|
||||
return yard.label ?? yard.name ?? yard.code ?? fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Chain the contract's ordered route legs into one corridor of stops —
|
||||
* origin of the first leg, then each leg's destination (Djibouti → Adama →
|
||||
* Dire Dawa). A leg whose origin differs from the previous destination inserts
|
||||
* that stop too, so gapped route lists stay readable.
|
||||
*/
|
||||
function contractRouteStops(routes: Freight.IContractRoute[]): string[] {
|
||||
const stops: string[] = [];
|
||||
for (const r of routes) {
|
||||
const origin = yardLabel(r.originYard);
|
||||
const destination = yardLabel(r.destinationYard);
|
||||
if (stops.length === 0 || stops[stops.length - 1] !== origin) {
|
||||
stops.push(origin);
|
||||
}
|
||||
stops.push(destination);
|
||||
}
|
||||
return stops;
|
||||
}
|
||||
|
||||
function toClearanceRow(contract: Freight.IContract): ClearanceRow {
|
||||
const routes = [...(contract.routes ?? [])].sort(
|
||||
(a, b) => a.sortOrder - b.sortOrder,
|
||||
@@ -109,6 +137,7 @@ function toClearanceRow(contract: Freight.IContract): ClearanceRow {
|
||||
freightType: contract.freightType ?? "—",
|
||||
originLabel: yardLabel(first?.originYard),
|
||||
destinationLabel: yardLabel(last?.destinationYard),
|
||||
routeStops: contractRouteStops(routes),
|
||||
contractKind: contract.contractKind,
|
||||
serviceTypeName: contract.serviceType?.serviceName ?? "—",
|
||||
customs:
|
||||
@@ -412,14 +441,23 @@ export default function ContractClearanceListPage() {
|
||||
const r = row.original;
|
||||
return (
|
||||
<Stack gap={4} py={2}>
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<Text size="sm" fw={500} truncate maw={120}>
|
||||
{r.originLabel}
|
||||
</Text>
|
||||
<ArrowRight size={14} className="shrink-0 text-muted-foreground" />
|
||||
<Text size="sm" fw={500} truncate maw={120}>
|
||||
{r.destinationLabel}
|
||||
</Text>
|
||||
<Group gap={6} wrap="wrap">
|
||||
{(r.routeStops.length >= 2
|
||||
? r.routeStops
|
||||
: [r.originLabel, r.destinationLabel]
|
||||
).map((stop, i) => (
|
||||
<Fragment key={i}>
|
||||
{i > 0 ? (
|
||||
<ArrowRight
|
||||
size={14}
|
||||
className="shrink-0 text-muted-foreground"
|
||||
/>
|
||||
) : null}
|
||||
<Text size="sm" fw={500}>
|
||||
{stop}
|
||||
</Text>
|
||||
</Fragment>
|
||||
))}
|
||||
</Group>
|
||||
<Group gap={8} align="center">
|
||||
<DirectionIcon direction={r.tradeDirection} />
|
||||
@@ -638,6 +676,11 @@ export default function ContractClearanceListPage() {
|
||||
`/dashboard/contracts/${row.contractId}/bookings/${row.id}/complete`,
|
||||
)
|
||||
}
|
||||
onRebook={(row) =>
|
||||
navigate(
|
||||
`/dashboard/contracts/${row.contractId}/create-booking?copyFrom=${row.id}`,
|
||||
)
|
||||
}
|
||||
onViewContract={(contractId) =>
|
||||
navigate(`/dashboard/contracts/clearance/${contractId}`)
|
||||
}
|
||||
@@ -731,6 +774,7 @@ function ShipmentBookingsTable({
|
||||
canCreateBooking,
|
||||
onOpen,
|
||||
onCreateBooking,
|
||||
onRebook,
|
||||
onViewContract,
|
||||
}: {
|
||||
rows: ShipmentBookingRow[];
|
||||
@@ -739,6 +783,7 @@ function ShipmentBookingsTable({
|
||||
canCreateBooking: boolean;
|
||||
onOpen: (id: string) => void;
|
||||
onCreateBooking: (row: ShipmentBookingRow) => void;
|
||||
onRebook: (row: ShipmentBookingRow) => void;
|
||||
onViewContract: (contractId: string) => void;
|
||||
}) {
|
||||
// A bare initiated instance that has cleared but not yet been created by GL.
|
||||
@@ -748,6 +793,14 @@ function ShipmentBookingsTable({
|
||||
!r.bookingCreated &&
|
||||
r.status === "CLEARANCE_READY";
|
||||
|
||||
// A customs shipment whose booking lost its slot — GL rebooks it (customer
|
||||
// can't self-rebook a customs booking). Copies the expired booking's cargo.
|
||||
const isRebookable = (r: ShipmentBookingRow) =>
|
||||
canCreateBooking &&
|
||||
Boolean(r.contractId) &&
|
||||
r.customs &&
|
||||
r.status === "EXPIRED";
|
||||
|
||||
const columns = useMemo<ColumnDef<ShipmentBookingRow>[]>(
|
||||
() => [
|
||||
{
|
||||
@@ -877,6 +930,7 @@ function ShipmentBookingsTable({
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
const bookable = isBookable(r);
|
||||
const rebookable = isRebookable(r);
|
||||
return (
|
||||
<Group
|
||||
justify="flex-end"
|
||||
@@ -896,6 +950,17 @@ function ShipmentBookingsTable({
|
||||
Create booking
|
||||
</Button>
|
||||
) : null}
|
||||
{rebookable ? (
|
||||
<Button
|
||||
size="compact-sm"
|
||||
color="grape"
|
||||
radius="md"
|
||||
leftSection={<RefreshCw size={14} />}
|
||||
onClick={() => onRebook(r)}
|
||||
>
|
||||
Rebook
|
||||
</Button>
|
||||
) : null}
|
||||
<Menu shadow="md" radius="md" position="bottom-end" withinPortal>
|
||||
<Menu.Target>
|
||||
<ActionIcon
|
||||
@@ -919,6 +984,14 @@ function ShipmentBookingsTable({
|
||||
Create booking
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
{rebookable ? (
|
||||
<Menu.Item
|
||||
leftSection={<RefreshCw size={14} />}
|
||||
onClick={() => onRebook(r)}
|
||||
>
|
||||
Rebook (GL)
|
||||
</Menu.Item>
|
||||
) : null}
|
||||
{r.contractId ? (
|
||||
<Menu.Item
|
||||
leftSection={<ExternalLink size={14} />}
|
||||
|
||||
Reference in New Issue
Block a user