mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Merge branch 'dev' into eims-bulk-register
This commit is contained in:
@@ -43,9 +43,10 @@ import { BookingClearanceChargeService } from './booking-clearance-charge.servic
|
||||
import { BookingPayablesService } from './booking-payables.service';
|
||||
import { ClearanceEventService } from './clearance-event.service';
|
||||
import {
|
||||
BillClearanceChargeDto,
|
||||
|
||||
RejectClearanceChargeDto,
|
||||
} from './dto/clearance-charge.dto';
|
||||
import { BillClearanceChargeDto } from './dto/clearance-charge.dto';
|
||||
import { AdditionalChargeService } from './additional-charge.service';
|
||||
import { CancelAdditionalChargeDto, CreateAdditionalChargeDto } from './dto/additional-charge.dto';
|
||||
import { BookingContractService } from './booking-contract.service';
|
||||
|
||||
@@ -18,6 +18,8 @@ interface TrainConsistViewProps {
|
||||
scheduleDetail: TrainScheduleDetail;
|
||||
scheduleId: string;
|
||||
maxWagons: number;
|
||||
/** Hide the consist-wide Wagons stat tile (dispatch shows leg capacity instead). */
|
||||
showWagonStat?: boolean;
|
||||
/** Booking id selected in the side panel — highlights its wagons in the consist. */
|
||||
highlightBookingId?: string | null;
|
||||
}
|
||||
@@ -45,6 +47,7 @@ export const TrainConsistView = ({
|
||||
scheduleDetail,
|
||||
scheduleId,
|
||||
maxWagons,
|
||||
showWagonStat = true,
|
||||
highlightBookingId,
|
||||
}: TrainConsistViewProps) => {
|
||||
const [selectedWagonId, setSelectedWagonId] = useState<string | null>(null);
|
||||
@@ -172,6 +175,7 @@ export const TrainConsistView = ({
|
||||
lengthMax={lengthMax}
|
||||
wagonCount={wagonsUsed}
|
||||
wagonMax={maxWagons}
|
||||
showWagons={showWagonStat}
|
||||
/>
|
||||
|
||||
{/* Consist panel */}
|
||||
|
||||
@@ -9,6 +9,12 @@ interface TrainStatsBarProps {
|
||||
lengthMax: number | null;
|
||||
wagonCount: number;
|
||||
wagonMax: number;
|
||||
/**
|
||||
* The wagon tile is a consist-wide count, which reads as wrong on a
|
||||
* multi-leg schedule where per-leg capacity is the real number. Dispatch
|
||||
* hides it (leg capacity is shown there instead); the batch board keeps it.
|
||||
*/
|
||||
showWagons?: boolean;
|
||||
}
|
||||
|
||||
function pctColor(pct: number) {
|
||||
@@ -83,6 +89,7 @@ export const TrainStatsBar = ({
|
||||
lengthMax,
|
||||
wagonCount,
|
||||
wagonMax,
|
||||
showWagons = true,
|
||||
}: TrainStatsBarProps) => {
|
||||
const weightPct = weightMax ? (weightUsed / weightMax) * 100 : null;
|
||||
const lengthPct = lengthMax ? (lengthUsed / lengthMax) * 100 : null;
|
||||
@@ -95,7 +102,7 @@ export const TrainStatsBar = ({
|
||||
withBorder
|
||||
style={{ borderColor: "var(--mantine-color-gray-2)", background: "white" }}
|
||||
>
|
||||
<SimpleGrid cols={{ base: 1, xs: 3 }} spacing="lg">
|
||||
<SimpleGrid cols={{ base: 1, xs: showWagons ? 3 : 2 }} spacing="lg">
|
||||
<StatTile
|
||||
icon={<Weight size={15} />}
|
||||
label="Gross weight"
|
||||
@@ -108,7 +115,7 @@ export const TrainStatsBar = ({
|
||||
px={{ base: 0, xs: "lg" }}
|
||||
style={{
|
||||
borderLeft: "1px solid var(--mantine-color-gray-2)",
|
||||
borderRight: "1px solid var(--mantine-color-gray-2)",
|
||||
borderRight: showWagons ? "1px solid var(--mantine-color-gray-2)" : undefined,
|
||||
}}
|
||||
>
|
||||
<StatTile
|
||||
@@ -120,6 +127,7 @@ export const TrainStatsBar = ({
|
||||
unit="m"
|
||||
/>
|
||||
</Box>
|
||||
{showWagons ? (
|
||||
<StatTile
|
||||
icon={<Train size={15} />}
|
||||
label="Wagons"
|
||||
@@ -128,6 +136,7 @@ export const TrainStatsBar = ({
|
||||
max={String(wagonMax)}
|
||||
unit=""
|
||||
/>
|
||||
) : null}
|
||||
</SimpleGrid>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
@@ -848,6 +848,7 @@ export default function TrainScheduleV2DetailPage() {
|
||||
scheduleDetail={schedule}
|
||||
scheduleId={scheduleId ?? ""}
|
||||
maxWagons={schedule.maxWagons ?? 53}
|
||||
showWagonStat={false}
|
||||
/>
|
||||
) : (
|
||||
<TrainCompositionDiagram
|
||||
|
||||
@@ -330,17 +330,6 @@ export default function TrainScheduleV2ListPage() {
|
||||
meta: { headerClassName, cellClassName },
|
||||
cell: ({ row }) => (
|
||||
<Stack gap={6}>
|
||||
<Group gap={6} wrap="nowrap">
|
||||
<Text size="sm" fw={600} lh={1.2}>
|
||||
{row.original.routeName ?? "—"}
|
||||
</Text>
|
||||
{row.original.direction ? (
|
||||
<Badge size="xs" variant="light" color={directionColor(row.original.direction)}>
|
||||
{row.original.direction}
|
||||
</Badge>
|
||||
) : null}
|
||||
<ShippingLineBadge schedule={row.original} />
|
||||
</Group>
|
||||
<Box maw={260}>
|
||||
<RouteCorridor
|
||||
origin={row.original.origin}
|
||||
@@ -349,6 +338,14 @@ export default function TrainScheduleV2ListPage() {
|
||||
orientation="vertical"
|
||||
/>
|
||||
</Box>
|
||||
<Group gap={6} wrap="nowrap">
|
||||
{row.original.direction ? (
|
||||
<Badge size="xs" variant="light" color={directionColor(row.original.direction)}>
|
||||
{row.original.direction}
|
||||
</Badge>
|
||||
) : null}
|
||||
<ShippingLineBadge schedule={row.original} />
|
||||
</Group>
|
||||
</Stack>
|
||||
),
|
||||
},
|
||||
@@ -888,12 +885,12 @@ export default function TrainScheduleV2ListPage() {
|
||||
}
|
||||
|
||||
/**
|
||||
* The row's wagon chips, matching the detail page's wagon plan: used is slots
|
||||
* carrying a booking allocation, the denominator is the schedule's capacity
|
||||
* (API-computed: the larger of coupled consist and planned `maxWagons`, since
|
||||
* wagons are coupled on demand), and remaining excludes wagons reserved by
|
||||
* bookings that have not paid yet — that space is claimed, so it is not
|
||||
* bookable.
|
||||
* The row's wagon chips: used is slots carrying a booking allocation, the
|
||||
* denominator is the schedule's capacity (API-computed: the larger of coupled
|
||||
* consist and planned `maxWagons`, since wagons are coupled on demand). Both
|
||||
* are consist-wide totals, so on a multi-leg schedule they do not describe any
|
||||
* single leg — the bookable/planned counts were dropped for that reason; the
|
||||
* detail page's wagon plan is the per-leg source of truth.
|
||||
*/
|
||||
/** Green tint for departures dedicated to a shipping line (overrides direction tint). */
|
||||
const SHIPPING_LINE_ROW_STYLE = {
|
||||
@@ -965,20 +962,15 @@ function WagonChips({ schedule }: { schedule: TrainScheduleListItem }) {
|
||||
const total = schedule.wagonsTotal ?? schedule.wagonCount;
|
||||
const used = schedule.wagonsUsed;
|
||||
const reserved = schedule.wagonsReserved ?? 0;
|
||||
const remaining = schedule.wagonsRemaining;
|
||||
|
||||
if (used == null) {
|
||||
if (used == null || schedule.wagonCount === 0) {
|
||||
return <MetricChip value={total} label="wgn" subtle />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<MetricChip
|
||||
value={`${used}/${total}`}
|
||||
label={schedule.wagonCount === 0 ? "wgn planned" : "wgn used"}
|
||||
/>
|
||||
<MetricChip value={`${used}/${total}`} label="wgn used" />
|
||||
{reserved > used ? <MetricChip value={reserved} label="reserved" subtle /> : null}
|
||||
{remaining != null ? <MetricChip value={remaining} label="bookable" subtle /> : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1049,9 +1041,6 @@ function ScheduleCard({
|
||||
{schedule.reference}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text fw={600} size="sm" lineClamp={1}>
|
||||
{schedule.routeName ?? "Train schedule"}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed">
|
||||
{day} · {time}
|
||||
|
||||
@@ -8,11 +8,12 @@ import {
|
||||
Package,
|
||||
TrainFront,
|
||||
Truck,
|
||||
Wallet,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
|
||||
import { useFileViewer } from "@/hooks/useFileViewer";
|
||||
import { bookingsService } from "@/services/bookings.service";
|
||||
@@ -44,17 +45,17 @@ import { HeaderButton, PageHeader } from "./components/PageHeader";
|
||||
import { PaymentMethodModal } from "./components/PaymentMethodModal";
|
||||
import { ScheduleCard } from "./components/ScheduleCard";
|
||||
import { WarehousePaymentsSection } from "./components/WarehousePaymentsSection";
|
||||
import { PaymentsDueStrip, PaymentsTab } from "./components/PaymentsTab";
|
||||
import { WarehouseLocationCard } from "./components/WarehouseLocationCard";
|
||||
import { ShipmentDetailsCard } from "./components/ShipmentDetailsCard";
|
||||
import { ShipmentTrackingCard } from "./components/ShipmentTrackingCard";
|
||||
import { StatusHero } from "./components/StatusHero";
|
||||
import { SupportCard } from "./components/SupportCard";
|
||||
import { WagonCancellationCard } from "./components/WagonCancellationCard";
|
||||
import { WagonsTab } from "./components/WagonsTab";
|
||||
import { fmtDate, isNegative, priceTotal } from "./utils";
|
||||
import { useScrollToHash } from "@/hooks/useScrollToHash";
|
||||
import { useBookingPayment } from "@/pages/bookings/payments/useBookingPayment";
|
||||
import { isUsdOfflineBooking } from "@/pages/bookings/payments/offline-payment";
|
||||
import { useBookingPayables } from "@/pages/bookings/payments/useBookingPayables";
|
||||
|
||||
// Pre-payment statuses the customer may self-cancel from this view (free of
|
||||
// charge). DRAFT / CHANGES_REQUESTED render their own views and drafts can
|
||||
@@ -87,6 +88,22 @@ export function ReadonlyBookingView({
|
||||
const navigate = useNavigate();
|
||||
// Deep-link support: e.g. /bookings/:id#warehouse-payments from an invoice.
|
||||
useScrollToHash();
|
||||
// Active tab lives in the URL (?tab=payments) so list/home "Pay" buttons and
|
||||
// notifications can land straight on the Payments tab.
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const tab = searchParams.get("tab") ?? "overview";
|
||||
const setTab = (next: string | null) =>
|
||||
setSearchParams(
|
||||
(prev) => {
|
||||
if (!next || next === "overview") prev.delete("tab");
|
||||
else prev.set("tab", next);
|
||||
return prev;
|
||||
},
|
||||
{ replace: true },
|
||||
);
|
||||
// Everything the customer owes or must decide on — drives the header "Pay"
|
||||
// button, the tab badge and the overview strip.
|
||||
const payables = useBookingPayables(booking);
|
||||
const status = booking.status as string;
|
||||
const { viewer } = useFileViewer();
|
||||
|
||||
@@ -187,17 +204,19 @@ export function ReadonlyBookingView({
|
||||
<PageHeader
|
||||
booking={booking}
|
||||
actions={
|
||||
(canApproveDelivery || (canPay && !showCountdown) || canCancel) && (
|
||||
(canApproveDelivery ||
|
||||
(payables.items.length > 0 && tab !== "payments") ||
|
||||
canCancel) && (
|
||||
<Group gap={8} wrap="nowrap">
|
||||
{canApproveDelivery && (
|
||||
<ApproveDeliveryButton bookingId={booking.id} />
|
||||
)}
|
||||
{canPay && !showCountdown && !isUsdOfflineBooking(booking) && (
|
||||
{payables.items.length > 0 && tab !== "payments" && (
|
||||
<HeaderButton
|
||||
green
|
||||
icon={<CreditCard size={16} />}
|
||||
label="Pay now"
|
||||
onClick={pay.open}
|
||||
label="Pay"
|
||||
onClick={() => setTab("payments")}
|
||||
/>
|
||||
)}
|
||||
{canCancel && (
|
||||
@@ -260,7 +279,8 @@ export function ReadonlyBookingView({
|
||||
<KeyFactsStrip booking={booking} />
|
||||
|
||||
<Tabs
|
||||
defaultValue="overview"
|
||||
value={tab}
|
||||
onChange={setTab}
|
||||
keepMounted={false}
|
||||
color="edr-green"
|
||||
styles={{
|
||||
@@ -277,6 +297,32 @@ export function ReadonlyBookingView({
|
||||
<Tabs.Tab value="overview" leftSection={<LayoutGrid size={15} />}>
|
||||
Overview
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab
|
||||
value="payments"
|
||||
leftSection={<Wallet size={15} />}
|
||||
rightSection={
|
||||
payables.items.length > 0 ? (
|
||||
<Text
|
||||
component="span"
|
||||
fz={11}
|
||||
fw={800}
|
||||
c="white"
|
||||
px={6}
|
||||
style={{
|
||||
borderRadius: 999,
|
||||
background: "#B45309",
|
||||
lineHeight: "18px",
|
||||
minWidth: 18,
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
{payables.items.length}
|
||||
</Text>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
Payments
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="cargo" leftSection={<Package size={15} />}>
|
||||
Cargo
|
||||
</Tabs.Tab>
|
||||
@@ -298,12 +344,16 @@ export function ReadonlyBookingView({
|
||||
|
||||
<Tabs.Panel value="overview">
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* The only payment surface on Overview — everything payable lives
|
||||
on the Payments tab. Hidden when nothing is outstanding. */}
|
||||
<PaymentsDueStrip booking={booking} onOpen={() => setTab("payments")} />
|
||||
|
||||
<ContractCard booking={booking} />
|
||||
|
||||
{isClearance && <ClearanceCard booking={booking} />}
|
||||
|
||||
{/* Customs (Path B) shipments: GL's phased progress, the duty /
|
||||
additional-duty payments and the final invoice — all per booking. */}
|
||||
{/* Customs (Path B) shipments: GL's phased progress, draft
|
||||
declaration and documents. Its payments moved to the Payments tab. */}
|
||||
<BookingClearanceWorkflowBanner booking={booking} />
|
||||
|
||||
<BodyGrid
|
||||
@@ -333,11 +383,6 @@ export function ReadonlyBookingView({
|
||||
title="Consignment & Schedule"
|
||||
consignment
|
||||
/>
|
||||
{/* Renders only on PAID + paid + contract-backed bookings. */}
|
||||
<WagonCancellationCard
|
||||
booking={booking}
|
||||
onBookingUpdated={onBookingUpdated}
|
||||
/>
|
||||
<CompanyInfoCard booking={booking} />
|
||||
<SupportCard />
|
||||
</>
|
||||
@@ -346,6 +391,15 @@ export function ReadonlyBookingView({
|
||||
</div>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="payments">
|
||||
<PaymentsTab
|
||||
booking={booking}
|
||||
pay={pay}
|
||||
showCountdown={showCountdown}
|
||||
onBookingUpdated={onBookingUpdated}
|
||||
/>
|
||||
</Tabs.Panel>
|
||||
|
||||
<Tabs.Panel value="cargo">
|
||||
<CargoTab booking={booking} />
|
||||
</Tabs.Panel>
|
||||
|
||||
Reference in New Issue
Block a user