mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
fix issue -> consolidation
This commit is contained in:
@@ -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}>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user