import type { AuthUser } from "@/auth/types"; import type { RuleEngineResourceSlug } from "@/types/rule-engine"; export const FREIGHT_PERMS = { overview: { view: "edr_freight_app:overview:view", }, support: { agentView: "edr_freight_app:support:agent_view", agentSend: "edr_freight_app:support:agent_send", }, reports: { view: "edr_freight_app:reports:view", }, procurement: { view: "edr_freight_app:procurement:view", vendorManage: "edr_freight_app:procurement:vendor_manage", acquisitionManage: "edr_freight_app:procurement:acquisition_manage", disposalManage: "edr_freight_app:procurement:disposal_manage", }, compliance: { view: "edr_freight_app:compliance:view", manage: "edr_freight_app:compliance:manage", }, facilities: { view: "edr_freight_app:facilities:view", manage: "edr_freight_app:facilities:manage", }, tradeAccess: { view: "edr_freight_app:trade_access:view", manage: "edr_freight_app:trade_access:manage", }, staffUsers: { view: "edr_freight_app:staff:users:view", }, chat: { view: "edr_freight_app:chat:view", sync: "edr_freight_app:chat:sync", }, bookings: { view: "edr_freight_app:bookings:view", create: "edr_freight_app:bookings:create", clearanceView: "edr_freight_app:bookings:clearance_view", staffAccept: "edr_freight_app:bookings:staff_accept", requestChanges: "edr_freight_app:bookings:request_changes", reject: "edr_freight_app:bookings:reject", approveLineStaff: "edr_freight_app:bookings:approve_line_staff", approveDirector: "edr_freight_app:bookings:approve_director", approveCeo: "edr_freight_app:bookings:approve_ceo", rejectApproval: "edr_freight_app:bookings:reject_approval", generateContract: "edr_freight_app:bookings:generate_contract", signStaff: "edr_freight_app:bookings:sign_staff", operations: "edr_freight_app:bookings:operations", cancel: "edr_freight_app:bookings:cancel", reviewDocuments: "edr_freight_app:bookings:review_documents", uploadClearanceOutput: "edr_freight_app:bookings:upload_clearance_output", finalizeClearance: "edr_freight_app:bookings:finalize_clearance", docReviewAlert: "edr_freight_app:bookings:doc_review_alert", governmentExpedite: "edr_freight_app:bookings:government_expedite", wagonCancellationView: "edr_freight_app:bookings:wagon_cancellation_view", wagonCancellationVoid: "edr_freight_app:bookings:wagon_cancellation_void", wagonCancellationRebook: "edr_freight_app:bookings:wagon_cancellation_rebook", approveConsolidation: "edr_freight_app:bookings:approve_consolidation", }, contracts: { view: "edr_freight_app:contracts:view", // Intake actions are split per freight type — mirror of the API registry. staffAccept: { bulk: "edr_freight_app:contracts:staff_accept:bulk", container: "edr_freight_app:contracts:staff_accept:container", }, requestChanges: { bulk: "edr_freight_app:contracts:request_changes:bulk", container: "edr_freight_app:contracts:request_changes:container", }, reject: { bulk: "edr_freight_app:contracts:reject:bulk", container: "edr_freight_app:contracts:reject:container", }, approveLineStaff: "edr_freight_app:contracts:approve_line_staff", approveDirector: "edr_freight_app:contracts:approve_director", approveCeo: "edr_freight_app:contracts:approve_ceo", hazardousApprovalOne: "edr_freight_app:contracts:hazardous_approval_one", hazardousApprovalTwo: "edr_freight_app:contracts:hazardous_approval_two", generateContract: "edr_freight_app:contracts:generate_contract", signStaff: { bulk: "edr_freight_app:contracts:sign_staff:bulk", container: "edr_freight_app:contracts:sign_staff:container", }, clearanceReview: "edr_freight_app:contracts:clearance_review", finalizeClearance: "edr_freight_app:contracts:finalize_clearance", createBooking: "edr_freight_app:contracts:create_booking", opsClearanceReview: "edr_freight_app:contracts:ops_clearance_review", clearanceDutyAdvise: "edr_freight_app:contracts:clearance_duty_advise", clearanceEtActions: "edr_freight_app:contracts:clearance_et_actions", clearanceDjActions: "edr_freight_app:contracts:clearance_dj_actions", suspend: "edr_freight_app:contracts:suspend", editDocument: "edr_freight_app:contracts:edit_document", finalInvoiceRaise: "edr_freight_app:contracts:final_invoice_raise", finalInvoiceConfirm: "edr_freight_app:contracts:final_invoice_confirm", }, trainScheduling: { view: "edr_freight_app:train_scheduling:view", create: "edr_freight_app:train_scheduling:create", update: "edr_freight_app:train_scheduling:update", /** Confirm cargo loaded/unloaded at a yard — import, export, and intercity alike. */ load: "edr_freight_app:train_scheduling:load", unload: "edr_freight_app:train_scheduling:unload", cancel: "edr_freight_app:train_scheduling:cancel", reschedule: "edr_freight_app:train_scheduling:reschedule", rulesManage: "edr_freight_app:train_scheduling:rules_manage", dispatch: "edr_freight_app:train_scheduling:dispatch", markPaid: "edr_freight_app:train_scheduling:mark_paid", expireBooking: "edr_freight_app:train_scheduling:expire_booking", editTrainNumber: "edr_freight_app:train_scheduling:edit_train_number", }, fleet: { view: "edr_freight_app:fleet:view", manage: "edr_freight_app:fleet:manage", }, admin: "edr_freight_app:admin", allocation: { manage: "edr_freight_app:allocation:manage", }, customers: { view: "edr_freight_app:customers:view", create: "edr_freight_app:customers:create", update: "edr_freight_app:customers:update", deactivate: "edr_freight_app:customers:deactivate", verify: "edr_freight_app:customers:verify", resetPassword: "edr_freight_app:customers:reset-password", }, shippingLines: { view: "edr_freight_app:shipping_lines:view", create: "edr_freight_app:shipping_lines:create", update: "edr_freight_app:shipping_lines:update", resetPassword: "edr_freight_app:shipping_lines:reset-password", }, shippingLineCredits: { view: "edr_freight_app:shipping_line_credits:view", invoice: "edr_freight_app:shipping_line_credits:invoice", cancel: "edr_freight_app:shipping_line_credits:cancel", invoiceMarkPaid: "edr_freight_app:shipping_line_credits:invoice_mark_paid", invoiceCancel: "edr_freight_app:shipping_line_credits:invoice_cancel", invoiceApprove: "edr_freight_app:shipping_line_credits:invoice_approve", invoiceReject: "edr_freight_app:shipping_line_credits:invoice_reject", }, payments: { view: "edr_freight_app:payments:view", }, invoices: { view: "edr_freight_app:invoices:view", export: "edr_freight_app:invoices:export", confirmOffline: "edr_freight_app:invoices:confirm_offline", // Filing with MoR EIMS. Off the general Finance role — automatic filing needs no permission // at all (the cron sweep runs as the system); these are the manual, exceptional-operations // actions, granted to the `chief` position (maker-checker, same as shipping-line credit // mark-paid/cancel approval) rather than every Finance user. eimsRegister: "edr_freight_app:invoices:eims_register", eimsResolve: "edr_freight_app:invoices:eims_resolve", eimsCancel: "edr_freight_app:invoices:eims_cancel", eimsReceiptRegister: "edr_freight_app:invoices:eims_receipt_register", // Issuing a credit/debit memo is filing-equivalent — same restricted grant as the eims_* keys. memoIssue: "edr_freight_app:invoices:memo_issue", }, firstMile: { view: "edr_freight_app:first_mile:view", accept: "edr_freight_app:first_mile:accept", create: "edr_freight_app:first_mile:create", update: "edr_freight_app:first_mile:update", delete: "edr_freight_app:first_mile:delete", assignVehicles: "edr_freight_app:first_mile:assign_vehicles", setDistances: "edr_freight_app:first_mile:set_distances", generateInvoice: "edr_freight_app:first_mile:generate_invoice", }, lastMile: { view: "edr_freight_app:last_mile:view", accept: "edr_freight_app:last_mile:accept", create: "edr_freight_app:last_mile:create", update: "edr_freight_app:last_mile:update", delete: "edr_freight_app:last_mile:delete", assignVehicles: "edr_freight_app:last_mile:assign_vehicles", setDistances: "edr_freight_app:last_mile:set_distances", generateInvoice: "edr_freight_app:last_mile:generate_invoice", requestView: "edr_freight_app:last_mile:request_view", requestReview: "edr_freight_app:last_mile:request_review", requestApprove: "edr_freight_app:last_mile:request_approve", }, locomotives: { view: "edr_freight_app:locomotives:view", create: "edr_freight_app:locomotives:create", update: "edr_freight_app:locomotives:update", delete: "edr_freight_app:locomotives:delete", /** Permanent purge — irreversible, granted separately from `delete`. */ hardDelete: "edr_freight_app:locomotives:hard_delete", }, wagons: { view: "edr_freight_app:wagons:view", create: "edr_freight_app:wagons:create", update: "edr_freight_app:wagons:update", delete: "edr_freight_app:wagons:delete", /** Permanent purge — irreversible, granted separately from `delete`. */ hardDelete: "edr_freight_app:wagons:hard_delete", transferRequest: "edr_freight_app:wagons:transfer_request", transferFulfill: "edr_freight_app:wagons:transfer_fulfill", transferHistoryAll: "edr_freight_app:wagons:transfer_history_all", /** Open the transfer desk. `wagons:view` is accepted as a fallback. */ transferView: "edr_freight_app:wagons:transfer_view", transferCancel: "edr_freight_app:wagons:transfer_cancel", transferCloseShort: "edr_freight_app:wagons:transfer_close_short", /** Maintenance ⇄ availability flip on the wagons desk (audited). */ statusToggle: "edr_freight_app:wagons:status_toggle", }, trains: { view: "edr_freight_app:trains:view", create: "edr_freight_app:trains:create", update: "edr_freight_app:trains:update", delete: "edr_freight_app:trains:delete", assignWagons: "edr_freight_app:trains:assign_wagons", /** Train-builder detail Actions menu — each item its own grant. */ changeLocomotives: "edr_freight_app:trains:change_locomotives", changeYard: "edr_freight_app:trains:change_yard", /** Move ONE coupled wagon to another yard from the Wagon order list. */ changeWagonYard: "edr_freight_app:trains:change_wagon_yard", toggleActive: "edr_freight_app:trains:toggle_active", disband: "edr_freight_app:trains:disband", }, routes: { view: "edr_freight_app:routes:view", create: "edr_freight_app:routes:create", update: "edr_freight_app:routes:update", delete: "edr_freight_app:routes:delete", /** Permanent purge — irreversible, granted separately from `delete`. */ hardDelete: "edr_freight_app:routes:hard_delete", }, containers: { view: "edr_freight_app:containers:view", create: "edr_freight_app:containers:create", update: "edr_freight_app:containers:update", delete: "edr_freight_app:containers:delete", }, cargoes: { view: "edr_freight_app:cargoes:view", create: "edr_freight_app:cargoes:create", update: "edr_freight_app:cargoes:update", delete: "edr_freight_app:cargoes:delete", }, vehicles: { view: "edr_freight_app:vehicles:view", create: "edr_freight_app:vehicles:create", update: "edr_freight_app:vehicles:update", delete: "edr_freight_app:vehicles:delete", }, drivers: { view: "edr_freight_app:drivers:view", create: "edr_freight_app:drivers:create", update: "edr_freight_app:drivers:update", delete: "edr_freight_app:drivers:delete", }, tracking: { view: "edr_freight_app:tracking:view", manage: "edr_freight_app:tracking:manage", }, fuel: { view: "edr_freight_app:fuel:view", create: "edr_freight_app:fuel:create", update: "edr_freight_app:fuel:update", delete: "edr_freight_app:fuel:delete", }, maintenance: { view: "edr_freight_app:maintenance:view", create: "edr_freight_app:maintenance:create", update: "edr_freight_app:maintenance:update", delete: "edr_freight_app:maintenance:delete", }, fleetReports: { view: "edr_freight_app:fleet_reports:view", export: "edr_freight_app:fleet_reports:export", }, fleetDashboard: { view: "edr_freight_app:fleet_dashboard:view", }, warehouseDashboard: { view: "edr_freight_app:warehouse_dashboard:view", }, warehouses: { view: "edr_freight_app:warehouses:view", create: "edr_freight_app:warehouses:create", update: "edr_freight_app:warehouses:update", delete: "edr_freight_app:warehouses:delete", }, warehouseYards: { view: "edr_freight_app:warehouse_yards:view", create: "edr_freight_app:warehouse_yards:create", update: "edr_freight_app:warehouse_yards:update", delete: "edr_freight_app:warehouse_yards:delete", }, warehouseZones: { view: "edr_freight_app:warehouse_zones:view", create: "edr_freight_app:warehouse_zones:create", update: "edr_freight_app:warehouse_zones:update", }, warehouseAllocationRules: { view: "edr_freight_app:warehouse_allocation_rules:view", create: "edr_freight_app:warehouse_allocation_rules:create", update: "edr_freight_app:warehouse_allocation_rules:update", delete: "edr_freight_app:warehouse_allocation_rules:delete", }, warehouseFeeRules: { view: "edr_freight_app:warehouse_fee_rules:view", create: "edr_freight_app:warehouse_fee_rules:create", update: "edr_freight_app:warehouse_fee_rules:update", delete: "edr_freight_app:warehouse_fee_rules:delete", }, warehouseInspectionReports: { view: "edr_freight_app:warehouse_inspection_reports:view", create: "edr_freight_app:warehouse_inspection_reports:create", update: "edr_freight_app:warehouse_inspection_reports:update", }, warehouseInventory: { view: "edr_freight_app:warehouse_inventory:view", receive: "edr_freight_app:warehouse_inventory:receive", move: "edr_freight_app:warehouse_inventory:move", load: "edr_freight_app:warehouse_inventory:load", unload: "edr_freight_app:warehouse_inventory:unload", dispatch: "edr_freight_app:warehouse_inventory:dispatch", gatePass: "edr_freight_app:warehouse_inventory:gate_pass", release: "edr_freight_app:warehouse_inventory:release", deliver: "edr_freight_app:warehouse_inventory:deliver", inspect: "edr_freight_app:warehouse_inventory:inspect", }, interchangeDocuments: { view: "edr_freight_app:interchange_documents:view", generate: "edr_freight_app:interchange_documents:generate", acknowledge: "edr_freight_app:interchange_documents:acknowledge", dispute: "edr_freight_app:interchange_documents:dispute", cancel: "edr_freight_app:interchange_documents:cancel", }, warehouseFeeInvoices: { view: "edr_freight_app:warehouse_fee_invoices:view", generate: "edr_freight_app:warehouse_fee_invoices:generate", cancel: "edr_freight_app:warehouse_fee_invoices:cancel", pay: "edr_freight_app:warehouse_fee_invoices:pay", }, additionalCharges: { view: "edr_freight_app:additional_charges:view", create: "edr_freight_app:additional_charges:create", send: "edr_freight_app:additional_charges:send", cancel: "edr_freight_app:additional_charges:cancel", }, /** * Audit trail. View-only — the API exposes no write routes for audit rows, * so there is no manage/delete counterpart to grant. */ auditLog: { view: "edr_freight_app:audit_log:view", }, settings: { fileUpload: { view: "edr_freight_app:settings:file_upload:view", manage: "edr_freight_app:settings:file_upload:manage", }, dropdown: { view: "edr_freight_app:settings:dropdown:view", manage: "edr_freight_app:settings:dropdown:manage", }, // The ONE company stamp/seal, applied to every generated document // (invoices, receipts, warehouse papers, the EDR side of contracts). stamp: { view: "edr_freight_app:settings:stamp:view", manage: "edr_freight_app:settings:stamp:manage", }, // The ONE company logo, applied to every generated document (invoices, // receipts, contracts, warehouse papers, train-scheduling manifests). logo: { view: "edr_freight_app:settings:logo:view", manage: "edr_freight_app:settings:logo:manage", }, // The per-officer approval teeter (ማህተም) + signature — genuinely per-person, // and NOT the company seal above. Retired: `invoiceStamp`, which used to // gate the company stamp before the two were untangled. teeter: { view: "edr_freight_app:settings:teeter:view", manage: "edr_freight_app:settings:teeter:manage", }, exchangeRate: { view: "edr_freight_app:settings:exchange_rate:view", manage: "edr_freight_app:settings:exchange_rate:manage", }, // Standard station stay, cycle and leg times, and the charged-tonnage // factors the operations reports measure actual performance against. operationsStandards: { view: "edr_freight_app:settings:operations_standards:view", manage: "edr_freight_app:settings:operations_standards:manage", }, // Whether Finance may settle invoices by hand, per currency. Finance holds // `view` (the worklist offers only enabled currencies); `manage` is admin. manualPayment: { view: "edr_freight_app:settings:manual_payment:view", manage: "edr_freight_app:settings:manual_payment:manage", }, contractTemplates: { view: "edr_freight_app:settings:contract_templates:view", manage: "edr_freight_app:settings:contract_templates:manage", create: "edr_freight_app:settings:contract_templates:create", update: "edr_freight_app:settings:contract_templates:update", delete: "edr_freight_app:settings:contract_templates:delete", read: "edr_freight_app:settings:contract_templates:read", }, // Portal-facing help/FAQ/legal copy, edited from Portal content. supportContent: { view: "edr_freight_app:settings:support_content:view", manage: "edr_freight_app:settings:support_content:manage", }, }, staff: { roles: { view: "edr_freight_app:staff:roles:view", create: "edr_freight_app:staff:roles:create", update: "edr_freight_app:staff:roles:update", delete: "edr_freight_app:staff:roles:delete", }, permissions: { view: "edr_freight_app:staff:permissions:view", assign: "edr_freight_app:staff:permissions:assign", }, employeeRegistration: { view: "edr_freight_app:employee_registration:view", create: "edr_freight_app:employee_registration:create", update: "edr_freight_app:employee_registration:update", activate: "edr_freight_app:employee_registration:activate", deactivate: "edr_freight_app:employee_registration:deactivate", }, roleAssignment: { view: "edr_freight_app:role_assignment:view", assign: "edr_freight_app:role_assignment:assign", replace: "edr_freight_app:role_assignment:replace", }, hierarchyUnits: { view: "edr_freight_app:hierarchy_units:view", create: "edr_freight_app:hierarchy_units:create", update: "edr_freight_app:hierarchy_units:update", delete: "edr_freight_app:hierarchy_units:delete", }, hierarchyPositions: { view: "edr_freight_app:hierarchy_positions:view", create: "edr_freight_app:hierarchy_positions:create", update: "edr_freight_app:hierarchy_positions:update", delete: "edr_freight_app:hierarchy_positions:delete", changeParent: "edr_freight_app:hierarchy_positions:change_parent", }, hierarchyEmployeeAssignment: { view: "edr_freight_app:hierarchy_employee_assignment:view", invite: "edr_freight_app:hierarchy_employee_assignment:invite", assign: "edr_freight_app:hierarchy_employee_assignment:assign", }, positionTypes: { view: "edr_freight_app:position_types:view", create: "edr_freight_app:position_types:create", update: "edr_freight_app:position_types:update", delete: "edr_freight_app:position_types:delete", }, }, } as const; const slugToResourceKey = (slug: RuleEngineResourceSlug): string => slug.replace(/-/g, "_"); export function getPermissionKeys(user: AuthUser | null | undefined): string[] { if (!user) return []; if (user.permissionKeys?.length) return user.permissionKeys; const keys = new Set(); for (const p of user.permissions ?? []) { if (p.key) keys.add(p.key); } for (const emp of user.employee ?? []) { for (const pos of emp.positions ?? []) { for (const p of pos.permissions ?? []) { if (p.key) keys.add(p.key); } // Positions created through the admin UI keep their grants on the // position TYPE, not the position — miss these and such staff resolve to // zero permissions and every gated route rejects them. `/api/me` folds // them into the position's permission list, but older payloads may still // carry them separately. for (const p of pos.positionType?.permissions ?? []) { if (p.key) keys.add(p.key); } } } return [...keys]; } /** * Position keys held by the user (e.g. "ethiopian_gl", "djibouti_gl"). * Tolerates IAM payload shape variants: the key flat on the employee position, * nested under `position.key`, or the GL modeled as a role instead. */ export function getPositionKeys(user: AuthUser | null | undefined): string[] { if (!user) return []; const keys = new Set(); for (const emp of user.employee ?? []) { for (const pos of emp.positions ?? []) { if (pos.key) keys.add(pos.key); if (pos.position?.key) keys.add(pos.position.key); } } for (const role of user.roles ?? []) { if (role.key) keys.add(role.key); } return [...keys]; } export function hasPosition( user: AuthUser | null | undefined, positionKey: string, ): boolean { return getPositionKeys(user).includes(positionKey); } export const POSITION_KEYS = { ethiopianGl: "ethiopian_gl", djiboutiGl: "djibouti_gl", } as const; /** Position-type keys held by the user (e.g. "djibouti-gl-officer"). */ export function getPositionTypeKeys( user: AuthUser | null | undefined, ): string[] { if (!user) return []; const keys = new Set(); for (const emp of user.employee ?? []) { for (const pos of emp.positions ?? []) { if (pos.positionType?.key) keys.add(pos.positionType.key); } } return [...keys]; } // GL staff are identified by the root position key (department heads) OR by // their position-type key (sub-positions: director/chief/officer) — both // forms get the clearance-only locked view. const ET_GL_TYPE_PREFIX = "commercial-global-logistics-(et)"; const DJ_GL_TYPE_PREFIX = "djibouti-gl"; export function isEthiopianGl(user: AuthUser | null | undefined): boolean { return ( hasPosition(user, POSITION_KEYS.ethiopianGl) || getPositionTypeKeys(user).some((k) => k.startsWith(ET_GL_TYPE_PREFIX)) ); } export function isDjiboutiGl(user: AuthUser | null | undefined): boolean { return ( hasPosition(user, POSITION_KEYS.djiboutiGl) || getPositionTypeKeys(user).some((k) => k.startsWith(DJ_GL_TYPE_PREFIX)) ); } export function isSuperAdmin(user: AuthUser | null | undefined): boolean { if (user?.isSuperAdmin) return true; return Boolean(user?.roles?.some((r) => r.key === "super_admin")); } /** Org-level admins may act on any approval step in the chain. */ export function isOrganizationAdmin( user: AuthUser | null | undefined, ): boolean { return Boolean(user?.roles?.some((r) => r.key === "organization_admin")); } export function isFreightApprovalAdmin( user: AuthUser | null | undefined, ): boolean { return isSuperAdmin(user) || isOrganizationAdmin(user); } export function hasPermission( user: AuthUser | null | undefined, key: string, ): boolean { if (!user) return false; if (isSuperAdmin(user)) return true; return getPermissionKeys(user).includes(key); } // Legacy chain roles predate position types; map each to the position types // that stand in for it. Mirror of the API's LEGACY_ROLE_POSITION_TYPES so the // button visibility matches what the approve/reject endpoint will accept. const LEGACY_ROLE_POSITION_TYPES: Record = { LINE_STAFF: ["employee", "teamLeader", "officeHead", "recordOfficer"], DIRECTOR: ["director", "operation-director"], CEO: ["chief", "deputy"], }; const CONTRACT_APPROVE_ROLE_PERMISSION: Record = { LINE_STAFF: FREIGHT_PERMS.contracts.approveLineStaff, DIRECTOR: FREIGHT_PERMS.contracts.approveDirector, CEO: FREIGHT_PERMS.contracts.approveCeo, }; /** * The two hazardous-goods steps prepended to a hazardous contract's chain. * They are not position types — they authorize purely on their own permission, * exactly as the API's HAZARDOUS_APPROVAL_ROLE_PERMISSION does. */ export const HAZARDOUS_APPROVAL_ROLE_PERMISSION: Record = { HAZARDOUS_APPROVAL_ONE: FREIGHT_PERMS.contracts.hazardousApprovalOne, HAZARDOUS_APPROVAL_TWO: FREIGHT_PERMS.contracts.hazardousApprovalTwo, }; /** Display label for an approval step's role (hazardous steps get real names). */ export const CONTRACT_APPROVAL_ROLE_LABELS: Record = { HAZARDOUS_APPROVAL_ONE: "Hazardous review — first approver", HAZARDOUS_APPROVAL_TWO: "Hazardous review — second approver", }; /** * Can this user action a contract approval step requiring `requiredRole`? * * `requiredRole` is an `iam.position_types.key` (the role vocabulary approval * chains are configured in), or a legacy LINE_STAFF/DIRECTOR/CEO string. Used * to show Approve/Reject only to the step's actual approver — a chief step * shows only to a chief, a marketing-officer step only to that officer. * * Deliberately STRICTER than the API's `assertCanApproveContractStep`, which * also lets through anyone holding any contract-approve permission (a fallback * for delegates whose token omits the position type). That blanket is what made * every approver see the button, so it is dropped here: the visibility rule is * admin OR the matching position type (direct / legacy alias) OR the role's own * legacy approve permission. The server still guards the mutation. */ export function canApproveContractStep( user: AuthUser | null | undefined, requiredRole: string | null | undefined, ): boolean { if (!user || !requiredRole) return false; if (isFreightApprovalAdmin(user)) return true; // Hazardous steps are permission-only — no position type stands in for them. const hazardousPermission = HAZARDOUS_APPROVAL_ROLE_PERMISSION[requiredRole]; if (hazardousPermission) return hasPermission(user, hazardousPermission); const positionTypes = getPositionTypeKeys(user); if (positionTypes.includes(requiredRole)) return true; const aliases = LEGACY_ROLE_POSITION_TYPES[requiredRole] ?? []; if (aliases.some((alias) => positionTypes.includes(alias))) return true; const legacyPermission = CONTRACT_APPROVE_ROLE_PERMISSION[requiredRole]; return Boolean(legacyPermission && hasPermission(user, legacyPermission)); } export function canAccessBookings(user: AuthUser | null | undefined): boolean { return hasPermission(user, FREIGHT_PERMS.bookings.view); } /** * Sees the header countdown warning that document review is about to end with * requests still undecided. Its own permission — granted per position type, so * only the desks that act on those requests get alarmed. */ export function canSeeDocReviewAlert( user: AuthUser | null | undefined, ): boolean { return hasPermission(user, FREIGHT_PERMS.bookings.docReviewAlert); } export function canAccessContracts(user: AuthUser | null | undefined): boolean { return hasPermission(user, FREIGHT_PERMS.contracts.view); } /** Can review the GL Ethiopia pre-booking contract clearance queue (Path B). */ export function canReviewContractClearance( user: AuthUser | null | undefined, ): boolean { return hasPermission(user, FREIGHT_PERMS.contracts.clearanceReview); } /** GL Ethiopia: can create a booking under a cleared contract (Path B). */ export function canCreateContractBooking( user: AuthUser | null | undefined, ): boolean { return hasPermission(user, FREIGHT_PERMS.contracts.createBooking); } /** Operations: can review the Path A self-clearance queue (non-customs). */ export function canReviewSelfClearance( user: AuthUser | null | undefined, ): boolean { return hasPermission(user, FREIGHT_PERMS.contracts.opsClearanceReview); } /** Can see/manage the customs document-clearance queue (Global Logistics). */ export function canViewClearance(user: AuthUser | null | undefined): boolean { return hasPermission(user, FREIGHT_PERMS.bookings.reviewDocuments); } export function canViewScheduling(user: AuthUser | null | undefined): boolean { return hasPermission(user, FREIGHT_PERMS.trainScheduling.view); } /** Any train-scheduling write action (create / update / cancel / reschedule). */ export function canManageScheduling( user: AuthUser | null | undefined, ): boolean { return ( hasPermission(user, FREIGHT_PERMS.trainScheduling.create) || hasPermission(user, FREIGHT_PERMS.trainScheduling.update) || hasPermission(user, FREIGHT_PERMS.trainScheduling.cancel) || hasPermission(user, FREIGHT_PERMS.trainScheduling.reschedule) ); } export function canCreateSchedule(user: AuthUser | null | undefined): boolean { return hasPermission(user, FREIGHT_PERMS.trainScheduling.create); } /** Dispatching a train and opening/closing its booking window are both updates. */ export function canUpdateSchedule(user: AuthUser | null | undefined): boolean { return hasPermission(user, FREIGHT_PERMS.trainScheduling.update); } export function canViewFleet(user: AuthUser | null | undefined): boolean { return hasPermission(user, FREIGHT_PERMS.fleet.view); } export type FleetCrudResource = | "locomotives" | "wagons" | "trains" | "routes" | "containers" | "cargoes" | "vehicles" | "drivers"; /** Per-resource fleet CRUD check — each resource needs its own grant. */ export function canFleetAction( user: AuthUser | null | undefined, resource: FleetCrudResource, action: "create" | "update" | "delete", ): boolean { return hasPermission(user, FREIGHT_PERMS[resource][action]); } /** * Permanent-purge check for locomotives and wagons. Unlike * {@link canFleetAction} this does NOT fall back to the coarse fleet:manage * key — an irreversible delete needs its own grant, and the API guards these * endpoints the same way. */ export function canFleetHardDelete( user: AuthUser | null | undefined, resource: "locomotives" | "wagons" | "routes", ): boolean { return hasPermission(user, FREIGHT_PERMS[resource].hardDelete); } export function isFreightAdmin(user: AuthUser | null | undefined): boolean { return hasPermission(user, FREIGHT_PERMS.admin); } export type RuleEngineAction = "view" | "create" | "update" | "delete"; export function ruleEngineActionKey( slug: RuleEngineResourceSlug, action: RuleEngineAction, ): string { return `edr_freight_app:rule_engine:${slugToResourceKey(slug)}:${action}`; } export function ruleEngineViewKey(slug: RuleEngineResourceSlug): string { return ruleEngineActionKey(slug, "view"); } /** * Deciding a filed change — a step above `manage`, which only lets a staff * member propose one. Only resources with an approval workflow have it. */ export function ruleEngineApproveKey(slug: "rates"): string { return `edr_freight_app:rule_engine:${slugToResourceKey(slug)}:approve`; } export function canApproveRuleEngineChange( user: AuthUser | null | undefined, slug: "rates", ): boolean { return hasPermission(user, ruleEngineApproveKey(slug)); } export function canAccessRuleEngineResource( user: AuthUser | null | undefined, slug: RuleEngineResourceSlug, mode: RuleEngineAction, ): boolean { return hasPermission(user, ruleEngineActionKey(slug, mode)); } /** Holds any write action on the resource — for surfaces gated on "can edit at all". */ export function canWriteRuleEngineResource( user: AuthUser | null | undefined, slug: RuleEngineResourceSlug, ): boolean { return (["create", "update", "delete"] as const).some((a) => canAccessRuleEngineResource(user, slug, a), ); } export function canAccessAnyRuleEngineView( user: AuthUser | null | undefined, slugs: RuleEngineResourceSlug[], ): boolean { return slugs.some((slug) => canAccessRuleEngineResource(user, slug, "view")); }