mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 13:05:44 +00:00
A booking sold WITHOUT the return service had no way to send its empties back: the containers were the customer's problem and nothing in the system priced, billed or planned the movement. Adds the request flow end to end. The customer opens the booking, says how many containers are coming back and types each number; the request lands in a new backoffice queue (Empty Return Requests). Approval prices it off the same live WITH_RETURN route rate the rule engine bills when the service IS bought up front — per container, converted to birr, and overridable by the reviewer — and issues the invoice there and then. Payment settles through the normal invoice path, whose `empty_return_request.invoice.paid` event moves the request to PAID; the customer then books the return date and the truck. Scheduled requests surface on Container Returns as Planned Empty Returns, where confirming the arrival records the containers through the existing empty-container-return flow — which in turn closes the request once its last container is in. Container freight only, never a booking that already ships WITH_RETURN, and only from IN_TRANSIT onward: the empty comes back after delivery, so the option has to outlive ARRIVED. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
831 lines
33 KiB
TypeScript
831 lines
33 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",
|
|
cancel: "edr_freight_app:contracts:cancel",
|
|
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",
|
|
/** Per-station loading/unloading time-window buttons (start/end pairs). */
|
|
loadingStart: "edr_freight_app:train_scheduling:loading_start",
|
|
loadingEnd: "edr_freight_app:train_scheduling:loading_end",
|
|
unloadingStart: "edr_freight_app:train_scheduling:unloading_start",
|
|
unloadingEnd: "edr_freight_app:train_scheduling:unloading_end",
|
|
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",
|
|
/** Decide detach/maintenance requests on a SCHEDULED train (4-eyes gate). */
|
|
approveWagonDetach: "edr_freight_app:trains:approve_wagon_detach",
|
|
},
|
|
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",
|
|
delete: "edr_freight_app:warehouse_zones:delete",
|
|
},
|
|
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",
|
|
},
|
|
emptyReturnRequests: {
|
|
view: "edr_freight_app:empty_return_requests:view",
|
|
review: "edr_freight_app:empty_return_requests:review",
|
|
},
|
|
/**
|
|
* 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"));
|
|
}
|