mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 15:18:11 +00:00
contrat nad booking modification
This commit is contained in:
@@ -1,79 +0,0 @@
|
||||
import type { BookingApprovalStep, BookingStatus } from "@/types/booking";
|
||||
import { getNextPendingApprovalStep } from "@/features/bookings/booking-actions.config";
|
||||
|
||||
export interface ApprovalProgressSummary {
|
||||
label: string;
|
||||
detail: string;
|
||||
complete: boolean;
|
||||
}
|
||||
|
||||
/** Compact approval chain summary for list rows and badges. */
|
||||
export function formatApprovalProgress(
|
||||
status: BookingStatus | string,
|
||||
steps?: BookingApprovalStep[] | null,
|
||||
): ApprovalProgressSummary {
|
||||
const sorted = [...(steps ?? [])].sort((a, b) => a.stepOrder - b.stepOrder);
|
||||
|
||||
if (sorted.length === 0) {
|
||||
if (status === "SUBMITTED") {
|
||||
return {
|
||||
label: "Awaiting accept",
|
||||
detail: "Staff must accept intake",
|
||||
complete: false,
|
||||
};
|
||||
}
|
||||
if (
|
||||
status === "PENDING_APPROVAL" ||
|
||||
status === "APPROVED_PENDING_SIGNATURE"
|
||||
) {
|
||||
return {
|
||||
label: "No steps",
|
||||
detail: "Approval chain not started",
|
||||
complete: false,
|
||||
};
|
||||
}
|
||||
if (
|
||||
[
|
||||
"APPROVED",
|
||||
"CONTRACT_READY",
|
||||
"SIGNED_CUSTOMER",
|
||||
"FULLY_EXECUTED",
|
||||
"PAID",
|
||||
"COMPLETED",
|
||||
].includes(status)
|
||||
) {
|
||||
return {
|
||||
label: "Approved",
|
||||
detail: "Internal approval complete",
|
||||
complete: true,
|
||||
};
|
||||
}
|
||||
return { label: "—", detail: "", complete: false };
|
||||
}
|
||||
|
||||
const approved = sorted.filter((s) => s.status === "APPROVED").length;
|
||||
const total = sorted.length;
|
||||
const next = getNextPendingApprovalStep(sorted);
|
||||
|
||||
if (!next && approved === total) {
|
||||
return {
|
||||
label: `${approved}/${total} done`,
|
||||
detail: sorted.map((s) => `${s.requiredRole} ✓`).join(" · "),
|
||||
complete: true,
|
||||
};
|
||||
}
|
||||
|
||||
if (next) {
|
||||
return {
|
||||
label: `${approved}/${total}`,
|
||||
detail: `Next: ${next.requiredRole} (step ${next.stepOrder})`,
|
||||
complete: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: `${approved}/${total}`,
|
||||
detail: sorted.map((s) => `${s.requiredRole}: ${s.status}`).join(" · "),
|
||||
complete: approved === total,
|
||||
};
|
||||
}
|
||||
@@ -14,18 +14,12 @@ import {
|
||||
hasPermission,
|
||||
isFreightApprovalAdmin,
|
||||
} from "@/lib/permissions";
|
||||
import type {
|
||||
BookingApprovalStep,
|
||||
BookingDetail,
|
||||
BookingStatus,
|
||||
} from "@/types/booking";
|
||||
import type { BookingDetail, BookingStatus } from "@/types/booking";
|
||||
|
||||
export type BookingActionId =
|
||||
| "accept"
|
||||
| "requestChanges"
|
||||
| "reject"
|
||||
| "approve"
|
||||
| "rejectApproval"
|
||||
| "viewContract"
|
||||
| "signContractStaff"
|
||||
| "reviewClearance"
|
||||
@@ -62,7 +56,6 @@ export type BookingActionContext = Pick<
|
||||
BookingDetail,
|
||||
| "status"
|
||||
| "paymentCurrency"
|
||||
| "approvalSteps"
|
||||
| "reference"
|
||||
| "schedulingStatus"
|
||||
| "customsClearingEnabled"
|
||||
@@ -86,39 +79,6 @@ export function canAllocateBooking(
|
||||
);
|
||||
}
|
||||
|
||||
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",
|
||||
@@ -232,7 +192,6 @@ 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,
|
||||
@@ -244,55 +203,12 @@ const ACTION_PERMISSION: Partial<Record<BookingActionId, string>> = {
|
||||
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;
|
||||
});
|
||||
@@ -303,7 +219,7 @@ export function getBookingActions(
|
||||
ctx: BookingActionContext,
|
||||
user?: AuthUser | null,
|
||||
): BookingActionDef[] {
|
||||
const { status, approvalSteps } = ctx;
|
||||
const { status } = ctx;
|
||||
|
||||
let actions: BookingActionDef[];
|
||||
|
||||
@@ -313,8 +229,6 @@ export function getBookingActions(
|
||||
break;
|
||||
case "PENDING_APPROVAL":
|
||||
case "APPROVED_PENDING_SIGNATURE":
|
||||
actions = withCancel(approvalActions(approvalSteps));
|
||||
break;
|
||||
case "APPROVED":
|
||||
actions = [CANCEL_ACTION];
|
||||
break;
|
||||
@@ -370,7 +284,7 @@ export function getBookingActions(
|
||||
}
|
||||
|
||||
if (user === undefined) return actions;
|
||||
return filterActionsByUser(actions, user, approvalSteps);
|
||||
return filterActionsByUser(actions, user);
|
||||
}
|
||||
|
||||
/** Opens contract page without confirmation dialog. */
|
||||
@@ -392,7 +306,6 @@ export function listRowHasActions(
|
||||
row: {
|
||||
status: BookingStatus;
|
||||
paymentCurrency: string;
|
||||
approvalSteps?: BookingApprovalStep[] | null;
|
||||
customsClearingEnabled?: boolean;
|
||||
},
|
||||
user?: AuthUser | null,
|
||||
@@ -402,7 +315,6 @@ export function listRowHasActions(
|
||||
status: row.status,
|
||||
paymentCurrency: row.paymentCurrency,
|
||||
reference: "",
|
||||
approvalSteps: row.approvalSteps ?? undefined,
|
||||
schedulingStatus: row.status,
|
||||
customsClearingEnabled: row.customsClearingEnabled,
|
||||
},
|
||||
|
||||
@@ -20,7 +20,6 @@ export function toBookingListRow(booking: BookingDetail): BookingListRow {
|
||||
reference: booking.reference,
|
||||
contractReference: booking.contractReference ?? null,
|
||||
contractId: booking.contractId ?? null,
|
||||
approvalSteps: booking.approvalSteps,
|
||||
customerLabel: booking.isGovernment
|
||||
? (booking.governmentInstitution ?? "Government")
|
||||
: labelFromRef(booking.company, booking.companyId ?? undefined),
|
||||
@@ -46,6 +45,8 @@ export function toBookingListRow(booking: BookingDetail): BookingListRow {
|
||||
consolidationPartnerId: booking.consolidationPartnerId ?? null,
|
||||
consolidationPartnerReference: booking.consolidationPartner?.reference ?? null,
|
||||
customsClearingEnabled: booking.customsClearingEnabled ?? false,
|
||||
bookingKind:
|
||||
booking.contractKind === "GENERAL" ? "GENERAL_CONTRACT" : "ONE_TIME",
|
||||
createdAt: booking.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user