Files
edr-platform/apps/edr-freight-web/portal/src/components/customer-actions/deriveContractCustomerAction.ts
2026-07-10 17:57:27 +00:00

266 lines
6.5 KiB
TypeScript

import type { Freight } from "@edr/types";
import type { LucideIcon } from "lucide-react";
import {
CreditCard,
Eye,
FileSignature,
Hourglass,
PackagePlus,
PencilLine,
Receipt,
RotateCcw,
Upload,
} from "lucide-react";
import { getContractBookingAction } from "@/pages/contracts/contract-booking-action";
const CLEARANCE_IN_PROGRESS_STATUSES = [
"AWAITING_CLEARANCE_DOCUMENTS",
"CLEARANCE_UNDER_REVIEW",
];
export function contractNeedsClearanceAction(c: Freight.IContract): {
show: boolean;
urgent: boolean;
} {
const clearance = (c as { clearanceStatus?: string }).clearanceStatus;
const ready =
clearance === "CLEARANCE_READY_FOR_BOOKING" ||
clearance === "SELF_CLEARED" ||
clearance === "ACTIVE_SHIPMENT_IN_PROGRESS";
if (ready) return { show: false, urgent: false };
const awaiting =
c.status === "AWAITING_CLEARANCE_DOCUMENTS" ||
clearance === "AWAITING_DOCUMENTS";
const inProgress =
CLEARANCE_IN_PROGRESS_STATUSES.includes(c.status) ||
clearance === "AWAITING_DOCUMENTS" ||
clearance === "DOCUMENTS_UNDER_REVIEW";
return { show: inProgress, urgent: awaiting };
}
export type ContractCustomerAction =
| {
type: "sign";
label: string;
to: string;
primary: boolean;
icon: LucideIcon;
}
| {
type: "navigate";
label: string;
to: string;
primary: boolean;
icon: LucideIcon;
}
| {
type: "clearance";
contractId: string;
label: string;
primary: boolean;
icon: LucideIcon;
urgent: boolean;
/** True when it's GL's turn — render calm/informational, not a call to action. */
waiting?: boolean;
}
| {
type: "pay";
booking: Freight.IBooking;
label: string;
primary: boolean;
icon: LucideIcon;
}
| {
/** One-click bare booking instance (GENERAL non-customs) — mutation, not navigation. */
type: "initiate";
contract: Freight.IContract;
label: string;
primary: boolean;
icon: LucideIcon;
};
function findPayableBookingForContract(
contractId: string,
bookings: Freight.IBooking[],
): Freight.IBooking | null {
return (
bookings.find((b) => {
if (b.contractId !== contractId) return false;
if (b.paymentStatus === "PAID") return false;
const isGeneral = b.bookingType === "GENERAL_CONTRACT";
return isGeneral
? b.status === "FULLY_EXECUTED"
: b.status === "SELECTED_FOR_BATCH";
}) ?? null
);
}
/** Single best customer action for a contract row (list / home). */
export function deriveContractCustomerAction(
contract: Freight.IContract,
bookings: Freight.IBooking[],
): ContractCustomerAction {
const id = contract.id;
if (contract.status === "CONTRACT_READY") {
return {
type: "sign",
label: "View & sign",
to: `/contracts/${id}/view`,
primary: true,
icon: FileSignature,
};
}
if (contract.status === "CHANGES_REQUESTED") {
return {
type: "navigate",
label: "Edit & resubmit",
to: `/contracts/${id}`,
primary: true,
icon: PencilLine,
};
}
// Saved-but-not-submitted contract — send the customer back into the wizard to
// finish editing and submit it for review.
if (contract.status === "DRAFT" || contract.status === "RENEWAL_DRAFT") {
return {
type: "navigate",
label: "Continue draft",
to: `/contracts/${id}/edit`,
primary: true,
icon: PencilLine,
};
}
const payable = findPayableBookingForContract(id, bookings);
if (payable) {
return {
type: "pay",
booking: payable,
label: "Pay now",
primary: true,
icon: CreditCard,
};
}
const clr = contractNeedsClearanceAction(contract);
if (clr.show) {
// Refine the generic clearance action by the persisted clearance phase so
// the button says what the customer actually has to do right now (e.g.
// "Pay duty & upload slip" during CUSTOMER_DUTY, not "Update clearance").
const phase = contract.clearancePhase ?? null;
switch (phase) {
case "CUSTOMER_INTAKE":
return {
type: "clearance",
contractId: id,
label: "Upload clearance documents",
primary: true,
icon: Upload,
urgent: true,
};
case "CUSTOMER_DUTY":
return {
type: "clearance",
contractId: id,
label: "Pay duty & upload slip",
primary: true,
icon: Receipt,
urgent: true,
};
case "GL_ET_REVIEW":
case "GL_DJ_COLLECTION":
case "GL_ET_OUTPUT":
case "GL_ET_POST_CLEARANCE":
case "GL_DJ_LOADING":
case "POST_TRANSIT":
// GL's turn — nothing for the customer to do; show a calm status.
return {
type: "clearance",
contractId: id,
label: "Clearance in progress",
primary: false,
icon: Hourglass,
urgent: false,
waiting: true,
};
default:
// No persisted phase (legacy / early cycles) — keep the status-derived label.
return {
type: "clearance",
contractId: id,
label: clr.urgent ? "Upload clearance" : "Update clearance",
primary: true,
icon: Upload,
urgent: clr.urgent,
};
}
}
if (
contract.customsClearingEnabled &&
["CLEARANCE_READY_FOR_BOOKING", "ACTIVE_SHIPMENT_IN_PROGRESS"].includes(
contract.status,
)
) {
return {
type: "navigate",
label: "View",
to: `/contracts/${id}`,
primary: false,
icon: Eye,
};
}
const bookingAction = getContractBookingAction(contract, bookings);
if (bookingAction.kind === "initiate") {
return {
type: "initiate",
contract,
label: "Initiate booking",
primary: true,
icon: PackagePlus,
};
}
if (bookingAction.kind === "book") {
return {
type: "navigate",
label: "Book shipment",
to: bookingAction.to,
primary: true,
icon: PackagePlus,
};
}
if (bookingAction.kind === "rebook") {
return {
type: "navigate",
label: "Re-book shipment",
to: bookingAction.to,
primary: true,
icon: RotateCcw,
};
}
if (bookingAction.kind === "request") {
return {
type: "navigate",
label: "Request shipment",
to: bookingAction.to,
primary: true,
icon: PackagePlus,
};
}
return {
type: "navigate",
label: "View",
to: `/contracts/${id}`,
primary: false,
icon: Eye,
};
}