mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
325 lines
9.1 KiB
TypeScript
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;
|
|
}
|