mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
818 lines
32 KiB
TypeScript
818 lines
32 KiB
TypeScript
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<string>();
|
|
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<string>();
|
|
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<string>();
|
|
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<string, string[]> = {
|
|
LINE_STAFF: ["employee", "teamLeader", "officeHead", "recordOfficer"],
|
|
DIRECTOR: ["director", "operation-director"],
|
|
CEO: ["chief", "deputy"],
|
|
};
|
|
|
|
const CONTRACT_APPROVE_ROLE_PERMISSION: Record<string, string> = {
|
|
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<string, string> = {
|
|
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<string, string> = {
|
|
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"));
|
|
}
|