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>
Run #120 (v0.3.0 deploy) failed at the microservice step: every service folder
and ecosystem.config.js were "absent", and pm2 reported "File ecosystem.config.js
not found". Root cause: ~/pms on pms1 is a sparse checkout limited to App/, so
`git checkout -f $TAG` never materialised the service folders or the root
ecosystem.config.js. The app itself deployed fine (App/ is in the sparse set) and
prod stayed healthy.
- deploy.yml: before managing services, disable sparse-checkout (and clear the
legacy core.sparseCheckout config + .git/info/sparse-checkout), then re-checkout
the tag to materialise the full tree. Idempotent / no-op once expanded.
- Guard the pm2 call: if ecosystem.config.js is still absent, fail with a clear
diagnostic (+ sparse-checkout list) instead of the cryptic PM2 error.
- README: note the sparse-checkout expansion.
Needs a fresh tag (e.g. v0.3.1) to re-run the deploy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The v* tag deploy previously only updated the Next app (ppms); GstService /
EpfoService / PdfService were never built or restarted by automation. Now the
same deploy manages them.
- ecosystem.config.js (root): pm2 definitions for gst-service (3003) /
epfo-service (3004) / pdf-service (3005). Registers only services whose source
is checked out (keyed on package.json), so a not-yet-merged service is skipped
and adopted automatically once its PR lands. Secrets come from the env at pm2
invocation; ports are fixed here.
- deploy.yml: after the app restart, export the few service secrets out of
App/.env (never PORT or the ephemeral FORGEJO_TOKEN), npm install + playwright
install chromium + build each present service, then
`pm2 startOrReload ecosystem.config.js --update-env` (create on first release,
reload after) + pm2 save, and health-check :3003/:3004/:3005.
- automation/README.md: documents the flow + the one-time alignment for any
pre-existing differently-named pm2 process.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an "Email to vendor" button on the PO detail (available once approved,
through CLOSED, and again after payment) that opens an Outlook draft addressed
to the vendor's primary contact with a time-limited PDF download link.
Since mailto: can't attach files, the PDF is rendered and stored, and the draft
carries a link (the approach chosen for this issue):
- PdfService/: new standalone Express + Playwright microservice (GstService/
EpfoService pattern) — POST /pdf { url } renders a page to a real PDF via
headless Chromium. SSRF-guarded (shared token + optional origin allowlist).
- export route: accepts a server-only `svc` token (PDF_SERVICE_TOKEN) so
PdfService can fetch /api/po/[id]/export?format=pdf without a user session;
`pdf=1` drops the print button + window.print() auto-trigger.
- lib/pdf-service.ts renderPoPdf(); prepareVendorEmail() server action renders →
uploads to R2 (po-pdf/…) → presigns a 7-day link → returns a mailto draft.
- po-detail: EmailVendorButton, shown when approved + vendor has a contact email.
- Gated by PDF_SERVICE_URL/PDF_SERVICE_TOKEN; friendly error if unconfigured.
- No DB model/migration. Tests: prepareVendorEmail (6, PdfService/storage mocked).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the free-text "Place of Delivery" with a dropdown sourced from a new
admin-managed Delivery Locations list (each = a Company FK + free-text address).
- schema + migration: new DeliveryLocation model (companyId, address, isActive).
- permission: manage_delivery_locations granted to Manager + SuperUser + Admin
(Manager-accessible, not admin-only, per the issue).
- admin screen /admin/delivery-locations: table + Add/Edit dialogs +
activate/deactivate + delete (mirrors /admin/sites); sidebar link under
Administration for Manager/SuperUser/Admin.
- PO forms (new / edit / manager-edit): shared <DeliveryLocationField> native
select populated from active locations, formatted "Company — address".
- PurchaseOrder.placeOfDelivery stays a free-text SNAPSHOT (no FK) — the dropdown
only changes how the value is picked, so export/import/historical POs are
unchanged, and an edit preserves a current value not in the list as a
"(current)" option. Deleting a location is therefore always safe.
- tests: delivery-location CRUD + permission guard (6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The structured payment-request lane (#91) should extend this column for the
ADVANCE/PART 'exact sum due', not add a parallel field.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The approving Manager decides how much of the PO is paid first, via a
0–100% slider on the approval card (default 100% = full). The slider is
convenience only — the resolved ABSOLUTE amount is stored on
PurchaseOrder.suggestedAdvancePayment (Decimal(12,2), nullable).
- schema + migration: add suggestedAdvancePayment (null = no explicit
advance ⇒ full payment, preserves legacy behaviour).
- approvePo(): accepts the amount, clamps to [0, totalAmount], persists
it, records it on the APPROVED audit row. Set once at approval; never
edited afterwards.
- approval-actions.tsx: whole-percent slider showing the resolved ₹
amount + remaining balance; value sent with Approve / Approve-with-Remarks.
- Accounts surface: payment queue + PO detail show the advance; it
prefills the FIRST payment amount (only when nothing is paid yet and
it is a true partial). Balance runs the normal PARTIALLY_PAID loop.
- Not shown on the exported PO/invoice (po-export-layout untouched).
- Tests: persist + audit metadata + clamp-to-total.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rank held applies to every candidate, not just ex-hands; it auto-updates
for returning crew on sign-off. Ex-hand designation is decoupled from the
Source dropdown and owned by the office:
- Candidate form: drop the EX_HAND source option, relabel "Rank held
(ex-hands)" to "Rank held". addCandidate always intakes NEW/CANDIDATE
(ex-hand recognition still reuses an existing EX_HAND row); updateCandidate
no longer rewrites type/status, so an admin-set EX_HAND or onboarded
EMPLOYEE is never clobbered by a candidate edit.
- Admin crew form: the type NEW/EX_HAND select becomes an "Ex-hand
(returning crew)" checkbox -- the only place ex-hand is tagged.
- List/detail ex-hand indicators key on type === EX_HAND (not source).
- Sign-off preserves the original recruitment source when flipping to EX_HAND.
- Tests seed EX_HAND rows directly; assert candidate intake stays NEW.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Purchasing, Crewing and Administration headings are now collapsible
buttons (chevron + aria-expanded/aria-controls) that collapse by
default. Single-open accordion: opening one heading collapses any
other open one. The section containing the active route auto-expands
on mount/navigation so the user is never stranded on a hidden link.
Adds a jsdom/Testing Library unit test covering default-collapsed,
toggle, single-open accordion, and active-route auto-expand.
Fixes#96
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refresh-test-db.sh mirrored prod with `pg_dump --clean`, which only drops objects
that exist in prod. Objects created on pelagia_test by a previously-applied
UNRELEASED migration (e.g. crewing tables/types not yet released to prod) survived
as leftovers, then collided with the `migrate deploy` replay ("type already
exists", P3009) — blocking every later migration and 500-ing the staging app.
Drop+recreate `public` before the restore so the test DB starts from exactly
prod's objects and unreleased migrations apply cleanly. Adds a guard that refuses
to run unless the derived target is the expected test DB.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Extract EpfoService's pure stub + validation logic into a dependency-free
module (EpfoService/src/stub.ts); index.ts now uses it in its stub branches so
the tested logic IS the production stub behaviour.
- epfo.test.ts (App integration): the deterministic stub contract
(OTP 000000 → matched, UAN/OTP validation, session expiry) and the Next proxy
routes' verify_bank_epf gate — 401 unauthenticated, 403 for the MPO, Accounts
passes through to a mocked upstream, body validated before the upstream call.
No EPFO_LIVE, no running service.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- A3 AC2: each requisition row shows its candidate count (sourced via
_count.applications in the list query) alongside the existing days-open age.
- A3 AC1: add rank and reason filters (derived from the visible data, like the
existing vessel/site filter) on top of search + status + location.
requisitions.test.ts asserts the per-row candidateCount (2 vs 0) the page exposes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- AC1: addCandidate recognizes a returning hand re-entered as a fresh candidate
— matched to their existing EX_HAND pool record by email (preferred) or exact
name — and reuses that row instead of creating a duplicate, preserving tour
history/documents/bank. Audited CANDIDATE_UPDATED { exHandRecognized: true }.
- AC2: the Candidates list sorts ex-hands above new candidates by default
(stable, preserving createdAt order within each group).
- AC3: the candidate detail "Returning crew" callout now renders the matched
member's actual tour history (ExperienceRecord) and documents on file.
candidates.test.ts covers email/name recognition, the no-match path, and the
ex-hand-first page ordering.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- C5 (Epic C5 AC1): advanceStage("verify_competency") now requires ≥1
ReferenceCheck before leaving COMPETENCY_AND_REFERENCES.
- C3 (Epic C3 AC1): verifyDocuments blocks advancement when a mandatory document
for the seat's rank that the candidate holds is expired. Missing-document
presence stays enforced post-onboarding in the verification queue (seafarer
docs aren't collected pre-onboarding) — documented inline + in wiki Tech-Debt.
- C4 (experience): deferred with an inline note (Requisition has no
min-experience field yet — Epic A2 AC1).
applications.test.ts: reference-gate block/pass and expired-required-doc
block/renew-pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- D1: require a Manager-approved SalaryStructure before onboarding; a SELECTED
application with none is now blocked instead of silently binding zero salary
rows.
- D3 AC2: the CREW_ONBOARDED CrewAction records the created IDs
(assignmentId, employeeId, salaryStructureId) in metadata.
- Atomicity: the contract letter is uploaded before the transaction and its row
is created INSIDE it, so onboarding is one atomic write (no half-onboarded
crew member without a contract on failure).
onboarding.test.ts asserts the metadata and the new D1 block (no assignment, the
candidate stays a CANDIDATE).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The crew profile page passed SeafarerDocument.number to the client unmasked for
all roles and all doc types, exposing full Aadhaar/PAN identity numbers to MPO /
Manager / Site staff — contradicting the field's PII annotation and §6 /
Roles-and-Permissions §3 (Aadhaar/PAN are gated to Accounts/SuperUser, same as
the bank account number).
- crew-pii.ts: add documentNumberValue(number, docType, role) — masks AADHAAR /
PAN for non-privileged roles via the existing canViewFullBankEpf gate +
maskTail; non-identity docs (passport, CDC, STCW…) pass through; preserves the
string|null contract.
- crew/[id]/page.tsx: mask the number server-side before it crosses to the client.
- Tests: unit cases for the helper; an integration test that invokes the server
component and asserts the documents prop is masked for MANAGER/SITE_STAFF/MPO
and full for ACCOUNTS/SUPERUSER.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds two integration suites covering reconciliation rulings that the existing
crewing tests left on the happy path only:
- leave-clash.test.ts (R6/A5, §5.3): the cover-subtraction and date-overlap
paths in leaveCausesClash — a same-rank crew already on an *overlapping*
approved leave is not available cover (auto-raises), a non-overlapping leave
still counts (no raise), different-rank crew never count, and a configured
minStrength still met after the leave does not raise.
- crewing-gates.test.ts: salary/selection *returns* are Manager-only and
audited (R8); an interview waiver can never reach a NEW candidate by any path,
incl. the Manager (R2); bank reject requires remarks; PPE / next-of-kin verify
gates are MPO-only with remarks on reject (R11/§8.11); and a SUBMITTED
appraisal cannot be Manager-approved without MPO verification (H3).
Full suite: 245 unit + 225 integration green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
lib/notifier.ts eagerly did `new Resend(process.env.RESEND_API_KEY)` whenever
NODE_ENV !== "development". Resend v4's constructor throws on a missing key, so
in any env without RESEND_API_KEY (CI, non-dev test runs) merely importing the
module crashed — surfaced by crew-records.test.ts once Phase 4c pulled
requisition-service → notifier into the crew actions' import graph.
Construct the client only when a key is present; otherwise fall back to console
logging (the send branches now gate on `!resend` instead of `isDev`). Verified by
running the full integration suite with RESEND_API_KEY unset (195 pass).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Office/admin crewing-management surface behind a new manage_crew permission
(Manager + SuperUser + Admin). Stacks on 4b. Behind NEXT_PUBLIC_CREWING_ENABLED.
What's in
- Permission: manage_crew added to the §6 matrix (MGR/SU/ADMIN).
- Direct placement (placeCrew): a Manager assigns a crew member to a vessel/site
WITHOUT a requisition — creates an ACTIVE CrewAssignment, promotes a candidate to
EMPLOYEE with a CRW- number (generateEmployeeId), blocked if already actively
assigned.
- Admin crew CRUD: createCrewMember / updateCrewMember / deleteCrewMember (delete
blocked when assignments/applications exist).
- Crew strength config: upsert/delete VesselRankRequirement (the minStrength that
drives R6 leave-clash detection).
- Screens under Administration (flag-gated, MGR/SU/ADMIN): /admin/crew (list + add/
edit/delete + Place modal) and /admin/crew-strength (requirement table + form).
Tests & docs
- Unit: permissions-crewing.test.ts gains a manage_crew check. Integration:
crewing-admin.test.ts (9) — CRUD, delete guard, direct placement (+promotion,
+active-assignment guard), strength upsert/delete, manage_crew gating.
type-check clean; full unit (241) + integration (192) green.
- CLAUDE.md updated with the crewing-admin surface.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>