mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 09:35:44 +00:00
implement Global Logistics booking process for customs contracts and enhance contract document handling
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
Alert,
|
||||
@@ -21,7 +20,6 @@ import {
|
||||
Download,
|
||||
Eye,
|
||||
FileText,
|
||||
PackagePlus,
|
||||
Plus,
|
||||
Upload,
|
||||
} from "lucide-react";
|
||||
@@ -87,7 +85,6 @@ export function ContractClearancePanel({
|
||||
bare,
|
||||
}: ContractClearancePanelProps) {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
const [pending, setPending] = useState<Record<string, File>>({});
|
||||
const [adHoc, setAdHoc] = useState<AdHocDoc[]>([]);
|
||||
const { view, viewer } = useFileViewer();
|
||||
@@ -195,8 +192,9 @@ export function ContractClearancePanel({
|
||||
)}
|
||||
{isReady ? (
|
||||
<Alert color="teal" radius="md" icon={<CheckCircle2 size={18} />} mb="md">
|
||||
Your clearance documents are approved. You can now create a shipment
|
||||
booking under this contract.
|
||||
{customsPath
|
||||
? "Your clearance documents are approved. Global Logistics will create your booking on your behalf — you will be notified when payment is due."
|
||||
: "Your clearance documents are approved. You can now create a shipment booking under this contract."}
|
||||
</Alert>
|
||||
) : isUnderReview ? (
|
||||
<Alert color="blue" radius="md" icon={<Clock size={18} />} mb="md">
|
||||
@@ -443,19 +441,6 @@ export function ContractClearancePanel({
|
||||
</Group>
|
||||
)}
|
||||
|
||||
{/* Clearance finalized → the customer creates the booking himself. */}
|
||||
{isReady && status === "CLEARANCE_READY_FOR_BOOKING" && (
|
||||
<Group justify="flex-end" mt="lg">
|
||||
<Button
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
leftSection={<PackagePlus size={16} />}
|
||||
onClick={() => navigate(`/contracts/${contractId}/bookings/new`)}
|
||||
>
|
||||
Create booking
|
||||
</Button>
|
||||
</Group>
|
||||
)}
|
||||
{viewer}
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -205,19 +205,20 @@ export default function ContractDetailPage() {
|
||||
|
||||
const canSign = contract.status === "CONTRACT_READY";
|
||||
const customsPath = contract.customsClearingEnabled;
|
||||
// The customer creates the booking himself on BOTH paths:
|
||||
// - Path A (no customs): once the contract is executed after self-clearance.
|
||||
// - Path B (customs): once GL finalizes the pre-booking clearance
|
||||
// (CLEARANCE_READY_FOR_BOOKING). GL "create booking" was removed.
|
||||
// Only the NON-customs (Path A) customer books himself — once the contract is
|
||||
// executed after self-clearance. Customs (Path B) bookings are created by
|
||||
// Global Logistics on the customer's behalf, so the customer gets no booking
|
||||
// button on a customs contract.
|
||||
const clearanceFinalized =
|
||||
contract.clearanceStatus === "CLEARANCE_READY_FOR_BOOKING" ||
|
||||
contract.status === "CLEARANCE_READY_FOR_BOOKING";
|
||||
const canBookShipment = customsPath
|
||||
? clearanceFinalized
|
||||
: PATH_A_BOOKABLE.includes(contract.status);
|
||||
// The customer uploads clearance documents while in a clearance status — but
|
||||
// once clearance is finalized the upload step is done; the action becomes
|
||||
// "create booking" instead.
|
||||
const canBookShipment =
|
||||
!customsPath && PATH_A_BOOKABLE.includes(contract.status);
|
||||
// Customs + clearance finalized: GL is preparing the booking — surface a
|
||||
// status notice instead of any action.
|
||||
const glPreparingBooking = customsPath && clearanceFinalized;
|
||||
// The customer uploads clearance documents while in a clearance status, until
|
||||
// clearance is finalized.
|
||||
const canUploadClearance =
|
||||
CLEARANCE_UPLOAD_STATUSES.includes(contract.status) && !clearanceFinalized;
|
||||
|
||||
@@ -292,6 +293,17 @@ export default function ContractDetailPage() {
|
||||
New shipment booking
|
||||
</Button>
|
||||
)}
|
||||
{glPreparingBooking && (
|
||||
<Badge
|
||||
size="lg"
|
||||
radius="md"
|
||||
variant="light"
|
||||
color="teal"
|
||||
leftSection={<CheckCircle2 size={14} />}
|
||||
>
|
||||
Global Logistics is creating your booking
|
||||
</Badge>
|
||||
)}
|
||||
{canUploadClearance && (
|
||||
<Button
|
||||
color="edr-green"
|
||||
|
||||
@@ -108,6 +108,21 @@ function getCustomerRowAction(
|
||||
icon: Upload,
|
||||
};
|
||||
}
|
||||
// Customs (Path B), clearance finalized: Global Logistics creates the booking
|
||||
// on the customer's behalf — the customer only views the contract.
|
||||
if (
|
||||
contract.customsClearingEnabled &&
|
||||
["CLEARANCE_READY_FOR_BOOKING", "ACTIVE_SHIPMENT_IN_PROGRESS"].includes(
|
||||
contract.status,
|
||||
)
|
||||
) {
|
||||
return {
|
||||
label: "View",
|
||||
to: `/contracts/${id}`,
|
||||
primary: false,
|
||||
icon: Eye,
|
||||
};
|
||||
}
|
||||
const booking = getContractBookingAction(contract, bookings);
|
||||
if (booking.kind === "book") {
|
||||
return {
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
Send,
|
||||
XCircle,
|
||||
} from "lucide-react";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Navigate, useLocation, useNavigate } from "react-router-dom";
|
||||
import useAuth from "@/hooks/useAuth";
|
||||
@@ -57,7 +57,6 @@ import {
|
||||
Step3CargoScope,
|
||||
Step4Route,
|
||||
Step8Review,
|
||||
StepDocuments,
|
||||
} from "./new-contract-form/steps";
|
||||
import { StepCard, StepHeader } from "./new-contract-form/shared";
|
||||
import { formatRateUnit } from "./new-contract-form/unit-rates";
|
||||
@@ -261,27 +260,11 @@ export default function NewContractPage() {
|
||||
return active?.licenseFiles ?? [];
|
||||
}, [auth.company, auth.activeCompanyProfileId]);
|
||||
|
||||
// The documents step validates required uploads imperatively (the requirement
|
||||
// set is async-loaded), so it registers a validator we call before advancing.
|
||||
const docsValidatorRef = useRef<(() => boolean) | null>(null);
|
||||
|
||||
async function handleContinue() {
|
||||
const valid = await form.trigger(contractStepFields[step], {
|
||||
shouldFocus: true,
|
||||
});
|
||||
if (!valid) {
|
||||
// TEMP DEBUG — surface which step-1 fields block Continue.
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn("[contract continue blocked] step", step, {
|
||||
errors: JSON.parse(JSON.stringify(form.formState.errors)),
|
||||
values: form.getValues(),
|
||||
});
|
||||
return;
|
||||
}
|
||||
// Step 2 — Documents: every required document must be on file or uploaded.
|
||||
if (step === 2 && docsValidatorRef.current && !docsValidatorRef.current()) {
|
||||
return;
|
||||
}
|
||||
if (!valid) return;
|
||||
goToStep(1);
|
||||
}
|
||||
|
||||
@@ -540,12 +523,8 @@ export default function NewContractPage() {
|
||||
)}
|
||||
|
||||
{/* Step 2 — Documents. */}
|
||||
{/* Step 2 — Review & Submit. */}
|
||||
{step === 2 && (
|
||||
<StepDocuments form={form} validatorRef={docsValidatorRef} />
|
||||
)}
|
||||
|
||||
{/* Step 3 — Review & Submit. */}
|
||||
{step === 3 && (
|
||||
<Step8Review
|
||||
form={form}
|
||||
setStep={setStep}
|
||||
|
||||
@@ -112,24 +112,21 @@ export default function NewShipmentPage() {
|
||||
);
|
||||
}
|
||||
|
||||
// Customs (Path B) contracts can only be booked once GL has finalized the
|
||||
// pre-booking clearance. Before that, send the customer to the clearance step.
|
||||
// (GL "create booking" was removed — the customer books once cleared.)
|
||||
const clearanceFinalized =
|
||||
contract.clearanceStatus === "CLEARANCE_READY_FOR_BOOKING" ||
|
||||
contract.status === "CLEARANCE_READY_FOR_BOOKING" ||
|
||||
contract.status === "ACTIVE_SHIPMENT_IN_PROGRESS";
|
||||
if (contract.customsClearingEnabled && !clearanceFinalized) {
|
||||
// Customs (Path B) contracts are booked by Global Logistics on behalf of the
|
||||
// customer — the customer never books one himself. Block the form entirely and
|
||||
// point back to the clearance workspace.
|
||||
if (contract.customsClearingEnabled) {
|
||||
return (
|
||||
<Box p="xl">
|
||||
<Alert color="orange" icon={<AlertCircle size={18} />} radius="md">
|
||||
<Alert color="blue" icon={<AlertCircle size={18} />} radius="md">
|
||||
<Text fw={700} mb="xs">
|
||||
Clearance not finalized yet
|
||||
Global Logistics handles bookings for this contract
|
||||
</Text>
|
||||
<Text size="sm" mb="md">
|
||||
This contract includes customs clearance. Upload your clearance
|
||||
documents — once Global Logistics finalizes the clearance you can
|
||||
create your shipment booking here.
|
||||
documents — once Global Logistics finalizes the clearance, they
|
||||
create the booking on your behalf and you will be notified when
|
||||
payment is due.
|
||||
</Text>
|
||||
<Button
|
||||
color="edr-green"
|
||||
|
||||
@@ -15,16 +15,6 @@ export const TERMINAL_BOOKING_STATUSES = [
|
||||
/** Path A statuses where a customer (no customs) may book against the contract. */
|
||||
const PATH_A_BOOKABLE = ["FULLY_EXECUTED", "CONTRACT_ACTIVE"];
|
||||
|
||||
/**
|
||||
* Path B (customs): the customer books once GL has finalized the pre-booking
|
||||
* clearance. ACTIVE_SHIPMENT_IN_PROGRESS is included so GENERAL contracts can
|
||||
* re-book after a prior shipment. (GL "create booking" was removed.)
|
||||
*/
|
||||
const PATH_B_BOOKABLE = [
|
||||
"CLEARANCE_READY_FOR_BOOKING",
|
||||
"ACTIVE_SHIPMENT_IN_PROGRESS",
|
||||
];
|
||||
|
||||
export type ContractBookingActionKind = "book" | "rebook" | "none";
|
||||
|
||||
export interface ContractBookingAction {
|
||||
@@ -45,10 +35,10 @@ export function getContractBookingAction(
|
||||
contract: Freight.IContract,
|
||||
bookings: Freight.IBooking[],
|
||||
): ContractBookingAction {
|
||||
const bookable = contract.customsClearingEnabled
|
||||
? PATH_B_BOOKABLE.includes(contract.status)
|
||||
: PATH_A_BOOKABLE.includes(contract.status);
|
||||
if (!bookable) return { kind: "none", to: "" };
|
||||
// Customs (Path B) contracts are booked by Global Logistics on behalf of the
|
||||
// customer — the customer never gets a Book button for them.
|
||||
if (contract.customsClearingEnabled) return { kind: "none", to: "" };
|
||||
if (!PATH_A_BOOKABLE.includes(contract.status)) return { kind: "none", to: "" };
|
||||
|
||||
const to = `/contracts/${contract.id}/bookings/new`;
|
||||
|
||||
|
||||
@@ -8,8 +8,7 @@ import * as z from "zod";
|
||||
export const CONTRACT_STEPS = [
|
||||
{ id: 0, label: "Setup", short: "Setup" },
|
||||
{ id: 1, label: "Cargo & Route", short: "Cargo & Route" },
|
||||
{ id: 2, label: "Documents", short: "Documents" },
|
||||
{ id: 3, label: "Review & Submit", short: "Review" },
|
||||
{ id: 2, label: "Review & Submit", short: "Review" },
|
||||
] as const;
|
||||
|
||||
export const OPERATION_TYPES = [
|
||||
@@ -331,8 +330,7 @@ export const contractStepFields: Record<
|
||||
"extraRoutes",
|
||||
"estimatedShipmentDate",
|
||||
],
|
||||
// Step 2 — Documents.
|
||||
2: ["documents"],
|
||||
// Step 3 — Review & Submit.
|
||||
3: ["notes"],
|
||||
// Step 2 — Review & Submit. (The separate Documents step was removed — the
|
||||
// company profile documents are attached to the contract automatically.)
|
||||
2: ["notes"],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user