pelagia-portal/App/lib/permissions.ts
Hardik a99b2ed5df
All checks were successful
PR checks / checks (pull_request) Successful in 43s
PR checks / integration (pull_request) Successful in 31s
feat(po): admin-managed Terms & Conditions catalogue + PO dropdowns (#11)
Mirrors the Place-of-Delivery (#19) pattern: an admin clause library that feeds
the PO T&C fields as dropdowns. (No "work order" type — POs only, per steer.)

- schema + migration: TermsCondition (category enum + text + isActive); the
  migration seeds the prior TC_DEFAULTS as the starting clauses.
- permission manage_terms (Manager + SuperUser + Admin).
- admin screen /admin/terms: table + Add/Edit dialogs + activate/deactivate +
  delete (mirrors /admin/delivery-locations); sidebar link under Administration.
- PO forms (new / edit / manager-edit): the five named T&C slots (Delivery /
  Dispatch / Inspection / Transit Insurance / Payment Terms) become a shared
  <TermsField> select sourced from active clauses of that category; "Others"
  stays free text; the fixed boilerplate lines are untouched.
- tc* columns stay free-text SNAPSHOTS (export/import unchanged); a current value
  not among active clauses is preserved as a "(current)" option.
- tests: terms CRUD + permission guard + grouping helper (6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 03:38:32 +05:30

276 lines
7.9 KiB
TypeScript

import type { Role } from "@prisma/client";
import { SUBMITTER_VIEW_ALL_ENABLED } from "./feature-flags";
export type Permission =
| "create_po"
| "submit_po"
| "edit_own_draft_po"
| "view_own_pos"
| "view_all_pos"
| "approve_po"
| "reject_po"
| "cancel_po"
| "request_edits"
| "request_vendor_id"
| "process_payment"
| "confirm_receipt"
| "view_analytics"
| "export_reports"
| "manage_users"
| "manage_vendors"
| "create_vendor"
| "manage_vessels_accounts"
| "manage_products"
| "manage_sites"
| "manage_delivery_locations"
| "manage_terms"
// ── Crewing (feature-flagged) — mirrors Crewing-Implementation-Spec §6 ──────
| "raise_requisition"
| "request_relief_cover"
| "convert_relief_to_requisition"
| "cancel_requisition"
| "view_requisitions"
| "manage_candidates"
| "record_reference_check"
| "record_interview_result"
| "request_interview_waiver"
| "approve_interview_waiver"
| "approve_salary_structure"
| "select_candidate"
| "onboard_crew"
| "sign_off_crew"
| "view_crew_records"
| "upload_crew_records"
| "issue_ppe"
| "apply_leave"
| "decide_leave"
| "record_attendance"
| "view_attendance"
| "verify_site_records"
| "verify_bank_epf"
| "raise_appraisal"
| "verify_appraisal"
| "approve_appraisal"
| "generate_wage_report"
| "approve_wage_report"
| "view_wage_report"
| "manage_ranks"
// Office/admin crew management — direct placement (no requisition), crew CRUD,
// and per-vessel rank-strength config. Held by Manager + Admin (+ SuperUser).
| "manage_crew";
// Purchasing / admin permissions (the original PPMS matrix). SITE_STAFF is a
// crewing-only role and holds no purchasing permissions.
const PO_ROLE_PERMISSIONS: Record<Role, Permission[]> = {
TECHNICAL: ["create_po", "submit_po", "edit_own_draft_po", "view_own_pos", "confirm_receipt", "create_vendor"],
MANNING: ["create_po", "submit_po", "edit_own_draft_po", "view_own_pos", "confirm_receipt", "create_vendor"],
ACCOUNTS: ["view_all_pos", "process_payment", "manage_vendors", "create_vendor"],
MANAGER: [
"create_po",
"submit_po",
"edit_own_draft_po",
"view_own_pos",
"view_all_pos",
"approve_po",
"reject_po",
"cancel_po",
"request_edits",
"request_vendor_id",
"view_analytics",
"export_reports",
"manage_vendors",
"create_vendor",
"manage_vessels_accounts",
"manage_products",
"manage_sites",
"manage_delivery_locations",
"manage_terms",
"confirm_receipt",
"process_payment"
],
SUPERUSER: [
"create_po",
"submit_po",
"edit_own_draft_po",
"view_own_pos",
"view_all_pos",
"approve_po",
"reject_po",
"cancel_po",
"request_edits",
"request_vendor_id",
"process_payment",
"confirm_receipt",
"view_analytics",
"export_reports",
"create_vendor",
"manage_delivery_locations",
"manage_terms",
],
AUDITOR: ["view_own_pos", "view_all_pos", "view_analytics", "export_reports"],
ADMIN: [
"view_own_pos",
"view_all_pos",
"view_analytics",
"export_reports",
"manage_users",
"manage_vendors",
"create_vendor",
"manage_vessels_accounts",
"manage_products",
"manage_sites",
"manage_delivery_locations",
"manage_terms",
],
SITE_STAFF: [],
};
// Crewing permissions — a verbatim transcription of the §6 grant matrix in
// wiki Crewing-Implementation-Spec. Gating these is harmless until the screens
// land (the module is behind NEXT_PUBLIC_CREWING_ENABLED). Notes from the spec:
// MPO (MANNING) has NO attendance/leave; decide_leave/approve_* and selection are
// Manager-only; manage_ranks is Manager + Admin (not SuperUser).
const CREWING_ROLE_PERMISSIONS: Record<Role, Permission[]> = {
TECHNICAL: [],
SITE_STAFF: [
"request_relief_cover",
"sign_off_crew",
"view_crew_records",
"upload_crew_records",
"issue_ppe",
"apply_leave",
"record_attendance",
"view_attendance",
"raise_appraisal",
],
MANNING: [
"raise_requisition",
"convert_relief_to_requisition",
"cancel_requisition",
"view_requisitions",
"manage_candidates",
"record_reference_check",
"record_interview_result",
"request_interview_waiver",
"onboard_crew",
"sign_off_crew",
"view_crew_records",
"upload_crew_records",
"issue_ppe",
"verify_site_records",
"verify_appraisal",
],
ACCOUNTS: ["view_crew_records", "verify_bank_epf", "view_wage_report"],
MANAGER: [
"raise_requisition",
"convert_relief_to_requisition",
"cancel_requisition",
"view_requisitions",
"manage_candidates",
"record_reference_check",
"record_interview_result",
"approve_interview_waiver",
"approve_salary_structure",
"select_candidate",
"onboard_crew",
"sign_off_crew",
"view_crew_records",
"upload_crew_records",
"issue_ppe",
"apply_leave",
"decide_leave",
"view_attendance",
"verify_site_records",
"raise_appraisal",
"verify_appraisal",
"approve_appraisal",
"generate_wage_report",
"approve_wage_report",
"view_wage_report",
"manage_ranks",
"manage_crew",
],
SUPERUSER: [
"raise_requisition",
"request_relief_cover",
"convert_relief_to_requisition",
"cancel_requisition",
"view_requisitions",
"manage_candidates",
"record_reference_check",
"record_interview_result",
"request_interview_waiver",
"approve_interview_waiver",
"approve_salary_structure",
"select_candidate",
"onboard_crew",
"sign_off_crew",
"view_crew_records",
"upload_crew_records",
"issue_ppe",
"apply_leave",
"decide_leave",
"record_attendance",
"view_attendance",
"verify_site_records",
"verify_bank_epf",
"raise_appraisal",
"verify_appraisal",
"approve_appraisal",
"generate_wage_report",
"approve_wage_report",
"view_wage_report",
"manage_crew",
],
AUDITOR: ["view_requisitions", "view_crew_records", "view_attendance", "view_wage_report"],
ADMIN: ["view_requisitions", "view_crew_records", "view_wage_report", "manage_ranks", "manage_crew"],
};
const ROLE_PERMISSIONS: Record<Role, Permission[]> = Object.fromEntries(
(Object.keys(PO_ROLE_PERMISSIONS) as Role[]).map((role) => [
role,
[...PO_ROLE_PERMISSIONS[role], ...CREWING_ROLE_PERMISSIONS[role]],
])
) as Record<Role, Permission[]>;
export function hasPermission(role: Role, permission: Permission): boolean {
return ROLE_PERMISSIONS[role]?.includes(permission) ?? false;
}
export function requirePermission(role: Role, permission: Permission): void {
if (!hasPermission(role, permission)) {
throw new Error(`Forbidden: role ${role} lacks permission ${permission}`);
}
}
export function getPermissions(role: Role): Permission[] {
return ROLE_PERMISSIONS[role] ?? [];
}
// ── Submitter roles & feature-flagged view-all ────────────────────────────────
// Submitters raise and track their own POs. The two "submitter" roles below hold
// `view_own_pos` but not `view_all_pos`.
export const SUBMITTER_ROLES: Role[] = ["TECHNICAL", "MANNING"];
export function isSubmitterRole(role: Role): boolean {
return SUBMITTER_ROLES.includes(role);
}
/**
* Feature-flagged: when NEXT_PUBLIC_SUBMITTER_VIEW_ALL_ENABLED=true, submitters may
* read & export every PO (not just their own) and reach the History page. This is a
* read-only widening — it does not grant approval, payment, or edit rights.
*/
export function submitterCanViewAll(role: Role): boolean {
return SUBMITTER_VIEW_ALL_ENABLED && isSubmitterRole(role);
}
/**
* Whether a role may view/export any PO, not just the ones they submitted.
* True for `view_all_pos` holders (ACCOUNTS, MANAGER, SUPERUSER, AUDITOR, ADMIN) and,
* when the feature flag is on, for submitters too.
*/
export function canViewAllPos(role: Role): boolean {
return hasPermission(role, "view_all_pos") || submitterCanViewAll(role);
}