mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 05:18:11 +00:00
changes
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Badge } from "@mantine/core";
|
||||
import { Badge, Group } from "@mantine/core";
|
||||
import { Link2 } from "lucide-react";
|
||||
import { BOOKING_STATUS_STYLES } from "@/features/bookings/booking-status.config";
|
||||
|
||||
const statusColorMap: Record<string, string> = {
|
||||
@@ -24,14 +25,26 @@ const statusColorMap: Record<string, string> = {
|
||||
CONSOLIDATED: "indigo",
|
||||
};
|
||||
|
||||
export function BookingStatusBadge({ status }: { status: string }) {
|
||||
interface BookingStatusBadgeProps {
|
||||
status: string;
|
||||
/** When the booking is part of a consolidation, show a sibling badge. */
|
||||
consolidated?: boolean;
|
||||
/** Partner booking reference for the consolidated badge tooltip. */
|
||||
partnerReference?: string | null;
|
||||
}
|
||||
|
||||
export function BookingStatusBadge({
|
||||
status,
|
||||
consolidated,
|
||||
partnerReference,
|
||||
}: BookingStatusBadgeProps) {
|
||||
const style = BOOKING_STATUS_STYLES[status] ?? {
|
||||
label: status,
|
||||
color: "gray",
|
||||
};
|
||||
const color = statusColorMap[status] ?? "gray";
|
||||
|
||||
return (
|
||||
const statusBadge = (
|
||||
<Badge
|
||||
color={color}
|
||||
variant="light"
|
||||
@@ -51,4 +64,34 @@ export function BookingStatusBadge({ status }: { status: string }) {
|
||||
{style.label}
|
||||
</Badge>
|
||||
);
|
||||
|
||||
if (!consolidated) return statusBadge;
|
||||
|
||||
return (
|
||||
<Group gap={4} wrap="nowrap">
|
||||
{statusBadge}
|
||||
<Badge
|
||||
color="indigo"
|
||||
variant="light"
|
||||
size="sm"
|
||||
radius="md"
|
||||
tt="uppercase"
|
||||
fw={600}
|
||||
leftSection={<Link2 size={12} />}
|
||||
title={
|
||||
partnerReference
|
||||
? `Consolidated with ${partnerReference}`
|
||||
: "Part of a consolidation"
|
||||
}
|
||||
style={{
|
||||
fontSize: "0.7rem",
|
||||
letterSpacing: "0.05em",
|
||||
display: "inline-flex",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
Consolidated
|
||||
</Badge>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,11 @@ export function BookingDetailHeader({
|
||||
<Title order={2} fw={700} style={{ letterSpacing: "-0.4px" }}>
|
||||
{booking.reference}
|
||||
</Title>
|
||||
<BookingStatusBadge status={booking.status} />
|
||||
<BookingStatusBadge
|
||||
status={booking.status}
|
||||
consolidated={Boolean(booking.consolidationPartnerId)}
|
||||
partnerReference={booking.consolidationPartner?.reference}
|
||||
/>
|
||||
</Group>
|
||||
<Group gap="xs">
|
||||
<Building2 size={14} color="var(--mantine-color-gray-5)" />
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Alert, List, Text } from "@mantine/core";
|
||||
import { Link2 } from "lucide-react";
|
||||
|
||||
import { bookingsService } from "@/services/bookings.service";
|
||||
import { QUERY_KEYS } from "@/constants/QUERY_KEYS";
|
||||
|
||||
/**
|
||||
* Shown on a booking parked in PENDING_CONSOLIDATION. Explains that the booking
|
||||
* cannot be approved until a complementary booking fills the wagon, and lists
|
||||
* the partial-wagon container lines that are waiting for a partner.
|
||||
*/
|
||||
export function ConsolidationWaitingBanner({ bookingId }: { bookingId: string }) {
|
||||
const { data } = useQuery({
|
||||
queryKey: [...QUERY_KEYS.BOOKINGS.byId(bookingId), "consolidation"],
|
||||
queryFn: () => bookingsService.getConsolidationDetails(bookingId),
|
||||
enabled: Boolean(bookingId),
|
||||
});
|
||||
|
||||
return (
|
||||
<Alert
|
||||
color="yellow"
|
||||
variant="light"
|
||||
icon={<Link2 size={18} />}
|
||||
title="Waiting for a consolidation partner"
|
||||
>
|
||||
<Text size="sm">
|
||||
This booking cannot be approved until a matching booking fills the
|
||||
wagon. It will return to the approval queue automatically once a partner
|
||||
is found.
|
||||
</Text>
|
||||
{data?.wagonSlots?.length ? (
|
||||
<List size="sm" mt="xs" spacing={2}>
|
||||
{data.wagonSlots.map((slot) => (
|
||||
<List.Item key={slot.containerTypeCode}>
|
||||
{slot.slotsNeeded} more × {slot.containerTypeCode} (
|
||||
{slot.containersPerWagon} per wagon; you have {slot.quantity})
|
||||
</List.Item>
|
||||
))}
|
||||
</List>
|
||||
) : null}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -347,6 +347,10 @@ export function getBookingActions(
|
||||
case "CHANGES_REQUESTED":
|
||||
actions = [CANCEL_ACTION];
|
||||
break;
|
||||
case "PENDING_CONSOLIDATION":
|
||||
// View-only while waiting for a consolidation partner; cancel still allowed.
|
||||
actions = [CANCEL_ACTION];
|
||||
break;
|
||||
default:
|
||||
actions = [];
|
||||
}
|
||||
|
||||
@@ -212,21 +212,25 @@ export const BOOKING_STATUS_META: Record<string, StatusMeta> = {
|
||||
},
|
||||
PENDING_CONSOLIDATION: {
|
||||
title: "Pending Consolidation",
|
||||
description: "Waiting for consolidation partner.",
|
||||
description: "Blocked — waiting for a consolidation partner before approval.",
|
||||
color: "text-amber-600",
|
||||
stage: 4,
|
||||
stage: 0,
|
||||
},
|
||||
CONSOLIDATED: {
|
||||
title: "Consolidated",
|
||||
description: "Paired with another booking.",
|
||||
color: "text-indigo-600",
|
||||
stage: 4,
|
||||
stage: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const BOOKING_LIST_TABS = [
|
||||
{ key: "all", label: "All bookings", statuses: null as string[] | null },
|
||||
{ key: "intake", label: "Submitted", statuses: ["SUBMITTED"] },
|
||||
{
|
||||
key: "intake",
|
||||
label: "Submitted",
|
||||
statuses: ["SUBMITTED", "PENDING_CONSOLIDATION"],
|
||||
},
|
||||
{
|
||||
key: "in_approval",
|
||||
label: "In approval",
|
||||
@@ -256,7 +260,7 @@ export const BOOKING_LIST_TABS = [
|
||||
{
|
||||
key: "operations",
|
||||
label: "Operations",
|
||||
statuses: ["PAID", "IN_TRANSIT", "PENDING_CONSOLIDATION", "CONSOLIDATED"],
|
||||
statuses: ["PAID", "IN_TRANSIT"],
|
||||
},
|
||||
{ key: "completed", label: "Completed", statuses: ["COMPLETED"] },
|
||||
{ key: "closed", label: "Closed", statuses: ["REJECTED", "CANCELLED"] },
|
||||
@@ -265,7 +269,15 @@ export const BOOKING_LIST_TABS = [
|
||||
export type BookingStatusTabKey = (typeof BOOKING_LIST_TABS)[number]["key"];
|
||||
|
||||
export const WORKFLOW_STAGES = [
|
||||
{ label: "Submission", statuses: ["DRAFT", "SUBMITTED", "CHANGES_REQUESTED"] },
|
||||
{
|
||||
label: "Submission",
|
||||
statuses: [
|
||||
"DRAFT",
|
||||
"SUBMITTED",
|
||||
"CHANGES_REQUESTED",
|
||||
"PENDING_CONSOLIDATION",
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Approval",
|
||||
statuses: ["PENDING_APPROVAL", "APPROVED_PENDING_SIGNATURE"],
|
||||
@@ -286,7 +298,7 @@ export const WORKFLOW_STAGES = [
|
||||
},
|
||||
{
|
||||
label: "Operations",
|
||||
statuses: ["PAID", "IN_TRANSIT", "PENDING_CONSOLIDATION", "CONSOLIDATED"],
|
||||
statuses: ["PAID", "IN_TRANSIT"],
|
||||
},
|
||||
{ label: "Done", statuses: ["COMPLETED"] },
|
||||
] as const;
|
||||
|
||||
@@ -42,6 +42,8 @@ export function toBookingListRow(booking: BookingDetail): BookingListRow {
|
||||
trainScheduleId: booking.trainScheduleId ?? null,
|
||||
isGovernment: booking.isGovernment ?? false,
|
||||
governmentInstitution: booking.governmentInstitution ?? null,
|
||||
consolidationPartnerId: booking.consolidationPartnerId ?? null,
|
||||
consolidationPartnerReference: booking.consolidationPartner?.reference ?? null,
|
||||
createdAt: booking.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ApprovalStepsCard } from "@/components/bookings/ApprovalStepsCard";
|
||||
import { BookingActionsToolbar } from "@/components/bookings/BookingActionsToolbar";
|
||||
import { BookingPricingSummary } from "@/components/bookings/BookingPricingSummary";
|
||||
import { BookingWorkflowStepper } from "@/components/bookings/BookingWorkflowStepper";
|
||||
import { ConsolidationWaitingBanner } from "@/components/bookings/detail/ConsolidationWaitingBanner";
|
||||
import {
|
||||
detailStyles,
|
||||
BookingRequestHero,
|
||||
@@ -128,6 +129,10 @@ export default function BookingRequestDetailPage() {
|
||||
titleColor={statusMeta.color}
|
||||
/>
|
||||
|
||||
{booking.status === "PENDING_CONSOLIDATION" && (
|
||||
<ConsolidationWaitingBanner bookingId={booking.id} />
|
||||
)}
|
||||
|
||||
<Grid gutter="lg">
|
||||
{/* LEFT — primary content */}
|
||||
<Grid.Col span={{ base: 12, lg: 8 }}>
|
||||
|
||||
@@ -227,7 +227,11 @@ export default function BookingRequestsPage() {
|
||||
header: () => <span className={bookingTable.headerCell}>Status</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="py-1">
|
||||
<BookingStatusBadge status={row.original.status} />
|
||||
<BookingStatusBadge
|
||||
status={row.original.status}
|
||||
consolidated={Boolean(row.original.consolidationPartnerId)}
|
||||
partnerReference={row.original.consolidationPartnerReference}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
meta: {
|
||||
|
||||
@@ -87,6 +87,21 @@ export interface ContractView {
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface ConsolidationWagonSlot {
|
||||
containerTypeCode: string;
|
||||
quantity: number;
|
||||
containersPerWagon: number;
|
||||
remainder: number;
|
||||
slotsNeeded: number;
|
||||
}
|
||||
|
||||
export interface ConsolidationDetails {
|
||||
statusMessage: string;
|
||||
wagonSlots: ConsolidationWagonSlot[];
|
||||
partner: { id: string; reference: string } | null;
|
||||
splitBilling: { bookingShare: number; partnerShare: number } | null;
|
||||
}
|
||||
|
||||
export interface SignContractPayload {
|
||||
role: "CUSTOMER" | "STAFF";
|
||||
signatureImageBase64: string;
|
||||
@@ -198,6 +213,13 @@ export const bookingsService = {
|
||||
return unwrap(response.data);
|
||||
},
|
||||
|
||||
getConsolidationDetails: async (
|
||||
id: string,
|
||||
): Promise<ConsolidationDetails> => {
|
||||
const response = await client.get<ConsolidationDetails>(B.CONSOLIDATION(id));
|
||||
return unwrap(response.data) as ConsolidationDetails;
|
||||
},
|
||||
|
||||
customerSign: (id: string, payload: SignContractPayload) =>
|
||||
postBooking<BookingDetail>(B.CUSTOMER_SIGN(id), {
|
||||
...payload,
|
||||
|
||||
@@ -101,6 +101,8 @@ export interface BookingDetail {
|
||||
cargoTotalWeightVgm: number;
|
||||
isHazardous: boolean;
|
||||
allowConsolidation: boolean;
|
||||
consolidationPartnerId?: string | null;
|
||||
consolidationPartner?: BookingNamedRef & { reference?: string } | null;
|
||||
priorityScore: number;
|
||||
schedulingStatus?: string;
|
||||
holdExpiresAt?: string | null;
|
||||
@@ -157,5 +159,7 @@ export interface BookingListRow {
|
||||
trainScheduleId?: string | null;
|
||||
isGovernment?: boolean;
|
||||
governmentInstitution?: string | null;
|
||||
consolidationPartnerId?: string | null;
|
||||
consolidationPartnerReference?: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user