fix issue -> consolidation

This commit is contained in:
Marshal
2026-08-24 12:24:23 +00:00
parent 979f024869
commit 2a107e8ba3
20 changed files with 475 additions and 39 deletions

View File

@@ -2410,9 +2410,9 @@ export default function GlCreateBookingForm() {
<Stack gap={6}>
{overweightLines.map((line, i) => (
<Text key={i} fz="sm" c="#9A5B00">
{line.containerTypeCode}: {line.totalVgmTons}t exceeds
limit {line.maxAllowedTons}t (+{line.excessTons}t
overweight)
{line.containerLabel || line.containerTypeCode}:{" "}
{line.totalVgmTons}t exceeds limit {line.maxAllowedTons}t
(+{line.excessTons}t overweight)
</Text>
))}
<Text fz="xs" c="#9A5B00" mt={2}>

View File

@@ -1,6 +1,6 @@
import { directionLabel } from "@/lib/utils";
import { useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import { useMemo, useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import { useLocation, useParams } from "react-router-dom";
import {
Alert,
@@ -14,6 +14,7 @@ import {
Stack,
Text,
} from "@mantine/core";
import { DatePickerInput } from "@mantine/dates";
import {
AlertCircle,
ArrowRight,
@@ -22,9 +23,16 @@ import {
PackageCheck,
RefreshCw,
ShieldCheck,
XCircle,
} from "lucide-react";
import toast from "react-hot-toast";
import { Link } from "react-router-dom";
import { api } from "@/auth/http";
import { extractErrorMessage } from "@/utils/errorExtractor";
import { formatMoney } from "@/lib/format";
import { toDayString } from "@/hooks/useListControls";
import { useAuth } from "@/auth/useAuth";
import {
FREIGHT_PERMS,
@@ -146,6 +154,32 @@ export default function ContractClearanceDetailPage() {
!isDjiboutiGl(user);
const canResubmitBooking = bookingNeedsChanges && isGlBookingOwner;
const canRebook = bookingExpired && isGlBookingOwner;
// The linked booking was CANCELLED with an open wagon-cancellation ledger row
// (consolidation partner lapsed unpaid, staff cut). FEE_PENDING: the customer
// must settle the cancellation-fee invoice first; CREDIT_AVAILABLE: the paid
// freight is credit and GL rebooks it from here (new booking under this
// contract, marked PAID — it re-enters consolidation pairing if odd 20ft).
const cancellation =
clearance?.linkedBookingStatus === "CANCELLED"
? clearance?.linkedBookingCancellation
: null;
const canRebookCredit =
cancellation?.status === "CREDIT_AVAILABLE" &&
hasPermission(user, FREIGHT_PERMS.bookings.wagonCancellationRebook);
const [creditRebookDate, setCreditRebookDate] = useState<Date | null>(null);
const creditRebook = useMutation({
mutationFn: () =>
api.post(`/bookings/wagon-cancellations/${cancellation!.id}/rebook`, {
scheduledDate: toDayString(creditRebookDate!),
}),
onSuccess: () => {
toast.success("Booking recreated from the credit and marked paid.");
void refetch();
void refetchContract();
},
onError: (err) =>
toast.error(extractErrorMessage(err, "Rebook failed")),
});
// Rebook completes the SAME expired booking (it already carries the price and
// cargo from its first completion) via the /complete endpoint's EXPIRED
// branch — routing it through create-booking instead would create a
@@ -252,7 +286,18 @@ export default function ContractClearanceDetailPage() {
Customs
</Badge>
) : null}
{bookingExpired ? (
{cancellation ? (
<Badge
variant="light"
color="red"
radius="sm"
leftSection={<XCircle size={13} />}
>
{cancellation.status === "FEE_PENDING"
? "Cancelled — fee pending"
: "Cancelled — rebook credit"}
</Badge>
) : bookingExpired ? (
<Badge
variant="light"
color="orange"
@@ -308,7 +353,59 @@ export default function ContractClearanceDetailPage() {
it can actually create the booking without checking the schedule board. */}
{id ? <GlUpcomingWindowsSection contractId={id} /> : null}
{bookingExpired ? (
{cancellation ? (
<Alert
color="red"
radius="md"
icon={<XCircle size={16} />}
title={`Booking ${clearance.linkedBookingReference ?? ""} cancelled — consolidation partner not paid`}
>
<Stack gap="sm" align="flex-start">
{cancellation.status === "FEE_PENDING" ? (
<Text size="sm">
The booking shared a wagon with a partner booking that was
never paid, so it could not board and was cancelled. Its paid
freight ({formatMoney(cancellation.creditAmount, cancellation.creditCurrency)}) is held as
rebooking credit. The customer must first pay the cancellation
fee from the portal Payments tab once it settles, rebook the
shipment here.
</Text>
) : (
<>
<Text size="sm">
The cancellation fee is settled. Pick the new shipment day
and rebook the booking is recreated under this contract
from the {formatMoney(cancellation.creditAmount, cancellation.creditCurrency)} credit and
marked paid (no new freight charge).
</Text>
{canRebookCredit ? (
<Group gap="sm" align="flex-end">
<DatePickerInput
label="New shipment day"
placeholder="Pick a day"
value={creditRebookDate}
onChange={(v) => setCreditRebookDate(v ? new Date(v) : null)}
minDate={new Date()}
w={220}
/>
<Button
color="grape"
radius="md"
size="sm"
leftSection={<RefreshCw size={15} />}
loading={creditRebook.isPending}
disabled={!creditRebookDate}
onClick={() => creditRebook.mutate()}
>
Rebook for customer
</Button>
</Group>
) : null}
</>
)}
</Stack>
</Alert>
) : bookingExpired ? (
<Alert
color="orange"
radius="md"

View File

@@ -56,6 +56,9 @@ export interface ShipmentPriceLine {
export interface ShipmentValidation {
overweightLines: Array<{
containerTypeCode: string;
/** Container number, or "<code> #2" when unnumbered. */
containerLabel: string;
/** This container's VGM — the limit is per container, never pooled. */
totalVgmTons: number;
maxAllowedTons: number;
excessTons: number;

View File

@@ -1024,8 +1024,9 @@ function PriceConfirmModal({
<Stack gap={6}>
{overweightLines.map((line, i) => (
<Text key={i} fz="sm" c="#9A5B00">
{line.containerTypeCode}: {line.totalVgmTons}t exceeds limit{" "}
{line.maxAllowedTons}t (+{line.excessTons}t overweight)
{line.containerLabel || line.containerTypeCode}:{" "}
{line.totalVgmTons}t exceeds limit {line.maxAllowedTons}t (+
{line.excessTons}t overweight)
</Text>
))}
<Text fz="xs" c="#9A5B00" mt={2}>

View File

@@ -644,8 +644,9 @@ function PriceConfirmModal({
<Stack gap={6}>
{overweightLines.map((line, i) => (
<Text key={i} fz="sm" c="#9A5B00">
{line.containerTypeCode}: {line.totalVgmTons}t exceeds limit{" "}
{line.maxAllowedTons}t (+{line.excessTons}t overweight)
{line.containerLabel || line.containerTypeCode}:{" "}
{line.totalVgmTons}t exceeds limit {line.maxAllowedTons}t (+
{line.excessTons}t overweight)
</Text>
))}
<Text fz="xs" c="#9A5B00" mt={2}>

View File

@@ -33,9 +33,12 @@ export interface SubmitContractResponse {
message?: string;
}
/** A container line whose total VGM exceeds the weight-limit rule. */
/** One physical container whose VGM exceeds the weight-limit rule. */
export interface OverweightLine {
containerTypeCode: string;
/** Container number, or "<code> #2" when unnumbered. */
containerLabel: string;
/** This container's VGM — the limit is per container, never pooled. */
totalVgmTons: number;
maxAllowedTons: number;
excessTons: number;

View File

@@ -70,6 +70,9 @@ export interface ShippingLinePriceQuote {
/** Containers over their type's weight limit — a surcharge, not a block. */
overweightLines: {
containerTypeCode: string;
/** Container number, or "<code> #2" when unnumbered. */
containerLabel: string;
/** This container's VGM — the limit is per container, never pooled. */
totalVgmTons: number;
maxAllowedTons: number;
excessTons: number;