Files
edr-platform/apps/edr-freight-web/backoffice/src/lib/permissions.ts
2026-08-19 08:41:51 +00:00

803 lines
31 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",
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",
},
/**
* 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",
},
// 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"));
}