mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 05:55:02 +00:00
changes
This commit is contained in:
@@ -16,7 +16,7 @@ export const BOOKING_LIST_TABS: ReadonlyArray<{
|
|||||||
statuses: readonly string[] | null;
|
statuses: readonly string[] | null;
|
||||||
}> = [
|
}> = [
|
||||||
{ key: 'all', statuses: null },
|
{ key: 'all', statuses: null },
|
||||||
{ key: 'intake', statuses: ['SUBMITTED'] },
|
{ key: 'intake', statuses: ['SUBMITTED', 'PENDING_CONSOLIDATION'] },
|
||||||
{
|
{
|
||||||
key: 'in_approval',
|
key: 'in_approval',
|
||||||
statuses: ['PENDING_APPROVAL', 'APPROVED_PENDING_SIGNATURE'],
|
statuses: ['PENDING_APPROVAL', 'APPROVED_PENDING_SIGNATURE'],
|
||||||
@@ -28,7 +28,7 @@ export const BOOKING_LIST_TABS: ReadonlyArray<{
|
|||||||
{ key: 'payment', statuses: ['FULLY_EXECUTED'] },
|
{ key: 'payment', statuses: ['FULLY_EXECUTED'] },
|
||||||
{
|
{
|
||||||
key: 'operations',
|
key: 'operations',
|
||||||
statuses: ['IN_TRANSIT', 'PENDING_CONSOLIDATION', 'CONSOLIDATED','PAID'],
|
statuses: ['IN_TRANSIT', 'PAID'],
|
||||||
},
|
},
|
||||||
{ key: 'completed', statuses: ['COMPLETED'] },
|
{ key: 'completed', statuses: ['COMPLETED'] },
|
||||||
{ key: 'closed', statuses: ['REJECTED', 'CANCELLED'] },
|
{ key: 'closed', statuses: ['REJECTED', 'CANCELLED'] },
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common';
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
ConflictException,
|
||||||
|
forwardRef,
|
||||||
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
} from '@nestjs/common';
|
||||||
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
|
import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type';
|
||||||
|
|
||||||
import { assertCanApproveBookingStep } from '../../common/freight-permission.util';
|
import { assertCanApproveBookingStep } from '../../common/freight-permission.util';
|
||||||
@@ -184,6 +190,16 @@ export class BookingTransitionService {
|
|||||||
const booking = await this.bookingsService.findById(bookingId);
|
const booking = await this.bookingsService.findById(bookingId);
|
||||||
assertBookingStatus(booking, ['SUBMITTED']);
|
assertBookingStatus(booking, ['SUBMITTED']);
|
||||||
|
|
||||||
|
// Consolidation gate: a booking whose containers don't fill whole wagons
|
||||||
|
// cannot be accepted until it is paired with a complementary booking.
|
||||||
|
const gate = await this.bookingsService.resolveConsolidationGate(bookingId);
|
||||||
|
if (gate.blocked) {
|
||||||
|
throw new ConflictException(
|
||||||
|
gate.message ??
|
||||||
|
'Booking requires consolidation and cannot be accepted until a partner is found.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
await this.ruleEngineService.instantiateApprovalSteps(bookingId, {
|
await this.ruleEngineService.instantiateApprovalSteps(bookingId, {
|
||||||
freightType: booking.freightType as 'CONTAINER' | 'BULK',
|
freightType: booking.freightType as 'CONTAINER' | 'BULK',
|
||||||
cargoTypeId: booking.cargoTypeId,
|
cargoTypeId: booking.cargoTypeId,
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ export class BookingsRepository extends BaseRepository<Booking> {
|
|||||||
.leftJoinAndSelect('booking.rateSnapshots', 'snapshots')
|
.leftJoinAndSelect('booking.rateSnapshots', 'snapshots')
|
||||||
.leftJoinAndSelect('booking.cargoModifiers', 'modifiers')
|
.leftJoinAndSelect('booking.cargoModifiers', 'modifiers')
|
||||||
.leftJoinAndSelect('booking.reviewNotes', 'reviewNotes')
|
.leftJoinAndSelect('booking.reviewNotes', 'reviewNotes')
|
||||||
|
.leftJoinAndSelect('booking.consolidationPartner', 'consolidationPartner')
|
||||||
.where('booking.id = :id', { id })
|
.where('booking.id = :id', { id })
|
||||||
.leftJoinAndMapMany(
|
.leftJoinAndMapMany(
|
||||||
'booking.files',
|
'booking.files',
|
||||||
@@ -177,7 +178,7 @@ export class BookingsRepository extends BaseRepository<Booking> {
|
|||||||
.andWhere('b.allowConsolidation = true')
|
.andWhere('b.allowConsolidation = true')
|
||||||
.andWhere('b.consolidationPartnerId IS NULL')
|
.andWhere('b.consolidationPartnerId IS NULL')
|
||||||
.andWhere('b.status IN (:...statuses)', {
|
.andWhere('b.status IN (:...statuses)', {
|
||||||
statuses: ['DRAFT', 'PENDING_CONSOLIDATION'],
|
statuses: ['DRAFT', 'SUBMITTED', 'PENDING_CONSOLIDATION'],
|
||||||
})
|
})
|
||||||
.andWhere('b.originYardId = :originYardId', {
|
.andWhere('b.originYardId = :originYardId', {
|
||||||
originYardId: booking.originYardId,
|
originYardId: booking.originYardId,
|
||||||
@@ -214,15 +215,27 @@ export class BookingsRepository extends BaseRepository<Booking> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Pair two bookings for consolidation. */
|
/**
|
||||||
|
* Pair two bookings for consolidation. Both return to SUBMITTED so staff can
|
||||||
|
* accept them into the approval chain; the link itself (consolidationPartnerId)
|
||||||
|
* marks them as consolidated in the UI.
|
||||||
|
*/
|
||||||
async pairConsolidation(bookingId: string, partnerId: string): Promise<void> {
|
async pairConsolidation(bookingId: string, partnerId: string): Promise<void> {
|
||||||
await this.repository.update(bookingId, {
|
await this.repository.update(bookingId, {
|
||||||
consolidationPartnerId: partnerId,
|
consolidationPartnerId: partnerId,
|
||||||
status: 'CONSOLIDATED',
|
status: 'SUBMITTED',
|
||||||
} as never);
|
} as never);
|
||||||
await this.repository.update(partnerId, {
|
await this.repository.update(partnerId, {
|
||||||
consolidationPartnerId: bookingId,
|
consolidationPartnerId: bookingId,
|
||||||
status: 'CONSOLIDATED',
|
status: 'SUBMITTED',
|
||||||
|
} as never);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Park a booking that needs consolidation but has no partner yet. */
|
||||||
|
async parkForConsolidation(bookingId: string): Promise<void> {
|
||||||
|
await this.repository.update(bookingId, {
|
||||||
|
consolidationPartnerId: null,
|
||||||
|
status: 'PENDING_CONSOLIDATION',
|
||||||
} as never);
|
} as never);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -429,6 +442,7 @@ export class BookingsRepository extends BaseRepository<Booking> {
|
|||||||
.leftJoinAndSelect('booking.destinationYard', 'destinationYard')
|
.leftJoinAndSelect('booking.destinationYard', 'destinationYard')
|
||||||
.leftJoinAndSelect('booking.serviceType', 'serviceType')
|
.leftJoinAndSelect('booking.serviceType', 'serviceType')
|
||||||
.leftJoinAndSelect('booking.approvalSteps', 'approvalSteps')
|
.leftJoinAndSelect('booking.approvalSteps', 'approvalSteps')
|
||||||
|
.leftJoinAndSelect('booking.consolidationPartner', 'consolidationPartner')
|
||||||
.where('booking.deleted_at IS NULL');
|
.where('booking.deleted_at IS NULL');
|
||||||
|
|
||||||
this.applyListFilters(qb, options);
|
this.applyListFilters(qb, options);
|
||||||
|
|||||||
@@ -204,6 +204,48 @@ export class BookingsService {
|
|||||||
return { booking: pending, messages };
|
return { booking: pending, messages };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consolidation gate used at staff-accept time. Returns the (possibly newly
|
||||||
|
* paired) booking plus whether it still needs a consolidation partner.
|
||||||
|
* When a booking needs consolidation and none is found, it is parked in
|
||||||
|
* PENDING_CONSOLIDATION and `blocked` is true so the caller refuses the accept.
|
||||||
|
*/
|
||||||
|
async resolveConsolidationGate(bookingId: string): Promise<{
|
||||||
|
booking: Booking;
|
||||||
|
blocked: boolean;
|
||||||
|
message?: string;
|
||||||
|
}> {
|
||||||
|
let booking = await this.findById(bookingId);
|
||||||
|
|
||||||
|
// Already paired — passes the gate.
|
||||||
|
if (booking.consolidationPartnerId) {
|
||||||
|
return { booking, blocked: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
const needs =
|
||||||
|
await this.consolidationService.needsConsolidationFromBooking(booking);
|
||||||
|
if (!needs) {
|
||||||
|
return { booking, blocked: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
// A partner may have appeared since submission — try to pair now.
|
||||||
|
const result = await this.tryAutoConsolidate(booking);
|
||||||
|
booking = result.booking;
|
||||||
|
if (booking.consolidationPartnerId) {
|
||||||
|
return { booking, blocked: false, message: result.messages.join(' ') };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Still no partner — park it and block the accept.
|
||||||
|
await this.bookingsRepository.parkForConsolidation(booking.id);
|
||||||
|
booking = await this.findById(booking.id);
|
||||||
|
const slots = await this.consolidationService.slotsFromBooking(booking);
|
||||||
|
return {
|
||||||
|
booking,
|
||||||
|
blocked: true,
|
||||||
|
message: this.consolidationService.describePending(booking, slots),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Create a new freight booking. */
|
/** Create a new freight booking. */
|
||||||
async create(
|
async create(
|
||||||
dto: CreateBookingDto,
|
dto: CreateBookingDto,
|
||||||
|
|||||||
@@ -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";
|
import { BOOKING_STATUS_STYLES } from "@/features/bookings/booking-status.config";
|
||||||
|
|
||||||
const statusColorMap: Record<string, string> = {
|
const statusColorMap: Record<string, string> = {
|
||||||
@@ -24,14 +25,26 @@ const statusColorMap: Record<string, string> = {
|
|||||||
CONSOLIDATED: "indigo",
|
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] ?? {
|
const style = BOOKING_STATUS_STYLES[status] ?? {
|
||||||
label: status,
|
label: status,
|
||||||
color: "gray",
|
color: "gray",
|
||||||
};
|
};
|
||||||
const color = statusColorMap[status] ?? "gray";
|
const color = statusColorMap[status] ?? "gray";
|
||||||
|
|
||||||
return (
|
const statusBadge = (
|
||||||
<Badge
|
<Badge
|
||||||
color={color}
|
color={color}
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -51,4 +64,34 @@ export function BookingStatusBadge({ status }: { status: string }) {
|
|||||||
{style.label}
|
{style.label}
|
||||||
</Badge>
|
</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" }}>
|
<Title order={2} fw={700} style={{ letterSpacing: "-0.4px" }}>
|
||||||
{booking.reference}
|
{booking.reference}
|
||||||
</Title>
|
</Title>
|
||||||
<BookingStatusBadge status={booking.status} />
|
<BookingStatusBadge
|
||||||
|
status={booking.status}
|
||||||
|
consolidated={Boolean(booking.consolidationPartnerId)}
|
||||||
|
partnerReference={booking.consolidationPartner?.reference}
|
||||||
|
/>
|
||||||
</Group>
|
</Group>
|
||||||
<Group gap="xs">
|
<Group gap="xs">
|
||||||
<Building2 size={14} color="var(--mantine-color-gray-5)" />
|
<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":
|
case "CHANGES_REQUESTED":
|
||||||
actions = [CANCEL_ACTION];
|
actions = [CANCEL_ACTION];
|
||||||
break;
|
break;
|
||||||
|
case "PENDING_CONSOLIDATION":
|
||||||
|
// View-only while waiting for a consolidation partner; cancel still allowed.
|
||||||
|
actions = [CANCEL_ACTION];
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
actions = [];
|
actions = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,21 +212,25 @@ export const BOOKING_STATUS_META: Record<string, StatusMeta> = {
|
|||||||
},
|
},
|
||||||
PENDING_CONSOLIDATION: {
|
PENDING_CONSOLIDATION: {
|
||||||
title: "Pending Consolidation",
|
title: "Pending Consolidation",
|
||||||
description: "Waiting for consolidation partner.",
|
description: "Blocked — waiting for a consolidation partner before approval.",
|
||||||
color: "text-amber-600",
|
color: "text-amber-600",
|
||||||
stage: 4,
|
stage: 0,
|
||||||
},
|
},
|
||||||
CONSOLIDATED: {
|
CONSOLIDATED: {
|
||||||
title: "Consolidated",
|
title: "Consolidated",
|
||||||
description: "Paired with another booking.",
|
description: "Paired with another booking.",
|
||||||
color: "text-indigo-600",
|
color: "text-indigo-600",
|
||||||
stage: 4,
|
stage: 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BOOKING_LIST_TABS = [
|
export const BOOKING_LIST_TABS = [
|
||||||
{ key: "all", label: "All bookings", statuses: null as string[] | null },
|
{ 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",
|
key: "in_approval",
|
||||||
label: "In approval",
|
label: "In approval",
|
||||||
@@ -256,7 +260,7 @@ export const BOOKING_LIST_TABS = [
|
|||||||
{
|
{
|
||||||
key: "operations",
|
key: "operations",
|
||||||
label: "Operations",
|
label: "Operations",
|
||||||
statuses: ["PAID", "IN_TRANSIT", "PENDING_CONSOLIDATION", "CONSOLIDATED"],
|
statuses: ["PAID", "IN_TRANSIT"],
|
||||||
},
|
},
|
||||||
{ key: "completed", label: "Completed", statuses: ["COMPLETED"] },
|
{ key: "completed", label: "Completed", statuses: ["COMPLETED"] },
|
||||||
{ key: "closed", label: "Closed", statuses: ["REJECTED", "CANCELLED"] },
|
{ 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 type BookingStatusTabKey = (typeof BOOKING_LIST_TABS)[number]["key"];
|
||||||
|
|
||||||
export const WORKFLOW_STAGES = [
|
export const WORKFLOW_STAGES = [
|
||||||
{ label: "Submission", statuses: ["DRAFT", "SUBMITTED", "CHANGES_REQUESTED"] },
|
{
|
||||||
|
label: "Submission",
|
||||||
|
statuses: [
|
||||||
|
"DRAFT",
|
||||||
|
"SUBMITTED",
|
||||||
|
"CHANGES_REQUESTED",
|
||||||
|
"PENDING_CONSOLIDATION",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: "Approval",
|
label: "Approval",
|
||||||
statuses: ["PENDING_APPROVAL", "APPROVED_PENDING_SIGNATURE"],
|
statuses: ["PENDING_APPROVAL", "APPROVED_PENDING_SIGNATURE"],
|
||||||
@@ -286,7 +298,7 @@ export const WORKFLOW_STAGES = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Operations",
|
label: "Operations",
|
||||||
statuses: ["PAID", "IN_TRANSIT", "PENDING_CONSOLIDATION", "CONSOLIDATED"],
|
statuses: ["PAID", "IN_TRANSIT"],
|
||||||
},
|
},
|
||||||
{ label: "Done", statuses: ["COMPLETED"] },
|
{ label: "Done", statuses: ["COMPLETED"] },
|
||||||
] as const;
|
] as const;
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ export function toBookingListRow(booking: BookingDetail): BookingListRow {
|
|||||||
trainScheduleId: booking.trainScheduleId ?? null,
|
trainScheduleId: booking.trainScheduleId ?? null,
|
||||||
isGovernment: booking.isGovernment ?? false,
|
isGovernment: booking.isGovernment ?? false,
|
||||||
governmentInstitution: booking.governmentInstitution ?? null,
|
governmentInstitution: booking.governmentInstitution ?? null,
|
||||||
|
consolidationPartnerId: booking.consolidationPartnerId ?? null,
|
||||||
|
consolidationPartnerReference: booking.consolidationPartner?.reference ?? null,
|
||||||
createdAt: booking.createdAt,
|
createdAt: booking.createdAt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { ApprovalStepsCard } from "@/components/bookings/ApprovalStepsCard";
|
|||||||
import { BookingActionsToolbar } from "@/components/bookings/BookingActionsToolbar";
|
import { BookingActionsToolbar } from "@/components/bookings/BookingActionsToolbar";
|
||||||
import { BookingPricingSummary } from "@/components/bookings/BookingPricingSummary";
|
import { BookingPricingSummary } from "@/components/bookings/BookingPricingSummary";
|
||||||
import { BookingWorkflowStepper } from "@/components/bookings/BookingWorkflowStepper";
|
import { BookingWorkflowStepper } from "@/components/bookings/BookingWorkflowStepper";
|
||||||
|
import { ConsolidationWaitingBanner } from "@/components/bookings/detail/ConsolidationWaitingBanner";
|
||||||
import {
|
import {
|
||||||
detailStyles,
|
detailStyles,
|
||||||
BookingRequestHero,
|
BookingRequestHero,
|
||||||
@@ -128,6 +129,10 @@ export default function BookingRequestDetailPage() {
|
|||||||
titleColor={statusMeta.color}
|
titleColor={statusMeta.color}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{booking.status === "PENDING_CONSOLIDATION" && (
|
||||||
|
<ConsolidationWaitingBanner bookingId={booking.id} />
|
||||||
|
)}
|
||||||
|
|
||||||
<Grid gutter="lg">
|
<Grid gutter="lg">
|
||||||
{/* LEFT — primary content */}
|
{/* LEFT — primary content */}
|
||||||
<Grid.Col span={{ base: 12, lg: 8 }}>
|
<Grid.Col span={{ base: 12, lg: 8 }}>
|
||||||
|
|||||||
@@ -227,7 +227,11 @@ export default function BookingRequestsPage() {
|
|||||||
header: () => <span className={bookingTable.headerCell}>Status</span>,
|
header: () => <span className={bookingTable.headerCell}>Status</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<div className="py-1">
|
<div className="py-1">
|
||||||
<BookingStatusBadge status={row.original.status} />
|
<BookingStatusBadge
|
||||||
|
status={row.original.status}
|
||||||
|
consolidated={Boolean(row.original.consolidationPartnerId)}
|
||||||
|
partnerReference={row.original.consolidationPartnerReference}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
meta: {
|
meta: {
|
||||||
|
|||||||
@@ -87,6 +87,21 @@ export interface ContractView {
|
|||||||
} | null;
|
} | 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 {
|
export interface SignContractPayload {
|
||||||
role: "CUSTOMER" | "STAFF";
|
role: "CUSTOMER" | "STAFF";
|
||||||
signatureImageBase64: string;
|
signatureImageBase64: string;
|
||||||
@@ -198,6 +213,13 @@ export const bookingsService = {
|
|||||||
return unwrap(response.data);
|
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) =>
|
customerSign: (id: string, payload: SignContractPayload) =>
|
||||||
postBooking<BookingDetail>(B.CUSTOMER_SIGN(id), {
|
postBooking<BookingDetail>(B.CUSTOMER_SIGN(id), {
|
||||||
...payload,
|
...payload,
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ export interface BookingDetail {
|
|||||||
cargoTotalWeightVgm: number;
|
cargoTotalWeightVgm: number;
|
||||||
isHazardous: boolean;
|
isHazardous: boolean;
|
||||||
allowConsolidation: boolean;
|
allowConsolidation: boolean;
|
||||||
|
consolidationPartnerId?: string | null;
|
||||||
|
consolidationPartner?: BookingNamedRef & { reference?: string } | null;
|
||||||
priorityScore: number;
|
priorityScore: number;
|
||||||
schedulingStatus?: string;
|
schedulingStatus?: string;
|
||||||
holdExpiresAt?: string | null;
|
holdExpiresAt?: string | null;
|
||||||
@@ -157,5 +159,7 @@ export interface BookingListRow {
|
|||||||
trainScheduleId?: string | null;
|
trainScheduleId?: string | null;
|
||||||
isGovernment?: boolean;
|
isGovernment?: boolean;
|
||||||
governmentInstitution?: string | null;
|
governmentInstitution?: string | null;
|
||||||
|
consolidationPartnerId?: string | null;
|
||||||
|
consolidationPartnerReference?: string | null;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user