Commit graph

29 commits

Author SHA1 Message Date
0679883273 refactor(crewing): correct audit action types + atomic auto-raise backfills
Audit-trail & transaction consistency (spec §11 "one transition, one row"):

- Action types: returnSalary/returnSelection/declineInterviewWaiver no longer
  mislabel a backward decision as its forward action. New CrewActionType members
  SALARY_RETURNED / SELECTION_RETURNED / WAIVER_DECLINED; added RECORD_DELETED;
  dropped the unused GATE_FAILED (migration recreates the enum).
- Deletions are audited: deleteDocument / deleteNextOfKin now write a
  RECORD_DELETED CrewAction (PII removals are traceable).
- Atomicity: autoRaiseRequisition takes an optional tx so the leave-clash and
  sign-off backfills are created INSIDE the approval/sign-off transaction; the
  office notification (notifyAutoRaised) fires after commit. An approved leave or
  a sign-off can no longer commit without its backfill requisition.

Tests assert the corrected action types (crewing-gates, crew-records) and the
existing clash/sign-off suites still pass with the in-transaction backfill.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 23:46:23 +05:30
e193e26368 feat(crewing): EPFO/UAN assisted verification (GstService pattern, flagged)
Scaffolds EPFO/UAN verification the same way GST works — a standalone Playwright
proxy microservice + an /api proxy + an assisted affordance that records the
result. Aadhaar stays manual (UIDAI-restricted). Stacks on the follow-ups branch.
Behind NEXT_PUBLIC_CREWING_ENABLED.

What's in
- EpfoService/ (new microservice, GstService pattern): Express + Playwright.
  POST /otp {uan} → session + OTP request; POST /verify {sessionId,uan,otp} →
  member record; GET /health. EPFO is OTP-gated (no anonymous captcha lookup like
  GST), so the handshake is two steps. Live portal navigation is gated behind
  EPFO_LIVE (default STUB: OTP 000000 → matched) until real selectors/OTP are
  validated. README documents the differences + that Aadhaar is out of scope.
- App: /api/epfo/otp + /api/epfo proxies (gated by verify_bank_epf) to
  EPFO_SERVICE_URL. EpfDetail += epfoMemberName + epfoCheckedAt (migration
  crewing_epfo_check). recordEpfoCheck action persists the EPFO result + audit.
- UI: an "EPFO check" affordance on the verification EPF rows — request OTP →
  enter OTP → matched member → record. Aadhaar noted as manual-only.

Tests & docs
- Integration: verification.test.ts gains recordEpfoCheck (records name+timestamp,
  Accounts-only gating). type-check clean; full unit (245) + integration (213)
  green (RESEND_API_KEY unset).
- .env.example (EPFO_SERVICE_URL/EPFO_LIVE), CLAUDE.md, EpfoService/README.

Note: the EpfoService live portal selectors/OTP are stubbed and must be validated
against a real EPFO session before enabling EPFO_LIVE.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:43:24 +05:30
df3b4bdc97 feat(crewing): resolve self-contained deferred follow-ups (flagged)
Clears the self-contained deferrals tracked across phases. Stacks on 5b appraisal.
Behind NEXT_PUBLIC_CREWING_ENABLED.

- SITE_STAFF login on onboard/placement (Epic D follow-up): lib/crew-login.ts
  maybeCreateSiteStaffLogin creates a passwordless SITE_STAFF User (sharing the
  CRW- employee no., siteId = the assignment's site) when a grantsLogin rank is
  onboarded (onboardCandidate) or placed (placeCrew) and the crew member has an
  email. No-op otherwise.
- Own-site scoping (Epic E follow-up, §8.7): User.siteId added (migration
  crewing_followups); the Crew directory filters a SITE_STAFF user with a home site
  to crew whose active assignment is at that site (graceful when unset). The link is
  set at login creation.
- PPE / next-of-kin verify gates (Epic F/I follow-up): PpeIssue/NextOfKin gained
  verificationStatus + verifiedById; verifyPpe / verifyNextOfKin (verify_site_records,
  MPO) + queue sections in /crewing/verification.

Tests & docs
- Integration: crewing-followups.test.ts (6) — login created/skipped by rank+email
  (+ siteId set), PPE/NoK verify + reject-reason + already-decided guard + gating.
  type-check clean; full unit (245) + integration (211) green (RESEND_API_KEY unset).
- CLAUDE.md updated.

Part of Epic D (#78), Epic E (#79), Epic F (#80), Epic I (#83).
Still deferred (not self-contained): public careers API (A2); Pay-status pay rows (Phase 6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:28:23 +05:30
c14a22588e feat(crewing): Phase 5b — appraisal (flagged)
Final slice of Phase 5. The appraisal lifecycle raise → verify → approve across
three role-gated surfaces, per Crewing-Implementation-Spec §5.4/§8.14. Stacks on
5a verification. Behind NEXT_PUBLIC_CREWING_ENABLED. Completes Phase 5.

What's in
- Schema: Appraisal (on CrewAssignment) + AppraisalStatus
  (DRAFT/SUBMITTED/MPO_VERIFIED/MANAGER_APPROVED/REJECTED); CrewActionType +=
  APPRAISAL_SUBMITTED/VERIFIED/APPROVED/REJECTED. Migration crewing_appraisal.
- State machine lib/appraisal-state-machine.ts: verify (SUBMITTED→MPO_VERIFIED,
  MPO/Manager), approve (MPO_VERIFIED→MANAGER_APPROVED, Manager); orthogonal reject.
- Actions (crewing/appraisals/actions.ts): raiseAppraisal (raise_appraisal — PM/
  site staff), verifyAppraisal (verify_appraisal — MPO), approveAppraisal
  (approve_appraisal — Manager); reject paths require remarks; notifications
  APPRAISAL_FOR_VERIFICATION / APPRAISAL_FOR_APPROVAL.
- Three surfaces (§8.14): PM raises + tracks status on the crew-profile Appraisals
  tab; MPO verifies in the Verification queue (Appraisals section); Manager approves
  in the central /approvals queue (Appraisal kind).

Tests & docs
- Unit: appraisal-state-machine.test.ts (4). Integration: appraisal.test.ts (4) —
  raise→verify→approve happy path, MPO reject, permission gating (MPO can't raise,
  site staff can't verify, MPO can't approve). type-check clean; full unit (245) +
  integration (205) green (verified with RESEND_API_KEY unset).
- CLAUDE.md updated — completes Phase 5 (I + H).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:09:32 +05:30
8982118eee feat(crewing): Phase 5a — verification queue (flagged)
First slice of Phase 5 (verification + appraisal). The office queue for verifying
site-entered records, per Crewing-Implementation-Spec §8.11/R11. Stacks on 4c.
Behind NEXT_PUBLIC_CREWING_ENABLED.

What's in
- Schema: CrewActionType += RECORD_VERIFIED/RECORD_REJECTED (migration
  crewing_verification_actions). No model changes — SeafarerDocument/BankDetail/
  EpfDetail already carry verificationStatus + verifiedById (3b/4a).
- Actions (crewing/verification/actions.ts): verifyDocument (verify_site_records —
  MPO/Manager) and verifyBankEpf (verify_bank_epf — Accounts) set
  verificationStatus VERIFIED/REJECTED + verifiedById; rejection requires remarks;
  each writes a CrewAction. Already-decided records are guarded.
- Screen: /crewing/verification — role-aware (MPO: pending documents with expiry
  flags; Accounts: pending bank/EPF), Verify / Reject-with-remarks. Leave is not
  here (Manager approval, R11). Verification added to nav (MPO + Accounts + SU, §7).

Tests & docs
- Integration: verification.test.ts (6) — doc verify/reject + already-decided
  guard, bank/EPF verify, permission gating (Accounts can't verify docs, MPO can't
  verify bank/EPF). type-check clean; full unit (241) + integration (201) green
  (verified with RESEND_API_KEY unset, mimicking CI).
- CLAUDE.md updated.

Deferred (per decision): PPE / next-of-kin verification gates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:59:31 +05:30
4e71863c57 feat(crewing): Phase 4c — sign-off & experience (flagged)
Final slice of Phase 4 (the Epic K piece deferred from Phase 2). Ends a tour of
duty and returns the crew member to the candidate pool as an ex-hand. Per
Crewing-Implementation-Spec §5.3. Behind NEXT_PUBLIC_CREWING_ENABLED.

What's in
- Schema: CrewActionType += CREW_SIGNED_OFF (migration crewing_signoff).
- signOffCrew(assignmentId, date, remarks) (crewing/crew/actions.ts, sign_off_crew):
  one transaction — assignment → SIGNED_OFF (+ signOffDate); append an internal
  ExperienceRecord (rank, on/off dates, computed durationMonths); flip the SAME
  CrewMember EMPLOYEE → EX_HAND (type/source EX_HAND), so they reappear in
  Candidates as a returning hand; CrewAction CREW_SIGNED_OFF; then auto-raise a
  SIGN_OFF backfill requisition via autoRaiseRequisition.
- Screen: a "Sign off" button on the crew-profile header (sign_off_crew holders —
  site staff / MPO / Manager); on success redirects to the Crew directory.

Tests & docs
- Integration: signoff.test.ts (3) — SIGNED_OFF + experience + EX_HAND + SIGN_OFF
  backfill, already-signed-off guard, permission gating. type-check clean; full
  unit (241) + integration (195) green.
- CLAUDE.md updated — completes Phase 4 (E/F/G + K).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:34:29 +05:30
040a66488d feat(crewing): clash detection by required strength (Option A)
Replace the implicit "strength = 1" clash rule with a configurable per-vessel,
per-rank requirement (director decision). Adds VesselRankRequirement
{vesselId, rankId, minStrength} (migration crewing_vessel_rank_requirement) and
reworks lib/leave-clash.ts → leaveCausesClash: a leave approval clashes when the
remaining active same-rank cover over the window would fall below minStrength
(default 1 when unconfigured), auto-raising a LEAVE requisition. The requirement
is managed by the office (manage_crew, admin UI in the follow-up).

- Integration: leave-attendance.test.ts gains a configured-strength case
  (minStrength 2, one remaining → clash). Full unit (240) + integration (183) green.
- CLAUDE.md updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:14:21 +05:30
aac31c6755 feat(crewing): Phase 4b — leave & attendance (flagged)
Second slice of Phase 4 (stacked on 4a crew records). Leave (site-applied,
Manager-decided) with clash auto-backfill, and the daily attendance calendar,
per Crewing-Implementation-Spec §5.3/§8.9–8.10. Behind NEXT_PUBLIC_CREWING_ENABLED.

What's in
- Schema (crewing_leave_attendance migration): LeaveRequest (LeaveType,
  LeaveStatus) + Attendance (AttendanceStatus, unique per assignment+date) on
  CrewAssignment; CrewActionType += LEAVE_APPLIED/LEAVE_DECIDED/ATTENDANCE_RECORDED.
- Leave (R1): site staff apply on behalf (apply_leave); Manager decides
  (decide_leave) → assignment ON_LEAVE; MPO has no leave role. Leave approvals also
  surface in the central /approvals queue (§8.13 Leave kind). Notification
  LEAVE_FOR_APPROVAL.
- Clash auto-backfill (R6): lib/leave-clash.ts, required strength = 1 — approving a
  leave that leaves the vessel with zero active same-rank cover auto-raises a LEAVE
  requisition via the Phase-2 autoRaiseRequisition.
- Attendance (R5): daily month calendar; site staff record (record_attendance),
  Manager views (view_attendance) but cannot edit, MPO neither. saveAttendance
  bulk-upserts dirty cells.
- Screens: /crewing/leave (apply-on-behalf + Manager Approve/Decline) and
  /crewing/attendance (tap-to-cycle calendar + Save). Leave + Attendance added to
  the flag-gated nav (Manager + Site staff).

Tests & docs
- Integration: leave-attendance.test.ts (7) — apply/decide, clash auto-raise (and
  no-raise when cover remains), MPO/Manager attendance lockout, permission gating.
  type-check clean; full unit (240) + integration (182) green.
- CLAUDE.md updated with the Phase 4b surface.

Deferred: the 6-month leave-planner timeline (lightweight list for now); hours/
overtime attendance (A7).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 21:07:15 +05:30
37b1debc9d feat(crewing): Phase 4a — crew records & profile + PPE (flagged)
First slice of Phase 4 (stacked on 3c onboarding). The Crew directory and tabbed
crew profile with documents, bank/EPF (role-masked), next of kin, PPE and
experience, per Crewing-Implementation-Spec §8.7–8.8. Behind
NEXT_PUBLIC_CREWING_ENABLED; production unchanged.

What's in
- Schema (crewing_crew_records migration): SeafarerDocument, NextOfKin
  (isEmergency), ExperienceRecord, PpeIssue (PpeItem enum) — all on CrewMember;
  CrewActionType += DOCUMENT_UPLOADED/RECORD_UPDATED/PPE_ISSUED/PPE_RETURNED/
  EXPERIENCE_ADDED.
- PII masking (lib/crew-pii.ts, §6/§8.8): bank account + Aadhaar full only for
  Accounts/SuperUser, masked otherwise; salary hidden from site staff. Applied
  server-side before crossing to the client.
- Actions (crewing/crew/actions.ts): uploadDocument/deleteDocument, saveBankEpf,
  addNextOfKin/deleteNextOfKin, issuePpe/returnPpe, addExperience — guarded by
  upload_crew_records / issue_ppe, each writes a CrewAction.
- Screens: /crewing/crew (directory, search + vessel filter, ex-hands excluded)
  and /crewing/crew/[id] (tabbed profile: Documents · Bank & EPF · Next of kin ·
  PPE · Experience · Pay status). Crew added to the flag-gated nav (MGR/MPO/Site/
  Accounts).

Tests & docs
- Unit: crew-pii.test.ts (6). Integration: crew-records.test.ts (7) — documents,
  bank/EPF upsert, NoK, PPE issue/return, experience + permission gating.
  type-check clean; full unit (240) + integration (175) green.
- CLAUDE.md updated with the Phase 4a surface.

Deferred: site-staff own-site scoping (needs a User↔Site link); the records verify
queue (§8.11, Phase 5); Pay-status shows the salary structure only until payroll
(Phase 6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 19:27:21 +05:30
c82efa71af feat(crewing): Phase 3c — onboarding (flagged)
Final slice of Phase 3 (stacked on 3b pipeline). The onboarding transaction that
turns a SELECTED candidate into active crew, per Crewing-Implementation-Spec
§8.5/§9/§11. Behind NEXT_PUBLIC_CREWING_ENABLED; production unchanged.

What's in
- Schema (crewing_onboarding migration): CrewAssignment + AssignmentStatus
  (ACTIVE/ON_LEAVE/SIGNED_OFF — leave/sign-off are Phase 4); ContractLetter
  (salaryRestricted); SalaryStructure += assignmentId; CrewActionType +=
  CREW_ONBOARDED. Employee numbers CRW-xxxx via lib/employee-number.ts.
- Action (onboardCandidate, onboard_crew): one transaction off a SELECTED
  application — assign employeeId, create CrewAssignment(ACTIVE, signOnDate),
  bind the approved SalaryStructure (assignmentId + effectiveFrom), Application →
  ONBOARDED, Requisition → FILLED, CrewMember → EMPLOYEE (+ currentRank); contract
  letter stored after. Guards flag + permission + SELECTED state.
- Screen: the SELECTED action card's "Onboard to crew" modal (joining date,
  contract upload, starts-automatically chips); the CRW- number shows on the
  ONBOARDED card.

Tests & docs
- Integration: onboarding.test.ts (5) — full transaction, requisition FILLED +
  salary binding, joining-date + SELECTED-only guards, permission gating, sequential
  CRW- ids. type-check clean; full unit (234) + integration (168) green.
- CLAUDE.md updated with the Phase 3c surface.

Deferred: SITE_STAFF login creation for management ranks (grantsLogin) — a
follow-up; attendance/experience/PPE records begin in Phase 4.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 19:12:53 +05:30
3ec3a2b4ef feat(crewing): Phase 3b — recruitment pipeline (flagged)
Second slice of Phase 3 (stacked on 3a candidates). The gated 7-stage
recruitment pipeline per Crewing-Implementation-Spec §5.1/§8.4–8.5/§8.13.
Behind NEXT_PUBLIC_CREWING_ENABLED; production unchanged.

What's in
- Schema (crewing_pipeline migration): Application (one per requisition+candidate)
  + 7-stage ApplicationStage; ApplicationGate (SALARY/SELECTION/WAIVER pending =
  Manager queue items); ReferenceCheck; effective-dated SalaryStructure (attached
  to the Application now, bound to the assignment in 3c); minimal BankDetail/EpfDetail
  captured at DOC_VERIFICATION (PII encryption deferred to Phase 4). CrewAction +=
  applicationId; pipeline CrewActionTypes.
- State machine: lib/application-pipeline.ts — sourcing advances MPO/Manager;
  approve_salary + select are Manager-only; orthogonal canReject; BOARD_STAGES.
- Actions: addApplication (first candidate → requisition SHORTLISTING), advanceStage,
  recordReferenceCheck, verifyDocuments (bank/EPF), agreeSalary→approveSalary/returnSalary,
  recordInterviewResult, requestInterviewWaiver→approve/decline, selectCandidate
  (→ requisition SELECTED)/returnSelection, rejectApplication. Waiver never automatic (R2).
  Notifications SALARY/SELECTION/WAIVER + CANDIDATE_PROPOSED.
- Screens: pipeline board per requisition (7 columns + Add candidate); application
  workhorse (7-step stepper + adaptive per-stage action card); "Open pipeline" on the
  requisition detail. Central /approvals gains a crewing section (inline Approve/Return)
  for one unified Manager queue (§8.13 R8).

Tests & docs
- Unit: application-pipeline.test.ts (9). Integration: applications.test.ts (10) —
  full happy path, salary/selection/waiver approvals + Manager-only gating, failed
  interview, reject, site-staff lockout. type-check clean; full unit (234) + integration
  (163) green.
- CLAUDE.md "Crewing" updated with the Phase 3b surface.

Deferred: onboarding (Epic D, Phase 3c) — SELECTED → ONBOARDED, CrewAssignment,
employeeId, requisition → FILLED, salary bound to the assignment.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:49:12 +05:30
be6db075dc feat(crewing): Phase 3a — candidates / talent pool (flagged)
First slice of Phase 3 (Epics B/C/D shipped as stacked sub-PRs). Adds the
CrewMember talent-pool spine and the Candidates screens. Behind
NEXT_PUBLIC_CREWING_ENABLED; production unchanged. Stacks on the requisitions
branch (Phase 2).

What's in
- Schema (crewing_candidates migration): CrewMember (spine) + CrewStatus,
  CandidateType, CandidateSource enums; CrewAction gains a nullable crewMemberId;
  CrewActionType += CANDIDATE_ADDED/UPDATED. employeeId is assigned at onboarding
  (3c), so it's nullable here.
- Actions (crewing/candidates/actions.ts): addCandidate / updateCandidate —
  guard flag + manage_candidates, write a CrewAction, optional CV upload via
  buildStorageKey("cv", …) + uploadBuffer (no parsing — A2 deferred). EX_HAND
  source ⇒ type/status EX_HAND; edits never downgrade an EMPLOYEE.
- Screens: /crewing/candidates (master list with search/source/rank-applied/
  min-experience filters as removable chips + match count + Clear all; Add-candidate
  modal) and /crewing/candidates/[id] (profile; pipeline stepper is 3b). Candidates
  added to the flag-gated Crewing nav (Manager + MPO).

Tests & docs
- Integration: candidates.test.ts (7) — add/update, ex-hand derivation, employee
  no-downgrade, permission gating. type-check clean; full unit (225) + integration
  (153) suites green.
- CLAUDE.md "Crewing" section updated with the Phase 3a surface.

Deferred: public careers intake API (A2, §13 open question); CV parsing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:23:01 +05:30
0b2ed9ac07 feat(crewing): Phase 2 — requisitions + relief requests (flagged)
Second slice of the Crewing module per wiki Crewing-Implementation-Spec §12
(build order item 2). Everything stays behind NEXT_PUBLIC_CREWING_ENABLED;
production is unchanged. Schema is added incrementally — this lands the
requisition lifecycle layer.

What's in
- Schema: Requisition (OPEN→SHORTLISTING→PROPOSING→INTERVIEWING→SELECTED→FILLED,
  →CANCELLED), ReliefRequest, CrewAction (the POAction mirror) + their enums.
  Migration crewing_requisitions.
- State machine: lib/requisition-state-machine.ts mirrors po-state-machine
  (selection Manager-only; orthogonal cancel from OPEN/SHORTLISTING by
  cancel_requisition holders, §6). Codes REQ-9000… via lib/requisition-number.ts.
- Actions: raise/cancel/transition + requestReliefCover/convertReliefToRequisition,
  each guarding flag+permission+state, writing a CrewAction and notifying. Shared
  autoRaiseRequisition() (lib/requisition-service.ts) is the backfill entry point
  for sign-off / leave-clash (later phases).
- Notifier: notifyCrew() PO-independent path + CrewNotificationEvent.
- Screens: /crewing/requisitions (list + Raise modal + relief convert) and
  /crewing/requisitions/[id] (detail). Requisitions added to the flag-gated
  Crewing sidebar (Manager + MPO, §7).

Tests & docs
- Unit: requisition-state-machine.test.ts (11).
- Integration: requisitions.test.ts (15) — raise/cancel/transition, relief
  request + convert, auto-raise, permission gating.
- CLAUDE.md "Crewing" section updated with the Phase 2 surface.

Deferred: sign-off/experience (Epic K, §12 item 2) depends on the crew/assignment
models from Phase 3/4; autoRaiseRequisition() is ready for it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 18:22:59 +05:30
d0006a8fc7 feat(crewing): foundations — SITE_STAFF role, ranks reference data + admin (flagged)
Phase 1 of the Crewing module per wiki Crewing-Implementation-Spec §12, all dark
behind NEXT_PUBLIC_CREWING_ENABLED (off by default — production unchanged).

- schema: add SITE_STAFF to Role; add Rank (self-referential org hierarchy, like
  Account) + RankDocRequirement, RankCategory & SeafarerDocType enums.
- permissions: full §6 crewing grant matrix (PO_ROLE_PERMISSIONS +
  CREWING_ROLE_PERMISSIONS merged); SITE_STAFF row; MPO has no attendance/leave,
  approvals are Manager-only, manage_ranks is Manager+Admin.
- feature flag: CREWING_ENABLED (opt-in "true").
- nav: flag-gated Crewing section scaffold + "Ranks & documents" under Admin.
- reference data: rank-data.ts + rank-doc-data.ts seeded via shared seed-ranks.ts
  in both dev and prod seeds (19 ranks, 118 doc requirements).
- screen: /admin/ranks — rank hierarchy card + per-rank required-documents card.
- role-label/prefix maps updated for the new role.

Tests: unit (permission matrix + flag), integration (ranks admin CRUD, parent
linking, cycle/children guards, doc-requirement upsert/remove, permission gating).
Docs: CLAUDE.md "Crewing (feature-flagged)" section + env var.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 13:26:04 +05:30
0b10ba5e54 feat(po): cancel POs (manager/superuser) + optional supersede link (#53)
Managers and superusers can cancel a PO from any state via a confirmation modal
that requires typing "cancel" and a mandatory reason. A cancelled PO becomes a
terminal CANCELLED state and drops out of every spend tracker/graph (those filter
on POST_APPROVAL_STATUSES / explicit whitelists, none of which include CANCELLED).

A cancelled PO may optionally be linked to the existing PO that supersedes it
(by PO number); the replacement shows the reciprocal "supersedes" link. No
vessel/account/vendor match is enforced and the link can be added any time.

Cancelled POs remain visible (greyed in history) and exportable, with a diagonal
"CANCELLED" watermark on both the PDF and XLSX exports.

- schema: POStatus CANCELLED; cancelledAt/cancellationReason; self-referential
  supersededById relation; ActionType CANCELLED/SUPERSEDED (+ migration)
- state machine canCancel(); cancel_po permission (MANAGER + SUPERUSER)
- cancelPo / supersedePo server actions + PO_CANCELLED notification
- cancel modal + supersede form; cancelled banner with reciprocal links
- exhaustive CANCELLED entries in all status label/variant maps
- diagonal CANCELLED watermark embedded for PDF (CSS) and XLSX (image)
- integration tests (cancel from any state, reason/role guards, supersede)

Inventory reversal on cancel is deferred to #55 (inventory is feature-flagged off).

Closes #53

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 12:20:54 +05:30
1071cb226f feat(po): per-company logo, stamp & brand bar on exported POs
Companies can upload a logo and a stamp/seal (Admin → Companies → Edit →
Branding); both render on exported PDF and XLSX purchase orders. A fixed
brand-colour bar (#92D050, matching the sample PO) runs along the bottom of
every export.

- Company.logoKey / stampKey + migration
- buildCompanyAssetKey() deterministic storage keys (overwrite-in-place)
- uploadCompanyAsset / removeCompanyAsset server actions (≤4MB PNG/JPG/WebP,
  manage_vessels_accounts gated)
- CompanyBrandingUploader in the company edit dialog with live previews
- Export route embeds logo (top-left), stamp (signatory block) and brand bar
  in both ExcelJS and print-HTML paths
- Unit test (storage keys) + integration test (branding actions)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 01:17:23 +05:30
Claude (auto-fix)
25d1164d34 feat(po): allow submitter to set an optional PO date
Add an optional PO Date field to the create and edit PO forms.
Submitters can pick any date (back-dated or forward-dated). If left
blank, the exported PO document falls back to the approved date, then
to the creation date.

Changes:
- Prisma schema: add `poDate DateTime?` to PurchaseOrder
- Migration 20260616000000_add_po_date: ALTER TABLE to add the column
- createPoSchema: add optional `poDate` string field
- new-po-form, edit-po-form: add PO Date picker in Order Information
- create/edit actions: persist poDate to DB
- edit action resubmit snapshot: track poDate changes for manager diff
- po-detail: show PO Date in Order Details; include in resubmit diff banner
- export route: use poDate ?? approvedAt ?? createdAt as the date on
  the exported PDF/XLSX document
- validations.test: fix pre-existing costCentreRef→vesselId mismatch
  and add poDate test cases

Fixes #4

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-16 13:06:12 +05:30
add0f3c19c feat(payments): compulsory payment date when Accounts records payment
- New PurchaseOrder.paymentDate field (migration 20260531000002)
- Backfill: existing POs use paidAt, else the earliest payment action date
- Accounts must enter a payment date with the payment reference
- Date input pre-selected to today, max=today (no future dates)
- Validated server-side (required + not in future) in processPaymentSchema
- paymentDate stored on both full and partial payments; paidAt set from it
- Shown on PO detail (Payment Date) and payment history (prefers paymentDate)
- Integration tests updated; added future-date rejection test

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 08:59:25 +05:30
56b0490229 feat: structured PO numbers, import closed, auto-vendor/product, company code, inventory flag
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 01:56:33 +05:30
4cb927cbd0 feat(companies): add invoiceEmail field separate from contact email
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 01:32:17 +05:30
e308d86e93 feat: Companies — multi-company PO support with admin CRUD and export integration
Schema:
- New Company model (name, gstNumber, address, telephone, mobile, email, invoiceAddress, isActive)
- PurchaseOrder.companyId FK (optional, SET NULL on company delete)
- Migration: 20260530000003_add_company

Admin:
- /admin/companies page with full CRUD (create, edit, deactivate, delete)
- Companies table shows name, GST, contact details, status
- Companies link added to Admin section of sidebar (Briefcase icon)

PO forms (new / edit / import / manager-edit):
- Company dropdown appears at the top of Order Information when companies exist
- Pre-populated with first active company; selection persisted to DB via companyId

Import form:
- parseSheet() now extracts companyName from Excel row 1 (col A)
- Import preview auto-matches detected company name against known companies
- Shows detected name as a hint; user can override before saving

Export (PDF + XLSX):
- Company constants (CO_NAME, CO_ADDR, CO_TEL, INV_ADDR, INV_GST) are now
  derived from the linked Company record when present, falling back to the
  original Pelagia Marine hardcoded defaults when no company is set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 19:31:34 +05:30
280966a369 refactor: revert cost centre to vessels only, remove vessel-site link
Cost Centre on PO forms now shows only Vessels (plain vesselId field).
Sites are a separate concept and not selectable as cost centres.

- PurchaseOrder.vesselId is required again (NOT NULL restored)
- Vessel.siteId and vessel->site relation removed from schema
- DB migration: drops Vessel.siteId column, restores PO.vesselId NOT NULL
- All PO forms (new/edit/import/manager-edit): plain vessel <select> with
  code-prefixed labels (e.g. "HNR1 — HNR 1")
- History, approvals, dashboard, my-orders, payments: back to vesselId
  filter params and po.vessel.name display
- Admin vessels: removed Site column and site-assignment dropdown
- Admin sites detail page: removed "Assigned Vessels" section
- Sites table: removed Vessels count column (no longer linked)
- seed-prod.ts and seed.ts: vessels created without siteId
- SearchableSelect accounting code picker retained from previous commit

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 18:14:24 +05:30
0d17672ea9 feat(accounts): hierarchical accounting codes with 6-digit format and category tree
- Account model gains parentId (self-referential, 3 levels: TopCategory → SubCategory → Item)
- DB migration: adds parentId FK column to Account table
- Code format changed from PREFIX-NNN to 6-digit numeric (e.g. 100101)
- Seeded all 300+ accounting codes from the official chart (Rev. 01/251227) across
  7 top categories: Capital Expenses, Business Development, Office Admin, Project
  Expenses, Manning, Technical, Bunker/Lubes
- Admin Accounting Code page: collapsible tree view (top category > sub-category > items),
  inline search, Add/Edit dialogs with parent selector and 6-digit code field
- All PO forms (new, edit, import, manager-edit): accounting code dropdown now shows
  only leaf items grouped in <optgroup> by sub-category, labelled "TopCat › SubCat"
- Seed data updated: old flat account codes replaced by mapped leaf codes from new hierarchy

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 03:27:31 +05:30
cc7251e6b7 feat: Cost Centre covers vessels and sites, vessel codes, Accounting Code rename, vessel-site assignment
- Undo Vessel→Cost Centre rename in admin (admin shows "Vessel Management" again)
- Sidebar: "Cost Centres"→"Vessels", "Accounts"→"Accounting Codes"
- PO forms (new/edit/import/manager-edit) now show both Vessels (with code) and Sites in the
  Cost Centre dropdown, encoded as v:<id> / s:<id> via a costCentreRef field
- vesselId on PurchaseOrder is now nullable; siteId is set when a site is the cost centre
- History, approvals, dashboard, my-orders, payments display vessel.name ?? site.name as Cost Centre
- History and approvals cost centre filters use costCentreRef URL param supporting both types
- Admin vessel form: adds Site assignment dropdown
- Admin accounts: renamed to "Accounting Code" throughout (pages, forms, sidebar)
- PO detail and exports: "Account" label renamed to "Accounting Code"
- Site detail: "Assigned Vessels (Cost Centres)" heading; vessel detail breadcrumb fixed
- Create PO links from vessel/site detail use ?costCentreRef= param
- Export routes handle costCentreRef filter param (with legacy vesselId fallback)
- DB migration: ALTER TABLE PurchaseOrder ALTER COLUMN vesselId DROP NOT NULL
- CLAUDE.md updated with Cost Centre Model documentation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 03:04:29 +05:30
56817a7d86 feat(auth): add Microsoft 365 SSO via Azure Entra ID
Adds the Microsoft Entra ID provider to NextAuth alongside the existing
credentials provider. Sign-in is restricted to Pelagia Marine's M365
tenant via the issuer URL; access is further gated by requiring a
matching active user record in the DB (DB-managed roles remain unchanged).

- auth.ts: add MicrosoftEntra provider, signIn callback (DB lookup),
  async jwt callback to populate id/role on first SSO sign-in
- login-form.tsx: add primary "Sign in with Microsoft 365" button with
  Microsoft logo; credentials form kept as a fallback below a divider
- prisma: make passwordHash nullable (migration applied) to allow
  SSO-only users without a local password
- admin/users: password is now optional when creating a user — leave
  blank for SSO-only accounts
- profile/actions: return a clear error if an SSO user (no passwordHash)
  attempts to use the change-password form
- .env.example: document AZURE_AD_CLIENT_ID/SECRET/TENANT_ID

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 22:48:37 +05:30
a2c35d0a93 feat(admin): auto-generate structured IDs for users, vendors, accounts and cost centres
Users: employeeId auto-generated from role prefix (TCH/MAN/ACC/MGR/SUP/AUD/ADM)
followed by next sequential number; shown read-only in edit form, removed
from create form. Cost Centres: new code field (SITE-001 ...) added to
Vessel model with migration + backfill; auto-generated on create, read-only
in edit. Vendors and Accounts: code/vendorId inputs pre-filled with the
next suggested ID (VND-001, ACC-001) from the server page; user can override
with any PREFIX-NUMBER format, validated by regex.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 15:02:50 +05:30
cf9ff40262 feat(payments): partial/advance payment support
Allow accounts to record partial/advance payments against a PO before
full delivery. A new PARTIALLY_PAID status tracks in-progress payment;
paidAmount accumulates across multiple markPaid calls. PO only closes
when both paidAmount >= totalAmount AND all line items are delivered.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 04:17:19 +05:30
2c39f0225f feat(vessels): remove IMO number tracking 2026-05-22 17:14:40 +05:30
19029a5a77 chore: restructure repo — flatten App/pelagia-portal to App, rename Prototype→Wireframe and Spec→Design
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 23:18:58 +05:30