mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 04:15:43 +00:00
- Added and to for better visibility of GL-created shipment bookings. - Implemented method in to fetch the latest clearance phase for contracts, improving list responses. - Introduced property in the entity to store the latest clearance cycle's phase. - Updated to surface linked booking information in the clearance view. - Created component to display detailed container information in booking details. - Refactored booking actions to remove contract-related actions from the booking request page. - Enhanced the component to reflect the current phase of clearance actions. - Updated UI components to provide clearer messaging regarding the status of clearance and linked bookings. - Adjusted action handling in to include duty payment actions. - Improved the to show hints for each phase of the clearance process.
413 lines
12 KiB
TypeScript
413 lines
12 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 {
|
|
BookingApprovalStep,
|
|
BookingDetail,
|
|
BookingStatus,
|
|
} from "@/types/booking";
|
|
|
|
export type BookingActionId =
|
|
| "accept"
|
|
| "requestChanges"
|
|
| "reject"
|
|
| "approve"
|
|
| "rejectApproval"
|
|
| "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"
|
|
| "approvalSteps"
|
|
| "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)
|
|
);
|
|
}
|
|
|
|
export function getNextPendingApprovalStep(
|
|
steps?: BookingApprovalStep[] | null,
|
|
): BookingApprovalStep | undefined {
|
|
if (!steps?.length) return undefined;
|
|
return [...steps]
|
|
.sort((a, b) => a.stepOrder - b.stepOrder)
|
|
.find((s) => s.status === "PENDING");
|
|
}
|
|
|
|
function approvalActions(
|
|
steps?: BookingApprovalStep[] | null,
|
|
): BookingActionDef[] {
|
|
const next = getNextPendingApprovalStep(steps);
|
|
if (!next) return [];
|
|
return [
|
|
buildApproveActionForStep(next),
|
|
{
|
|
id: "rejectApproval",
|
|
label: "Reject approval",
|
|
shortLabel: "Reject",
|
|
description: "Reject at the current approval step",
|
|
confirmTitle: "Reject at approval step?",
|
|
confirmDescription:
|
|
"The booking will be marked rejected. This action cannot be undone from the UI.",
|
|
variant: "destructive",
|
|
icon: XCircle,
|
|
input: "reason",
|
|
inputLabel: "Rejection reason",
|
|
inputPlaceholder: "Explain why this booking is rejected…",
|
|
},
|
|
];
|
|
}
|
|
|
|
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,
|
|
rejectApproval: FREIGHT_PERMS.bookings.rejectApproval,
|
|
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.manage,
|
|
cancel: FREIGHT_PERMS.bookings.cancel,
|
|
};
|
|
|
|
const approvePermissionForRole = (role: string): string | undefined => {
|
|
if (role === "LINE_STAFF") return FREIGHT_PERMS.bookings.approveLineStaff;
|
|
if (role === "DIRECTOR") return FREIGHT_PERMS.bookings.approveDirector;
|
|
if (role === "CEO") return FREIGHT_PERMS.bookings.approveCeo;
|
|
return undefined;
|
|
};
|
|
|
|
/** True when this step is the current pending step and the user may approve it. */
|
|
export function canActOnApprovalStep(
|
|
user: AuthUser | null | undefined,
|
|
step: BookingApprovalStep,
|
|
steps?: BookingApprovalStep[] | null,
|
|
): boolean {
|
|
if (step.status !== "PENDING") return false;
|
|
const next = getNextPendingApprovalStep(steps);
|
|
if (!next || next.id !== step.id) return false;
|
|
if (isFreightApprovalAdmin(user)) return true;
|
|
const perm = approvePermissionForRole(step.requiredRole);
|
|
return perm ? hasPermission(user, perm) : false;
|
|
}
|
|
|
|
export function buildApproveActionForStep(
|
|
step: BookingApprovalStep,
|
|
): BookingActionDef {
|
|
return {
|
|
id: "approve",
|
|
label: `Approve (${step.requiredRole})`,
|
|
shortLabel: "Approve",
|
|
description: `Complete step ${step.stepOrder} as ${step.requiredRole}`,
|
|
confirmTitle: `Approve as ${step.requiredRole}?`,
|
|
confirmDescription:
|
|
"This records your approval and advances the booking to the next step in the chain.",
|
|
variant: "default",
|
|
icon: Check,
|
|
primary: true,
|
|
};
|
|
}
|
|
|
|
function filterActionsByUser(
|
|
actions: BookingActionDef[],
|
|
user: AuthUser | null | undefined,
|
|
approvalSteps?: BookingApprovalStep[] | null,
|
|
): BookingActionDef[] {
|
|
if (!user) return [];
|
|
const next = getNextPendingApprovalStep(approvalSteps);
|
|
return actions.filter((action) => {
|
|
if (action.id === "approve" && next) {
|
|
return canActOnApprovalStep(user, next, approvalSteps);
|
|
}
|
|
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, approvalSteps } = ctx;
|
|
|
|
let actions: BookingActionDef[];
|
|
|
|
switch (status) {
|
|
case "SUBMITTED":
|
|
actions = withCancel(SUBMITTED_ACTIONS);
|
|
break;
|
|
case "PENDING_APPROVAL":
|
|
case "APPROVED_PENDING_SIGNATURE":
|
|
actions = withCancel(approvalActions(approvalSteps));
|
|
break;
|
|
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, approvalSteps);
|
|
}
|
|
|
|
/** 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;
|
|
approvalSteps?: BookingApprovalStep[] | null;
|
|
customsClearingEnabled?: boolean;
|
|
},
|
|
user?: AuthUser | null,
|
|
): boolean {
|
|
const actions = getBookingActions(
|
|
{
|
|
status: row.status,
|
|
paymentCurrency: row.paymentCurrency,
|
|
reference: "",
|
|
approvalSteps: row.approvalSteps ?? undefined,
|
|
schedulingStatus: row.status,
|
|
customsClearingEnabled: row.customsClearingEnabled,
|
|
},
|
|
user,
|
|
);
|
|
return actions.length > 0;
|
|
}
|