Files
edr-platform/apps/edr-freight-web/backoffice/src/features/bookings/booking-actions.config.ts
2026-07-23 20:24:20 +00:00

325 lines
9.1 KiB
TypeScript

import type { LucideIcon } from "lucide-react";
import {
Ban,
Check,
MessageSquareWarning,
Play,
ShieldCheck,
XCircle,
} from "lucide-react";
import type { AuthUser } from "@/auth/types";
import {
FREIGHT_PERMS,
hasPermission,
isFreightApprovalAdmin,
} from "@/lib/permissions";
import type { BookingDetail, BookingStatus } from "@/types/booking";
export type BookingActionId =
| "accept"
| "requestChanges"
| "reject"
| "viewContract"
| "signContractStaff"
| "reviewClearance"
| "allocateBooking"
| "startTransit"
| "complete"
| "operationAccept"
| "operationRequestChanges"
| "cancel";
export type BookingActionInputKind =
| "note"
| "reason"
| "file"
| "days"
| "amount";
export interface BookingActionDef {
id: BookingActionId;
label: string;
shortLabel: string;
description: string;
confirmTitle: string;
confirmDescription: string;
variant: "default" | "destructive" | "outline";
icon: LucideIcon;
input?: BookingActionInputKind;
inputLabel?: string;
inputPlaceholder?: string;
primary?: boolean;
}
export type BookingActionContext = Pick<
BookingDetail,
| "status"
| "paymentCurrency"
| "reference"
| "schedulingStatus"
| "customsClearingEnabled"
>;
const ALLOCATABLE_SCHEDULING_STATUSES = new Set([
"NOT_SCHEDULED",
"HOLDING",
"ELIGIBLE",
undefined,
null,
"",
]);
export function canAllocateBooking(
booking: Pick<BookingDetail, "status" | "schedulingStatus">,
) {
return (
booking.status === "PAID" &&
ALLOCATABLE_SCHEDULING_STATUSES.has(booking.schedulingStatus ?? undefined)
);
}
const SUBMITTED_ACTIONS: BookingActionDef[] = [
{
id: "accept",
label: "Accept for approval",
shortLabel: "Accept",
description: "Start the formal approval chain",
confirmTitle: "Accept submission?",
confirmDescription:
"Set how long the contract stays valid, then the booking moves to pending approval and approval steps are created from the rule engine.",
variant: "default",
icon: ShieldCheck,
primary: true,
input: "days",
inputLabel: "Contract validity (days)",
inputPlaceholder: "e.g. 30",
},
{
id: "requestChanges",
label: "Request changes",
shortLabel: "Changes",
description: "Ask the customer to update and resubmit",
confirmTitle: "Request changes from customer?",
confirmDescription:
"The customer will see your note and can edit the booking before resubmitting.",
variant: "outline",
icon: MessageSquareWarning,
input: "note",
inputLabel: "Message to customer",
inputPlaceholder: "Describe what needs to be corrected or added…",
},
{
id: "reject",
label: "Reject booking",
shortLabel: "Reject",
description: "Reject this submission",
confirmTitle: "Reject booking?",
confirmDescription:
"The booking will be marked rejected and removed from active queues.",
variant: "destructive",
icon: Ban,
input: "reason",
inputLabel: "Rejection reason",
inputPlaceholder: "Reason for rejection…",
},
];
// Marketing/operations review of a drawdown order's operation request.
const OPERATION_REVIEW_ACTIONS: BookingActionDef[] = [
{
id: "operationAccept",
label: "Accept operation",
shortLabel: "Accept",
description: "Accept the operation request and release it for dispatch",
confirmTitle: "Accept operation request?",
confirmDescription:
"Train orders enter the batch pool; road orders move to truck dispatch.",
variant: "default",
icon: Check,
primary: true,
},
{
id: "operationRequestChanges",
label: "Request changes",
shortLabel: "Changes",
description: "Ask the customer to adjust the operation request",
confirmTitle: "Request changes to the operation?",
confirmDescription:
"The customer will see your note and can adjust and resubmit the order.",
variant: "outline",
icon: MessageSquareWarning,
input: "note",
inputLabel: "Message to customer",
inputPlaceholder: "Describe what needs to change…",
},
];
const CANCEL_ACTION: BookingActionDef = {
id: "cancel",
label: "Cancel booking",
shortLabel: "Cancel",
description: "Cancel this booking",
confirmTitle: "Cancel booking?",
confirmDescription:
"The booking will be marked cancelled. Provide a reason for the audit trail.",
variant: "destructive",
icon: Ban,
input: "reason",
inputLabel: "Cancellation reason",
inputPlaceholder: "Reason for cancellation…",
};
// Opens the booking detail straight on the Clearance tab so Marketing can
// review the customer's clearance documents (non-customs bookings only).
const REVIEW_CLEARANCE_ACTION: BookingActionDef = {
id: "reviewClearance",
label: "Review clearance",
shortLabel: "Clearance",
description: "Approve or query the customer's clearance documents",
confirmTitle: "",
confirmDescription: "",
variant: "default",
icon: ShieldCheck,
primary: true,
};
function withCancel(actions: BookingActionDef[]): BookingActionDef[] {
return [...actions, CANCEL_ACTION];
}
const ACTION_PERMISSION: Partial<Record<BookingActionId, string>> = {
accept: FREIGHT_PERMS.bookings.staffAccept,
requestChanges: FREIGHT_PERMS.bookings.requestChanges,
reject: FREIGHT_PERMS.bookings.reject,
viewContract: FREIGHT_PERMS.bookings.view,
signContractStaff: FREIGHT_PERMS.bookings.signStaff,
reviewClearance: FREIGHT_PERMS.bookings.reviewDocuments,
startTransit: FREIGHT_PERMS.bookings.operations,
complete: FREIGHT_PERMS.bookings.operations,
operationAccept: FREIGHT_PERMS.bookings.operations,
operationRequestChanges: FREIGHT_PERMS.bookings.operations,
allocateBooking: FREIGHT_PERMS.trainScheduling.update,
cancel: FREIGHT_PERMS.bookings.cancel,
};
function filterActionsByUser(
actions: BookingActionDef[],
user: AuthUser | null | undefined,
): BookingActionDef[] {
if (!user) return [];
return actions.filter((action) => {
const perm = ACTION_PERMISSION[action.id];
return perm ? hasPermission(user, perm) : true;
});
}
/** Actions available for the current booking status (detail or list). */
export function getBookingActions(
ctx: BookingActionContext,
user?: AuthUser | null,
): BookingActionDef[] {
const { status } = ctx;
let actions: BookingActionDef[];
switch (status) {
case "SUBMITTED":
actions = withCancel(SUBMITTED_ACTIONS);
break;
case "PENDING_APPROVAL":
case "APPROVED_PENDING_SIGNATURE":
case "APPROVED":
actions = [CANCEL_ACTION];
break;
case "CONTRACT_READY":
case "SIGNED_CUSTOMER":
case "FULLY_EXECUTED":
// Contract view/sign/executed buttons intentionally removed from the
// booking-request page.
actions = [];
break;
case "AWAITING_DOCUMENTS":
case "DOCUMENTS_UNDER_REVIEW":
// Marketing reviews non-customs clearance here; customs bookings are
// handled in the Global Logistics clearance queue, not the booking list.
actions = ctx.customsClearingEnabled
? [CANCEL_ACTION]
: withCancel([REVIEW_CLEARANCE_ACTION]);
break;
case "OPERATION_REQUEST_PENDING":
actions = withCancel(OPERATION_REVIEW_ACTIONS);
break;
case "PAID":
// Allocate is handled by the Operations "Ready to allocate" queue, not the
// per-booking action menu. Start transit was removed entirely. No per-row
// action remains in the PAID state.
actions = [];
break;
case "IN_TRANSIT":
actions = [
{
id: "complete",
label: "Complete booking",
shortLabel: "Complete",
description: "Mark journey finished",
confirmTitle: "Complete booking?",
confirmDescription:
"Marks the booking as completed. No further staff transitions apply.",
variant: "default",
icon: Play,
primary: true,
},
];
break;
case "CHANGES_REQUESTED":
actions = [CANCEL_ACTION];
break;
case "PENDING_CONSOLIDATION":
// View-only while waiting for a consolidation partner; cancel still allowed.
actions = [CANCEL_ACTION];
break;
default:
actions = [];
}
if (user === undefined) return actions;
return filterActionsByUser(actions, user);
}
/** Opens contract page without confirmation dialog. */
export function isContractNavAction(id: BookingActionId): boolean {
return id === "viewContract" || id === "signContractStaff";
}
/** Opens allocation wizard without confirmation dialog. */
export function isAllocateAction(id: BookingActionId): boolean {
return id === "allocateBooking";
}
/** Opens the booking detail on the Clearance tab without a confirm dialog. */
export function isClearanceNavAction(id: BookingActionId): boolean {
return id === "reviewClearance";
}
export function listRowHasActions(
row: {
status: BookingStatus;
paymentCurrency: string;
customsClearingEnabled?: boolean;
},
user?: AuthUser | null,
): boolean {
const actions = getBookingActions(
{
status: row.status,
paymentCurrency: row.paymentCurrency,
reference: "",
schedulingStatus: row.status,
customsClearingEnabled: row.customsClearingEnabled,
},
user,
);
return actions.length > 0;
}