mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 09:28:19 +00:00
add wagon allocation snapshot to train schedules
This commit is contained in:
@@ -21,6 +21,7 @@ import {
|
||||
RingProgress,
|
||||
SimpleGrid,
|
||||
Stack,
|
||||
Table,
|
||||
Tabs,
|
||||
Text,
|
||||
Title,
|
||||
@@ -60,6 +61,13 @@ import { fileViewUrl } from "@/constants/apiConfig";
|
||||
import { useFileViewer } from "@/hooks/useFileViewer";
|
||||
import toast from "react-hot-toast";
|
||||
import { labelForDocCode } from "@/pages/bookings/resubmit";
|
||||
import {
|
||||
BookingStatusBadge,
|
||||
BookingTypeBadge,
|
||||
CargoModeCell,
|
||||
PaymentBadge,
|
||||
SchedulingCell,
|
||||
} from "@/pages/bookings/booking-display";
|
||||
import { ContractClearancePanel } from "./ContractClearancePanel";
|
||||
import { ContractClearanceWorkflowBanner } from "./ContractClearanceWorkflowBanner";
|
||||
import { ClearanceUploadedDocumentsPanel } from "@/components/contracts/ClearanceUploadedDocumentsPanel";
|
||||
@@ -148,8 +156,15 @@ interface DocGroup {
|
||||
* contract PDF, company profile / onboarding documents, and clearance documents
|
||||
* (everything else — the clearance set uses dynamic per-contract codes). Empty
|
||||
* groups are dropped so the tab only renders sections that have files.
|
||||
*
|
||||
* GENERAL contracts clear per booking, so their contract-level "clearance"
|
||||
* leftovers are not shown — pass includeClearance: false to keep only the
|
||||
* profile / business-licence sections.
|
||||
*/
|
||||
function groupContractDocuments(files: ContractFile[]): DocGroup[] {
|
||||
function groupContractDocuments(
|
||||
files: ContractFile[],
|
||||
{ includeClearance = true }: { includeClearance?: boolean } = {},
|
||||
): DocGroup[] {
|
||||
const businessLicense: ContractFile[] = [];
|
||||
const profile: ContractFile[] = [];
|
||||
const clearance: ContractFile[] = [];
|
||||
@@ -159,7 +174,7 @@ function groupContractDocuments(files: ContractFile[]): DocGroup[] {
|
||||
if (f.code === "contract" || f.code.startsWith("signature_")) continue;
|
||||
else if (BUSINESS_LICENSE_DOC_CODES.has(f.code)) businessLicense.push(f);
|
||||
else if (PROFILE_DOC_CODES.has(f.code)) profile.push(f);
|
||||
else clearance.push(f);
|
||||
else if (includeClearance) clearance.push(f);
|
||||
}
|
||||
return [
|
||||
{ key: "clearance", title: "Clearance documents", files: clearance },
|
||||
@@ -328,7 +343,12 @@ export default function ContractDetailPage() {
|
||||
const routes = contract.routes ?? [];
|
||||
const pricing = contract.pricingBreakdown;
|
||||
const files = contract.files ?? [];
|
||||
const docGroups = groupContractDocuments(files);
|
||||
// GENERAL contracts clear per booking — clearance documents live on each
|
||||
// booking's detail page, so this tab keeps only profile/licence documents.
|
||||
const docGroups = groupContractDocuments(files, {
|
||||
includeClearance: contract.contractKind !== "GENERAL",
|
||||
});
|
||||
const docCount = docGroups.reduce((sum, g) => sum + g.files.length, 0);
|
||||
// The generated contract PDF — surfaced via a dedicated "View contract" button
|
||||
// in the header (it's excluded from the Documents tab groups).
|
||||
const contractPdf = files.find((f) => f.code === "contract");
|
||||
@@ -621,7 +641,7 @@ export default function ContractDetailPage() {
|
||||
active={tab === "documents"}
|
||||
icon={<Download size={16} />}
|
||||
label="Documents"
|
||||
count={files.length + (isPhasedCustomsClearance ? workflowFileCount : 0)}
|
||||
count={docCount + (isPhasedCustomsClearance ? workflowFileCount : 0)}
|
||||
/>
|
||||
<DetailTab
|
||||
value="bookings"
|
||||
@@ -1191,8 +1211,9 @@ export default function ContractDetailPage() {
|
||||
No documents yet
|
||||
</Text>
|
||||
<Text fz={13} c="dimmed" ta="center" maw={420}>
|
||||
The signed contract and any uploaded clearance documents will
|
||||
appear here.
|
||||
{isGeneral
|
||||
? "Your company profile documents (TIN certificate, business licence, ID) appear here. Clearance documents are managed on each booking."
|
||||
: "The signed contract and any uploaded clearance documents will appear here."}
|
||||
</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
@@ -1399,49 +1420,113 @@ export default function ContractDetailPage() {
|
||||
</Text>
|
||||
</Stack>
|
||||
) : (
|
||||
<Stack gap={10}>
|
||||
{contractBookings.map((booking) => (
|
||||
<Group
|
||||
key={booking.id}
|
||||
justify="space-between"
|
||||
wrap="nowrap"
|
||||
p="sm"
|
||||
style={{
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${BORDER}`,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => navigate(`/bookings/${booking.id}`)}
|
||||
>
|
||||
<Group gap={12} wrap="nowrap" style={{ minWidth: 0 }}>
|
||||
<Package
|
||||
size={16}
|
||||
color={MUTED}
|
||||
style={{ flexShrink: 0 }}
|
||||
/>
|
||||
<Box style={{ minWidth: 0 }}>
|
||||
<Text
|
||||
fz={14}
|
||||
fw={700}
|
||||
style={{ color: INK }}
|
||||
truncate
|
||||
<Table.ScrollContainer minWidth={980}>
|
||||
<Table
|
||||
verticalSpacing="sm"
|
||||
horizontalSpacing="md"
|
||||
highlightOnHover
|
||||
style={{ fontSize: 13 }}
|
||||
>
|
||||
<Table.Thead>
|
||||
<Table.Tr>
|
||||
<BookingsTh label="Booking" />
|
||||
<BookingsTh label="Type" />
|
||||
<BookingsTh label="Cargo" />
|
||||
<BookingsTh label="Route" />
|
||||
<BookingsTh label="Payment" />
|
||||
<BookingsTh label="Train" />
|
||||
<BookingsTh label="Status" />
|
||||
<BookingsTh label="Amount" right />
|
||||
</Table.Tr>
|
||||
</Table.Thead>
|
||||
<Table.Tbody>
|
||||
{contractBookings.map((booking) => {
|
||||
const origin =
|
||||
booking.originYard?.label ??
|
||||
booking.originYard?.code ??
|
||||
"—";
|
||||
const dest =
|
||||
booking.destinationYard?.label ??
|
||||
booking.destinationYard?.code ??
|
||||
"—";
|
||||
const amount = Number(booking.totalAmount ?? 0);
|
||||
return (
|
||||
<Table.Tr
|
||||
key={booking.id}
|
||||
style={{ cursor: "pointer" }}
|
||||
onClick={() => navigate(`/bookings/${booking.id}`)}
|
||||
>
|
||||
{booking.reference}
|
||||
</Text>
|
||||
{booking.scheduledDate && (
|
||||
<Text fz={12} c="dimmed" truncate>
|
||||
Ship{" "}
|
||||
{new Date(
|
||||
booking.scheduledDate,
|
||||
).toLocaleDateString()}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Group>
|
||||
<ContractStatusBadge status={booking.status} />
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
<Table.Td>
|
||||
<Group gap={10} wrap="nowrap">
|
||||
<Package
|
||||
size={16}
|
||||
color={MUTED}
|
||||
style={{ flexShrink: 0 }}
|
||||
/>
|
||||
<Box style={{ minWidth: 0 }}>
|
||||
<Text
|
||||
fz={13.5}
|
||||
fw={700}
|
||||
style={{ color: INK }}
|
||||
truncate
|
||||
>
|
||||
{booking.reference}
|
||||
</Text>
|
||||
<Text fz={11.5} c="dimmed">
|
||||
{booking.freightType === "BULK"
|
||||
? "Bulk cargo"
|
||||
: "Container"}
|
||||
</Text>
|
||||
</Box>
|
||||
</Group>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<BookingTypeBadge booking={booking} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<CargoModeCell booking={booking} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text fz={13} fw={600} style={{ color: INK }}>
|
||||
{origin} → {dest}
|
||||
</Text>
|
||||
{booking.scheduledDate && (
|
||||
<Text fz={11.5} c="dimmed">
|
||||
{new Date(
|
||||
booking.scheduledDate,
|
||||
).toLocaleDateString()}
|
||||
</Text>
|
||||
)}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<PaymentBadge status={booking.paymentStatus} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<SchedulingCell booking={booking} />
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<BookingStatusBadge status={booking.status} />
|
||||
</Table.Td>
|
||||
<Table.Td style={{ textAlign: "right" }}>
|
||||
<Text
|
||||
fz={13.5}
|
||||
fw={700}
|
||||
style={{
|
||||
color: amount > 0 ? INK : "#94A3B8",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{amount > 0
|
||||
? `ETB ${amount.toLocaleString()}`
|
||||
: "—"}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
);
|
||||
})}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
)}
|
||||
</Card>
|
||||
</Tabs.Panel>
|
||||
@@ -1572,6 +1657,23 @@ function SectionLabel({
|
||||
);
|
||||
}
|
||||
|
||||
/** Column header for the bookings table — mirrors the /bookings list styling. */
|
||||
function BookingsTh({ label, right }: { label: string; right?: boolean }) {
|
||||
return (
|
||||
<Table.Th style={{ textAlign: right ? "right" : "left" }}>
|
||||
<Text
|
||||
fz={11}
|
||||
fw={700}
|
||||
tt="uppercase"
|
||||
c="dimmed"
|
||||
style={{ letterSpacing: "0.06em", whiteSpace: "nowrap" }}
|
||||
>
|
||||
{label}
|
||||
</Text>
|
||||
</Table.Th>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unit noun for a capacity line: "containers" for CONTAINER freight, else the
|
||||
* bulk cargo's unit of measure ("tons" for PER_TON, "items" for PER_ITEM).
|
||||
|
||||
Reference in New Issue
Block a user