docs: Crewing management System

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Hardik 2026-06-21 20:51:02 +05:30
parent 2029ae7083
commit cd4f85918c
9 changed files with 1638 additions and 0 deletions

188
Crewing-Architecture.md Normal file

@ -0,0 +1,188 @@
# Crewing Architecture
The Crewing Module is **not a new application** — it is a feature area inside the
existing Pelagia Portal. It adopts the same stack and the same patterns
documented in [Architecture](Architecture); this page only describes what
crewing *adds*.
## 1. Where it lives in the stack
```
┌─────────────────────────────────────────────┐
│ Browser — React 19 + shadcn/ui + Tailwind │
│ Crewing pages: requisitions, candidates, │
│ crew, leave planner, attendance, payroll │
└───────────────────┬──────────────────────────┘
│ HTTPS
┌───────────────────▼──────────────────────────┐
│ Next.js 15 App Server │
│ RSC pages (read) │ Server Actions (write) │
│ ┌──────────────────────────────────────────┐│
│ │ requisition-state-machine ││
│ │ application-pipeline (candidate gates) ││
│ │ onboarding side-effects ││
│ │ wage-report generator ││
│ │ permissions (extended) · crew-notifier ││
│ └──────────────────────────────────────────┘│
└───────┬───────────────┬───────────────┬───────┘
│ │ │
┌───────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ PostgreSQL │ │ R2 / local │ │ Resend │
│ (Prisma) │ │ docs, CVs, │ │ (email) │
│ crew tables │ │ PPE, signs │ │ │
└──────────────┘ └─────────────┘ └─────────────┘
┌───────┴──────────────────────────────────────┐
│ (future) CvParseService · EpfoService │
│ best-effort CV extraction; UAN/Aadhaar check │
│ same out-of-process pattern as GstService │
└───────────────────────────────────────────────┘
```
Reused as-is: NextAuth v5 session/auth, `lib/storage.ts` (file storage),
`lib/notifier.ts` (email + `Notification` rows), `Vessel`/`Site`/`User`,
Recharts for dashboards.
## 2. Component diagram
```mermaid
flowchart TB
subgraph UI["UI — app/(portal)/crewing/*"]
REQ["Requisitions<br/>/crewing/requisitions"]
CAND["Candidates &amp; Pipeline<br/>/crewing/candidates"]
CREW["Crew Directory<br/>/crewing/crew/[id]"]
PLAN["Leave Planner<br/>/crewing/leave"]
ATT["Attendance<br/>/crewing/attendance"]
PPE["PPE Register<br/>/crewing/ppe"]
APPR["Appraisals<br/>/crewing/appraisals"]
PAY["Wage Reports<br/>/crewing/payroll"]
VERI["Office Verification<br/>/crewing/verify"]
APPLY["Public Apply<br/>/careers/apply"]
end
subgraph SA["Server Actions — actions.ts (mutations)"]
A1["requisition actions"]
A2["application/pipeline actions"]
A3["onboarding action"]
A4["crew-record actions<br/>(docs/bank/epf/nok)"]
A5["ppe actions"]
A6["leave + attendance actions"]
A7["appraisal actions"]
A8["payroll actions"]
A9["verification actions"]
end
subgraph LIB["Domain logic — lib/"]
SM1["requisition-state-machine.ts"]
SM2["application-pipeline.ts"]
SM3["assignment-lifecycle.ts"]
SM4["appraisal-lifecycle.ts"]
ON["onboarding.ts (side-effects)"]
WR["wage-report.ts (generator)"]
PERM["permissions.ts (extended)"]
NOT["crew-notifier.ts → notifier.ts"]
REQDOC["rank-doc-requirements.ts"]
end
subgraph RH["Route Handlers — api/ (non-mutations)"]
H1["/api/careers/apply (public POST)"]
H2["/api/crewing/cv-parse"]
H3["/api/crewing/payroll/[id]/export"]
H4["/api/files/sign (reused)"]
end
subgraph DATA["Persistence"]
DB[("PostgreSQL<br/>Prisma crew models")]
FS[("File storage<br/>CVs · docs · PPE · signatures")]
end
subgraph EXT["External / future"]
CVP["CvParseService"]
EPFO["EpfoService"]
MAIL["Resend / console"]
end
REQ --> A1 --> SM1 --> DB
CAND --> A2 --> SM2 --> DB
CAND --> A3 --> ON --> DB
ON --> SM3
APPLY --> H1 --> H2 --> CVP
H1 --> DB
CREW --> A4 --> DB
A4 --> REQDOC
PPE --> A5 --> DB
PLAN --> A6
ATT --> A6 --> DB
APPR --> A7 --> SM4 --> DB
PAY --> A8 --> WR --> DB
PAY --> H3
VERI --> A9 --> DB
A9 --> EPFO
A1 & A2 & A3 & A7 & A8 --> NOT --> MAIL
A1 & A2 & A3 & A4 & A5 & A6 & A7 & A8 & A9 --> PERM
A4 & A5 & H1 --> FS
WR -.reads.-> DB
```
## 3. Module layout
```
app/(portal)/crewing/
├── requisitions/ # list, new, [id] (vacancies)
├── candidates/ # pipeline board, [id] (per-candidate gates)
│ └── [id]/onboard/ # joining formalities → CrewAssignment
├── crew/ # directory, crew/[id] (records, docs, bank, epf, ppe)
├── leave/ # site leave planner + apply
├── attendance/ # daily capture per site
├── ppe/ # PPE issue register
├── appraisals/ # raise / verify / approve
├── payroll/ # monthly wage reports
└── verify/ # office verification queue (MPO / Accounts)
app/careers/apply/ # public candidate application (CV upload)
app/api/
├── careers/apply/ # public POST (rate-limited)
├── crewing/cv-parse/ # CV → extracted fields
└── crewing/payroll/[id]/export/ # XLSX/PDF wage report
lib/
├── requisition-state-machine.ts # vacancy transitions + roles
├── application-pipeline.ts # candidate gate transitions
├── assignment-lifecycle.ts # active ↔ on-leave → signed-off
├── appraisal-lifecycle.ts # draft → verified → approved
├── onboarding.ts # the side-effecting onboard step
├── wage-report.ts # crew × salary × days generator
├── rank-doc-requirements.ts # which docs a rank needs
├── crew-notifier.ts # crew events → notifier.ts
└── permissions.ts # + crewing permissions
```
This mirrors the PO module's split: **Server Actions for every mutation**,
**route handlers only** for the public apply endpoint, CV parsing, file signing
and report export. State changes go through a single state-machine module and
are recorded as `CrewAction` rows.
## 4. Cross-cutting concerns
| Concern | Approach (reused) |
|---|---|
| **Auth** | NextAuth v5 session; public apply route is unauthenticated + rate-limited. |
| **Authorisation** | `requirePermission(role, permission)` at the top of every Server Action; RSC pages gate whole segments. See [Crewing Roles & Permissions](Crewing-Roles-and-Permissions). |
| **Audit** | `CrewAction` rows (actor, type, note, metadata) — the `POAction` pattern. |
| **Files** | `lib/storage.ts` signed uploads (R2 prod / local dev) for CVs, documents, PPE proofs, signatures. |
| **Email / in-app** | `lib/notifier.ts`; events persisted as `Notification`. |
| **Decimals** | salary/victualing are `Decimal`; convert with `Number()` before crossing into Client Components (same gotcha as PO line items). |
| **Feature flag** | whole surface behind `NEXT_PUBLIC_CREWING_ENABLED` for staged rollout. |
| **Migrations** | new models = new Prisma migration, reviewed in a PR; never edit `schema.prisma` without one. |
## 5. Integration points with the PO module
- **Cost centre** — crew assignments reference the same `Vessel`/`Site` used by
POs, so crew cost and PO spend share a cost-centre axis for reporting.
- **PPE as inventory (optional)** — if the [inventory flag](Inventory-and-Catalogue)
is on, PPE issues can draw down `ItemInventory` at a site, reusing the
consumption model.
- **Notifications & dashboard** — crew events surface in the same notification
bell; the dashboard gains crewing stat cards.

349
Crewing-Data-Model.md Normal file

@ -0,0 +1,349 @@
# Crewing Data Model
**Proposed source of truth:** `App/prisma/schema.prisma` (new models). Mirrors
the conventions on [Data Model](Data-Model): monetary values are `Decimal`,
IDs are `cuid()`, every lifecycle change writes an audit row.
## 1. Enums
```prisma
// who/what kind of vacancy
enum RequisitionReason { LEAVE END_OF_CONTRACT TERMINATION MEDICAL OTHER }
enum RequisitionStatus { OPEN SHORTLISTING PROPOSING INTERVIEWING SELECTED FILLED CANCELLED }
// candidate progress against a requisition (the gated pipeline)
enum CandidateType { EX_HAND NEW }
enum ApplicationStage {
SHORTLISTED COMPETENCY_CHECK DOC_VERIFICATION REFERENCE_CHECK
SALARY_AGREEMENT PROPOSED INTERVIEW SELECTED REJECTED JOINING ONBOARDED
}
// the person's overall status
enum CrewStatus { PROSPECT CANDIDATE EMPLOYEE EX_HAND BLACKLISTED }
// an onboarded tour of duty
enum AssignmentStatus { ACTIVE ON_LEAVE SIGNED_OFF }
// site HR
enum AttendanceStatus { PRESENT ABSENT HALF_DAY ON_LEAVE SIGN_OFF }
enum LeaveType { ANNUAL MEDICAL EMERGENCY UNPAID OTHER }
enum LeaveStatus { APPLIED APPROVED REJECTED CANCELLED }
// PPE kit
enum PpeItem {
BOILER_SUIT SAFETY_SHOES HELMET VEST GLOVES MASK GOGGLES
TIFFIN TORCH WALKIE_TALKIE
}
// documents
enum SeafarerDocType {
STCW AADHAAR PAN PASSPORT CDC COC PHOTOGRAPH
DRIVING_LICENSE MEDICAL_FITNESS CONTRACT_LETTER
}
enum VerificationStatus { PENDING VERIFIED REJECTED EXPIRED }
// appraisal + payroll
enum AppraisalStatus { DRAFT SUBMITTED MPO_VERIFIED MANAGER_APPROVED REJECTED }
enum WageReportStatus { DRAFT GENERATED MANAGER_APPROVED SENT_TO_ACCOUNTS }
// audit
enum CrewActionType {
REQUISITION_RAISED REQUISITION_CANCELLED REQUISITION_FILLED
CANDIDATE_SHORTLISTED GATE_PASSED GATE_FAILED PROPOSED INTERVIEWED
SELECTED REJECTED ONBOARDED SIGNED_OFF
RECORD_UPDATED RECORD_VERIFIED RECORD_REJECTED
PPE_ISSUED PPE_RETURNED LEAVE_APPLIED LEAVE_DECIDED
APPRAISAL_SUBMITTED APPRAISAL_VERIFIED APPRAISAL_APPROVED
WAGE_REPORT_GENERATED WAGE_REPORT_APPROVED WAGE_REPORT_SENT
}
```
## 2. Rank — the org hierarchy
`Rank` is reference data with a self-relation (parent/children), exactly like the
3-level `Account` accounting-code hierarchy. It drives requisition criteria,
required documents per rank, and the org chart. Captured from
`Crewing.excalidraw`:
```mermaid
flowchart TB
PM["PM"] --> APM["Ass. PM"]
APM --> ACC["Accountant"]
APM --> DRV["Driver"]
APM --> COOK["Cook"]
COOK --> CH["Cook Helper"]
APM --> SIC["Site in-charge"]
SIC --> DIC["Dredger in-charge"]
DIC --> SDO["Sr. Dredge Op."]
SDO --> PLS["Pipeline Supervisor"]
PLS --> PLA["Pipeline Ass."]
SDO --> JDO["Jr. Dredge Op."]
JDO --> ERO["Engine Room Op."]
ERO --> DH["Deck Hand"]
DH --> TR["Trainee"]
DH --> MB["Mess Boy"]
SDO --> ELE["Electrician"]
SDO --> FAB["Sr. Fab"]
FAB --> FW["Fab / Welder"]
classDef sup fill:#eef,stroke:#88a;
class ACC,DRV,COOK,CH sup;
```
Ranks carry a `category` (e.g. `OPERATIONAL` vs `SUPPORT`) and an
`isSeafarer` flag (drives which document set applies). `Driver` additionally
requires a driving licence (`RankDocRequirement`).
## 3. Entity-relationship diagram
```mermaid
erDiagram
USER ||--o{ CREW_ACTION : actor
USER ||--o{ REQUISITION : raised_by
USER ||--o{ APPRAISAL : "added/verified/approved"
RANK ||--o{ RANK : parent
RANK ||--o{ REQUISITION : for_rank
RANK ||--o{ CREW_ASSIGNMENT : in_rank
RANK ||--o{ RANK_DOC_REQUIREMENT : requires
VESSEL ||--o{ REQUISITION : cost_centre
VESSEL ||--o{ CREW_ASSIGNMENT : on
SITE ||--o{ WAGE_REPORT : for_site
REQUISITION ||--o{ APPLICATION : receives
REQUISITION ||--o| CREW_ASSIGNMENT : filled_by
REQUISITION }o--|| CREW_MEMBER : vacated_by
CREW_MEMBER ||--o{ APPLICATION : applies
CREW_MEMBER ||--o{ CREW_ASSIGNMENT : has
CREW_MEMBER ||--o{ SEAFARER_DOCUMENT : holds
CREW_MEMBER ||--o| BANK_DETAIL : has
CREW_MEMBER ||--o| EPF_DETAIL : has
CREW_MEMBER ||--o{ NEXT_OF_KIN : has
CREW_MEMBER ||--o{ EMERGENCY_CONTACT : has
CREW_MEMBER ||--o{ EXPERIENCE_RECORD : has
APPLICATION ||--o{ APPLICATION_GATE : evaluated_by
APPLICATION ||--o{ REFERENCE_CHECK : has
CREW_ASSIGNMENT ||--o| SALARY_STRUCTURE : paid_by
CREW_ASSIGNMENT ||--o| CONTRACT_LETTER : documented_by
CREW_ASSIGNMENT ||--o{ ATTENDANCE : records
CREW_ASSIGNMENT ||--o{ LEAVE_REQUEST : has
CREW_ASSIGNMENT ||--o{ PPE_ISSUE : issued
CREW_ASSIGNMENT ||--o{ APPRAISAL : appraised
CREW_ASSIGNMENT ||--o{ WAGE_LINE : billed
WAGE_REPORT ||--o{ WAGE_LINE : contains
CREW_MEMBER {
string id PK
string employeeId "unique, set at onboard"
string name
date dob
string phone
string email
enum status "CrewStatus"
enum type "EX_HAND|NEW"
string cvKey "storage key"
string currentRankId FK
}
REQUISITION {
string id PK
string vesselId FK
string rankId FK
enum reason "RequisitionReason"
enum status "RequisitionStatus"
string vacatedByCrewId FK
int minExperienceMonths
string vesselTypeCriteria
string raisedById FK
bool autoRaised
date neededBy
}
APPLICATION {
string id PK
string requisitionId FK
string candidateId FK
enum type "CandidateType"
enum stage "ApplicationStage"
decimal proposedSalary
string remarks
bool interviewWaived
}
APPLICATION_GATE {
string id PK
string applicationId FK
string gate "competency|document|experience|reference|salary|interview"
enum result "PENDING|VERIFIED|REJECTED"
string note
string decidedById FK
}
CREW_ASSIGNMENT {
string id PK
string crewMemberId FK
string vesselId FK
string rankId FK
string requisitionId FK
date signOnDate
date signOffDate
enum status "AssignmentStatus"
}
SALARY_STRUCTURE {
string id PK
string assignmentId FK
decimal basic
decimal victualingPerDay
json allowances
string currency
string approvedById FK "Manager"
}
CONTRACT_LETTER {
string id PK
string assignmentId FK
string fileKey
bool salaryRestricted "hide salary from site staff"
}
ATTENDANCE {
string id PK
string assignmentId FK
date date
enum status "AttendanceStatus"
string recordedById FK
}
LEAVE_REQUEST {
string id PK
string assignmentId FK
enum type "LeaveType"
date fromDate
date toDate
enum status "LeaveStatus"
string appliedById FK
}
PPE_ISSUE {
string id PK
string assignmentId FK
enum item "PpeItem"
string size "for boiler suit / shoes"
int quantity
date issuedDate
date returnedDate
string issuedById FK
}
SEAFARER_DOCUMENT {
string id PK
string crewMemberId FK
enum docType "SeafarerDocType"
string number "masked"
string fileKey
date issueDate
date expiryDate
enum verificationStatus "VerificationStatus"
string verifiedById FK "MPO"
}
BANK_DETAIL {
string id PK
string crewMemberId FK
string accountName
string accountNumberEnc "encrypted"
string ifsc
string bankName
enum verificationStatus "by Accounts"
string verifiedById FK
}
EPF_DETAIL {
string id PK
string crewMemberId FK
string uan
string aadhaarLast4
string pfNumber
enum verificationStatus "EPFO check by Accounts"
string verifiedById FK
}
EXPERIENCE_RECORD {
string id PK
string crewMemberId FK
string vesselType
string rankId FK
date fromDate
date toDate
int durationMonths
string source "internal|declared"
}
APPRAISAL {
string id PK
string assignmentId FK
string period
json ratings
string comments
enum status "AppraisalStatus"
string addedById FK "PM"
string verifiedById FK "MPO"
string approvedById FK "Manager"
}
WAGE_REPORT {
string id PK
string siteId FK
string period "YYYY-MM"
enum status "WageReportStatus"
decimal totalAmount
string generatedById FK
string approvedById FK
}
WAGE_LINE {
string id PK
string wageReportId FK
string assignmentId FK
int daysAttended
decimal dailyRate
decimal victualing
decimal lineTotal
}
CREW_ACTION {
string id PK
string entityType "requisition|application|assignment|..."
string entityId
enum actionType "CrewActionType"
string actorId FK
string note
json metadata
datetime createdAt
}
```
## 4. Model notes
- **`CrewMember`** is the spine: a row exists from the first application
(`PROSPECT`/`CANDIDATE`) and persists as `EMPLOYEE` then `EX_HAND`. This is
what makes "ex-hand preferred" cheap — their experience and documents are
already on file. `employeeId` is assigned at onboarding.
- **`Application` vs `Requisition`** — one requisition, many applications;
`APPLICATION_GATE` rows are the audit of each vetting gate (the columns in
notebook page 2: competency, document, experience, reference, salary,
interview). `interviewWaived` is set true for ex-hands.
- **`CrewAssignment`** is a single tour of duty. Onboarding creates it and
flips the requisition to `FILLED`. Sign-off sets `signOffDate`/`SIGNED_OFF`,
appends an `EXPERIENCE_RECORD`, and triggers a backfill requisition.
- **Verification is a field**`verificationStatus + verifiedById` on
documents, bank and EPF. Routing rule (from the notebook): site-entered data →
**MPO**; bank + EPF → **Accounts**; attendance is operational (no separate
verify gate).
- **`SalaryStructure`** holds `basic`, `victualingPerDay`, and a JSON
`allowances`; approved by a Manager. Salary is the restricted field on the
contract letter for site staff.
- **`WageReport` / `WageLine`** are the generated monthly artefact:
`lineTotal = daysAttended × dailyRate + victualing`; the report
`totalAmount` is the sum. Exportable to XLSX/PDF like a PO.
- **PPE as inventory** — when the inventory flag is on, a `PPE_ISSUE` can also
decrement `ItemInventory` at the site (optional integration).
## 5. Relationship to existing models
| Existing model | Crewing relationship |
|---|---|
| `Vessel` (cost centre) | `Requisition.vesselId`, `CrewAssignment.vesselId` |
| `Site` | `WageReport.siteId`; site grouping for attendance & leave planner; optional PPE inventory |
| `User` | actors on every `CrewAction`; raisers/verifiers/approvers |
| `Notification` | crew events reuse the in-app bell + email log |
| `ItemInventory` (flagged) | optional PPE draw-down |

164
Crewing-Design-Document.md Normal file

@ -0,0 +1,164 @@
# Crewing Design Document
> Companion pages: [Architecture](Crewing-Architecture) ·
> [Data Model](Crewing-Data-Model) · [Workflows](Crewing-Workflows) ·
> [Use Cases](Crewing-Use-Cases) · [Roles & Permissions](Crewing-Roles-and-Permissions) ·
> [User Stories](Crewing-User-Stories)
## 1. Purpose & scope
The Crewing Module digitises Pelagia's **manning process**: keeping every
dredger and site crewed with competent, documented, paid people, and recording
the full lifecycle of each crew member.
**In scope**
1. Vacancy detection & **requisitions**.
2. **Recruitment pipeline** (shortlist → vetting → proposal → interview →
selection) for ex-hands and new candidates.
3. **Onboarding** and the automatic processes it kicks off.
4. **Crew records**: documents, bank, EPF, next-of-kin, emergency contact,
contract, PPE.
5. **Site HR**: leave planner, attendance.
6. **Appraisal** (PM → MPO → Manager).
7. **Sign-off** and experience accrual.
8. **Wage report** generation for Accounts.
**Out of scope (v1)** — actual salary disbursement / banking integration (we
produce the report; Accounts pays), payslip generation, statutory return filing,
training-course delivery, and seafarer-document issuance (we store and verify,
we don't issue).
## 2. Actors
| Actor | Real-world role | Maps to PPMS role |
|---|---|---|
| **Site staff** | PM / Assistant PM / Site In-charge on the dredger | new `SITE_STAFF` role (proposed) |
| **MPO** | Manning / Personnel Officer in the office | existing `MANNING` role |
| **Accounts** | Finance team | existing `ACCOUNTS` role |
| **Manager** | Department / crewing manager | existing `MANAGER` role |
| **Candidate** | External applicant (ex-hand or new) | unauthenticated public form / lightweight `CANDIDATE` identity |
| **Crew member** | Onboarded employee | data subject; not necessarily a portal login in v1 |
| Admin / Superuser / Auditor | as in PO module | existing roles |
See the on-vessel **rank hierarchy** (PM → … → Mess Boy) in
[Data Model § Rank](Crewing-Data-Model#rank-the-org-hierarchy).
## 3. Domain narrative
### 3.1 A vacancy opens
A crew member goes on leave, finishes a contract (EOC), is terminated, is
signed off on medical grounds, or leaves for another reason. Any of these
creates a **gap** for a specific **rank** on a specific **vessel/site**. On a
*scheduled sign-off* the gap (and its **Requisition**) is raised automatically;
otherwise site staff or the MPO raise it manually.
### 3.2 Sourcing candidates
The MPO shortlists candidates for the rank. **Ex-hands are preferred** (people
who have sailed with Pelagia before — their record is already in the system).
New candidates either **apply on the Pelagia site** (uploading a CV, which is
**parsed** for age, vessel type, current/past rank and experience — with manual
entry as fallback) or are **uploaded manually** by the MPO.
### 3.3 Vetting (gated)
Each shortlisted candidate is evaluated against the rank's **minimum criteria**:
- **Competency** — qualifications for the rank.
- **Document verification** — seafarer documents present and valid.
- **Experience***type of vessel*, *time in rank*, *total duration*.
- **Reference check** — previous-employer confirmation.
- **Salary agreement** — proposed structure accepted in principle.
The candidate is then **proposed** and (except for ex-hands, where it's
**optional**) **interviewed**. The outcome is **Selection** or **Rejection with
remarks**. Every gate is recorded.
### 3.4 Onboarding
On selection: **joining formalities** run — salary is confirmed and a
**contract letter** issued. Creating the **CrewAssignment** automatically starts:
- **salary** (the agreed structure becomes active),
- **victualing** (food/messing allowance accrual),
- **attendance** capture,
- **experience** accrual on this vessel/rank,
- **PF** tracking (UAN/EPF), and
- the **PPE issue** checklist.
The crew member receives an **Employee ID**.
### 3.5 Life on site
Site staff record **attendance** daily, **issue PPE** (with boiler-suit and shoe
sizes), maintain **documents/bank/EPF/next-of-kin/emergency contact**, and apply
for **leave** through the site **leave planner** (which shows every crew
member's contract span and leaves on one timeline). The PM raises **appraisals**;
the **MPO verifies**, the **Manager approves**.
### 3.6 Office verification
The office is the source of truth for verified data. **The MPO verifies
everything site staff enter, except attendance**; **bank details and EPF are
verified by Accounts** (UAN/Aadhaar checked against EPFO).
### 3.7 Sign-off & backfill
When a crew member signs off, the assignment closes, their **experience record
is updated** with the completed tour, and a **backfill Requisition** is raised
automatically — closing the loop back to §3.1.
### 3.8 Payroll feed
At month end a **Wage Report** is generated per site:
`crew × salary × days-attended (+ victualing)`. It is reviewed, **approved by the
Manager**, and **sent to Accounts** for disbursement.
## 4. Salient features & design decisions
| # | Decision | Rationale |
|---|---|---|
| D1 | **Two coupled state machines**`Requisition` (the vacancy) and `Application` (a candidate's progress against a requisition). | The requisition is "is this seat filled?"; the application is the per-candidate gated pipeline. Many applications per requisition; exactly one ends in onboarding. |
| D2 | **Unified `CrewMember`** record with a `status` (PROSPECT → CANDIDATE → EMPLOYEE → EX_HAND). | "Ex-hand preferred" only works if a candidate and an ex-employee are the same record; experience carries forward automatically. |
| D3 | **Onboarding is a side-effecting transition**, exactly like PO approval increments inventory. | Single source of truth: salary/victualing/attendance/experience/PF/PPE all start from one event, recorded as one `CrewAction`. |
| D4 | **Verification is a field-level state**, not a separate workflow. Each verifiable record carries `verificationStatus + verifiedBy`. Routing: site-entered → MPO; bank/EPF → Accounts. | Mirrors "view-only / upload / verify" split from the notebook; keeps the office as the trust boundary. |
| D5 | **Sensitive fields are masked & gated.** Salary on the contract letter, full bank account number, and Aadhaar/PAN are restricted; site staff get *view-only-except-salary* on contracts and *view-only* bank. | Matches the explicit notebook rules and basic PII hygiene. |
| D6 | **Rank is reference data with a self-hierarchy** (3+ levels), mirroring the `Account` accounting-code hierarchy. | Drives requisition criteria, document requirements (e.g. licence for drivers), and the org chart. |
| D7 | **Document requirements are rank-driven.** A `RankDocRequirement` defines which seafarer docs a rank must hold. | "Seafarer docs (based on role)"; driving licence only for drivers. |
| D8 | **Wage report is a generated artefact**, not live-computed on the Accounts screen. | Auditable monthly snapshot; mirrors PO export/report pattern. |
| D9 | **Reuse `Vessel`/`Site` as the cost centre** so crew cost is attributable on the same axis as PO spend. | Single cost-centre vocabulary across the portal. |
| D10 | **CV parsing is best-effort with manual fallback.** Extraction populates a draft; a human confirms. | Never block intake on a parser; keep the office in control. |
## 5. Non-functional requirements
- **Auditability** — every lifecycle transition and verification writes a
`CrewAction` row (actor, type, note, metadata), like `POAction`.
- **Privacy** — bank/EPF/identity documents are PII: stored via the same
signed-URL [file storage](File-Storage); account numbers and identity numbers
stored masked/encrypted; access gated by permission.
- **Consistency** — all mutations are Server Actions calling `requirePermission`
then the relevant state-machine guard; **no mutation REST endpoints**.
- **Notifications** — declarative per-transition email/in-app side-effects via
`lib/notifier.ts` (e.g. requisition raised → MPO; proposal → Manager;
appraisal verified → Manager; wage report ready → Accounts).
- **Document expiry** — documents with `expiryDate` surface upcoming-expiry
warnings (medical fitness, passport, CDC, STCW).
## 6. Assumptions
1. PM/APM/Site In-charge get a dedicated `SITE_STAFF` role rather than reusing
`TECHNICAL`. (Confirm in review — see Open Questions.)
2. Crew members are **data subjects**, not portal logins, in v1. A future phase
may give crew a self-service login.
3. **EPFO** UAN/Aadhaar verification is a manual/assisted step in v1 (record the
result); a programmatic check can follow the GstService precedent.
4. "Victualing" is a per-day messing allowance, configurable per rank/vessel.
5. One **active** assignment per crew member at a time.
## 7. Open questions
| # | Question |
|---|---|
| Q1 | New role `SITE_STAFF`, or extend `TECHNICAL`/`MANNING`? |
| Q2 | Does the candidate self-apply form live inside the portal (public route) or on the marketing site posting to an API? |
| Q3 | EPFO verification — assisted-manual now, or build an EPFO proxy microservice like GstService? |
| Q4 | Should salary structures be versioned per assignment (raises mid-contract) in v1, or single-structure-per-assignment? |
| Q5 | Is victualing part of the wage report total, or a separate accrual line? |
| Q6 | Do we need crew self-service (view own docs/payslip) in v1 or v2? |
| Q7 | Attendance granularity — daily present/absent only, or shift/hours? |
See the requirement backlog in [Crewing User Stories](Crewing-User-Stories).

72
Crewing-Module.md Normal file

@ -0,0 +1,72 @@
# Crewing Module
The **Crewing Module** (a.k.a. *Manning*) extends Pelagia Portal (PPMS) from a
purchasing system into a crew lifecycle system. It manages the people who run
Pelagia's dredgers and sites — from the moment a vacancy opens, through
recruitment and onboarding, to day-to-day site HR (attendance, leave, PPE,
documents) and finally to the monthly wage report that feeds Accounts.
It reuses the platform primitives already proven by the [PO module](Purchase-Orders):
a single **state machine** per lifecycle, a centralised **permission map**, an
**audit trail** of actions, **file storage** for documents, and the **notifier**
for email/in-app events.
## Why software
Today crewing lives in notebooks, WhatsApp, and spreadsheets (the source notes
for this design were literally two notebook pages). The pain points it removes:
- **Vacancies are reactive and untracked.** A sign-off or leave creates a gap
that nobody formally owns until a vessel is short-handed. → Auto-raised
**Requisitions** on sign-off / end-of-contract.
- **Candidate vetting is ad-hoc.** Competency, document, reference and salary
checks happen over chat with no record of who cleared what. → A gated
**Candidate Pipeline** with an event log.
- **Site data and office verification are disconnected.** PMs hold PPE/leave/doc
data on paper; the office can't see or verify it. → Site-entered records with
an explicit **MPO / Accounts verification** step.
- **Wage calculation is manual.** Days-attended × salary is re-derived by hand
every month. → A generated **Wage Report** per site.
## What it does (salient features)
| Area | Capability |
|---|---|
| **Requisitions** | Auto- or manually-raised vacancy for a rank on a vessel/site; reason (leave / end-of-contract / termination / medical / other). |
| **Candidate pipeline** | Shortlist (ex-hand preferred) → competency → document verification → reference check → salary agreement → proposal → interview (optional for ex-hands) → selection / rejection with remarks. |
| **CV intake** | Self-apply on the Pelagia site or MPO upload; CV parsed for age, vessel type, rank and experience, with manual fallback. |
| **Onboarding** | One selection event starts salary, victualing, attendance, experience accrual, PF tracking and the PPE issue checklist. |
| **Crew records** | Bank details, EPF/UAN, next of kin, emergency contact, contract letter, and role-based seafarer documents (STCW, Aadhaar, PAN, Passport, CDC, COC, photo, licence, medical fitness). |
| **PPE issue register** | Boiler-suit & shoe sizes plus issue tracking for the full PPE kit. |
| **Leave & attendance** | Site-level leave planner (contracts + leaves on one timeline) and daily attendance capture. |
| **Appraisal** | PM raises → MPO verifies → Manager approves. |
| **Sign-off** | Updates experience, closes the assignment, and auto-raises the backfill requisition. |
| **Wage report** | Monthly, per site: crew × salary × days attended (+ victualing), generated and sent to Accounts. |
| **Verification** | Office sign-off: MPO verifies everything except attendance; bank + EPF verified by Accounts. |
## How it fits PPMS
- **Same stack** — Next.js 15 App Router, Prisma + PostgreSQL, NextAuth v5,
Tailwind v4, shadcn/ui. See [Crewing Architecture](Crewing-Architecture).
- **Same patterns**`lib/requisition-state-machine.ts` and
`lib/application-pipeline.ts` mirror `lib/po-state-machine.ts`; a `CrewAction`
audit row mirrors `POAction`; verification/permission gating reuses
`requirePermission`.
- **Shared entities**`Vessel` (cost centre), `Site`, `User`, `Notification`
are reused; crew salary cost is attributable to the same cost-centre concept
used by POs.
## Page map
| Page | Purpose | Detail |
|---|---|---|
| [Crewing Design Document](Crewing-Design-Document) | Goals, scope, domain narrative, decisions | — |
| [Crewing Architecture](Crewing-Architecture) | System & component diagrams, module layout | Component diagram |
| [Crewing Data Model](Crewing-Data-Model) | Entities, enums, relationships | **ER diagram** |
| [Crewing Workflows](Crewing-Workflows) | Lifecycles & flows | **Sequence + state diagrams** |
| [Crewing Use Cases](Crewing-Use-Cases) | Actors × use cases | **Use-case diagram** |
| [Crewing Roles and Permissions](Crewing-Roles-and-Permissions) | Permission matrix extension | — |
| [Crewing User Stories](Crewing-User-Stories) | Backlog by epic with acceptance criteria | — |
> **Status:** design proposal. Entity, route and permission names are
> proposals to be confirmed in review (see [Open Questions](Crewing-Design-Document#open-questions)).

@ -0,0 +1,102 @@
# Crewing Roles and Permissions
Crewing extends the existing `lib/permissions.ts` map (see
[Roles and Permissions](Roles-and-Permissions)) with crewing permissions. Every
crewing Server Action calls `requirePermission(role, permission)` first; the
relevant state machine adds the status+role gate on top.
## 1. Role mapping
| Crewing actor | PPMS role | Notes |
|---|---|---|
| PM / APM / Site In-charge | **`SITE_STAFF`** (new, proposed) | apply-only leave, attendance, PPE issue, doc upload, view-only contract (except salary) & bank |
| MPO | **`MANNING`** (existing — "crew-management staff") | recruitment + verifies all site data except bank/EPF |
| Accounts | **`ACCOUNTS`** | verifies bank + EPF; consumes wage report |
| Manager | **`MANAGER`** | approves salary structures, candidate list, appraisals, wage reports |
| Superuser | **`SUPERUSER`** | combined authority across crewing actions |
| Auditor | **`AUDITOR`** | read-only across crewing |
| Admin | **`ADMIN`** | manages ranks & doc requirements |
| Candidate | none | public careers form only |
> **Proposed enum change:** add `SITE_STAFF` to `Role`. If review prefers reuse,
> `TECHNICAL` could host site staff, but its PO semantics ("deck/engine crew")
> differ from PM/APM managerial duties — hence the new role.
## 2. Permission → role matrix
✓ = granted. (`SITE` = SITE_STAFF, `MAN` = MANNING/MPO, `ACC` = ACCOUNTS,
`MGR` = MANAGER, `SU` = SUPERUSER, `AUD` = AUDITOR, `ADM` = ADMIN.)
| Permission | SITE | MAN | ACC | MGR | SU | AUD | ADM |
|---|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
| `raise_requisition` | ✓ | ✓ | | ✓ | ✓ | | |
| `cancel_requisition` | | ✓ | | ✓ | ✓ | | |
| `view_requisitions` | ✓ | ✓ | | ✓ | ✓ | ✓ | ✓ |
| `manage_candidates` (shortlist/vetting) | | ✓ | | ✓ | ✓ | | |
| `record_reference_check` | | ✓ | | ✓ | ✓ | | |
| `propose_interview` | | ✓ | | ✓ | ✓ | | |
| `approve_salary_structure` | | | | ✓ | ✓ | | |
| `select_candidate` | | ✓ | | ✓ | ✓ | | |
| `onboard_crew` | | ✓ | | ✓ | ✓ | | |
| `sign_off_crew` | ✓ | ✓ | | ✓ | ✓ | | |
| `view_crew_records` | ✓¹ | ✓ | ✓² | ✓ | ✓ | ✓ | ✓ |
| `upload_crew_records` (docs/NoK/emergency/EPF) | ✓ | ✓ | | ✓ | ✓ | | |
| `issue_ppe` | ✓ | ✓ | | ✓ | ✓ | | |
| `apply_leave` | ✓ | ✓ | | ✓ | ✓ | | |
| `decide_leave` | | ✓ | | ✓ | ✓ | | |
| `record_attendance` | ✓ | ✓ | | ✓ | ✓ | | |
| `verify_site_records` (docs/PPE/leave/NoK) | | ✓ | | ✓ | ✓ | | |
| `verify_bank_epf` | | | ✓ | ✓ | ✓ | | |
| `raise_appraisal` | ✓ | | | ✓ | ✓ | | |
| `verify_appraisal` | | ✓ | | ✓ | ✓ | | |
| `approve_appraisal` | | | | ✓ | ✓ | | |
| `generate_wage_report` | | ✓ | | ✓ | ✓ | | |
| `approve_wage_report` | | | | ✓ | ✓ | | |
| `view_wage_report` | | | ✓ | ✓ | ✓ | ✓ | ✓ |
| `manage_ranks` | | | | ✓ | | | ✓ |
¹ Site staff get crew records for **their own site only**, with contract
**view-only-except-salary** and bank **view-only**.
² Accounts see bank/EPF and the wage report, not the full HR record.
## 3. Field-level restrictions (PII)
Enforced in Server Components/Actions on top of the permission map:
| Field | Visible to | Hidden from |
|---|---|---|
| Salary (contract letter, salary structure) | MPO, Manager, Accounts, Superuser, Auditor | **Site staff** |
| Full bank account number | Accounts (verify), Manager, Superuser | Site staff (view-only masked), MPO |
| Aadhaar / PAN number | MPO (verify), Accounts (EPF), Superuser | Site staff (masked) |
| Candidate rejection remarks | MPO, Manager, Superuser, Auditor | Candidate |
## 4. Business rules layered on top
- A candidate cannot be **onboarded** until the salary structure is
**Manager-approved** and a contract letter is attached.
- **Bank and EPF** records can only be set `VERIFIED` by **Accounts** (UAN/Aadhaar
EPFO check); everything else site staff enter is verified by **MPO**.
- **Attendance** has no verification gate (operational record).
- An **appraisal** must be `MPO_VERIFIED` before a Manager can approve it.
- A **wage report** must be `MANAGER_APPROVED` before it is `SENT_TO_ACCOUNTS`.
- Only **one ACTIVE assignment** per crew member; sign-off is required before a
new one.
- Site staff actions are scoped to the **site/vessel they are assigned to**.
## 5. Implementation pattern
```ts
// app/(portal)/crewing/candidates/actions.ts
'use server'
export async function onboardCandidate(input: OnboardInput) {
const session = await auth()
requirePermission(session.user.role, 'onboard_crew') // role gate
const app = await db.application.findUniqueOrThrow(...)
assertCanTransition(app.stage, 'onboard') // pipeline gate
// ...create assignment + side-effects in a transaction,
// write a CrewAction, dispatch notifications
}
```
Same shape as every PO Server Action: **permission → state-machine guard →
transactional write → audit row → notify.**

107
Crewing-Use-Cases.md Normal file

@ -0,0 +1,107 @@
# Crewing Use Cases
Actors and the use cases each can perform. The diagram uses a flowchart to
approximate UML use-case notation (Mermaid has no native use-case diagram);
actors are on the edges, use cases are rounded nodes, grouped by area.
## 1. Use-case diagram
```mermaid
flowchart LR
SS([Site staff<br/>PM / APM / Site I-C])
MPO([MPO])
ACC([Accounts])
MGR([Manager])
CAND([Candidate])
ADM([Admin])
subgraph Requisition
U1(["Raise requisition"])
U2(["Sign off crew → auto-requisition"])
U3(["Cancel requisition"])
end
subgraph Recruitment
U4(["Apply with CV"])
U5(["Shortlist candidates"])
U6(["Run vetting gates"])
U7(["Reference check"])
U8(["Propose & interview"])
U9(["Select / reject with remarks"])
U10(["Approve salary structure & list"])
U11(["Onboard candidate"])
end
subgraph SiteHR["Site HR"]
U12(["Apply for leave"])
U13(["View leave planner"])
U14(["Record attendance"])
U15(["Issue PPE (sizes)"])
U16(["Upload documents / EPF / NoK"])
end
subgraph Verify["Office verification"]
U17(["Verify docs / PPE / leave"])
U18(["Verify bank & EPF"])
end
subgraph Appraisal
U19(["Raise appraisal"])
U20(["Verify appraisal"])
U21(["Approve appraisal"])
end
subgraph Payroll
U22(["Generate wage report"])
U23(["Approve wage report"])
U24(["View / export wage report"])
end
subgraph Admin
U25(["Manage ranks & doc requirements"])
end
SS --- U1 & U2 & U12 & U13 & U14 & U15 & U16 & U19
CAND --- U4
MPO --- U5 & U6 & U7 & U8 & U9 & U11 & U17 & U20
MGR --- U3 & U9 & U10 & U11 & U21 & U22 & U23 & U24
ACC --- U18 & U24
ADM --- U25
```
## 2. Use-case catalogue
| ID | Use case | Primary actor | Pre-condition | Main success outcome |
|---|---|---|---|---|
| UC-01 | Raise requisition | Site staff / MPO | a rank is or will be vacant | `Requisition(OPEN)` created, MPO notified |
| UC-02 | Auto-requisition on sign-off | System | crew member signed off | requisition raised automatically; experience updated |
| UC-03 | Cancel requisition | Manager | requisition not yet filled | `CANCELLED` with reason |
| UC-04 | Apply with CV | Candidate | careers form reachable | `CrewMember(PROSPECT)` + application; CV parsed or manual |
| UC-05 | Shortlist candidates | MPO | requisition `OPEN` | applications `SHORTLISTED`; ex-hands preferred |
| UC-06 | Run vetting gates | MPO | candidate shortlisted | competency/document/experience gates recorded |
| UC-07 | Reference check | MPO | candidate in vetting | reference result recorded |
| UC-08 | Propose & interview | MPO | gates cleared | proposal sent; interview recorded (waived for ex-hands) |
| UC-09 | Select / reject with remarks | MPO / Manager | interview done / waived | `SELECTED` or `REJECTED` + remarks |
| UC-10 | Approve salary structure & candidate list | Manager | candidate proposed | salary structure approved |
| UC-11 | Onboard candidate | MPO / Manager | candidate selected | `CrewAssignment(ACTIVE)`; salary/victualing/attendance/experience/PF/PPE started; employeeId issued; requisition `FILLED` |
| UC-12 | Apply for leave | Site staff | active assignment | `LeaveRequest(APPLIED)` |
| UC-13 | View leave planner | Site staff / MPO / Manager | — | timeline of contracts + leaves per site |
| UC-14 | Record attendance | Site staff | active assignment | daily `Attendance` rows |
| UC-15 | Issue PPE | Site staff | active assignment | `PpeIssue` with size (boiler suit/shoes) |
| UC-16 | Upload documents / EPF / next-of-kin / emergency | Site staff | crew exists | records created `PENDING` verification |
| UC-17 | Verify docs / PPE / leave | MPO | pending records exist | `VERIFIED` (everything except attendance) |
| UC-18 | Verify bank & EPF | Accounts | pending bank/EPF | `VERIFIED`; UAN/Aadhaar checked vs EPFO |
| UC-19 | Raise appraisal | Site staff (PM) | active assignment | `Appraisal(SUBMITTED)` |
| UC-20 | Verify appraisal | MPO | appraisal submitted | `MPO_VERIFIED` |
| UC-21 | Approve appraisal | Manager | appraisal verified | `MANAGER_APPROVED` |
| UC-22 | Generate wage report | Manager / system | month end | `WageReport(GENERATED)` per site |
| UC-23 | Approve wage report | Manager | report generated | `MANAGER_APPROVED``SENT_TO_ACCOUNTS` |
| UC-24 | View / export wage report | Accounts / Manager | report sent | XLSX/PDF export for disbursement |
| UC-25 | Manage ranks & doc requirements | Admin | — | rank hierarchy + per-rank required documents |
## 3. Notable extensions / alternates
- **UC-04 alt** — CV not parseable → manual field entry (never blocks intake).
- **UC-06/07/09 alt** — any gate fails → `REJECTED` with remarks; application
ends, requisition stays open for other candidates.
- **UC-08 alt** — ex-hand → interview waived (`interviewWaived = true`).
- **UC-11 alt** — joining formalities incomplete (no signed contract / unapproved
salary) → onboarding blocked.
- **UC-16/17 alt** — verifier rejects a record → back to `PENDING` with remarks
for the site to correct.
- **UC-22 alt** — attendance gaps flagged before generation; report can be
regenerated until approved.

329
Crewing-User-Stories.md Normal file

@ -0,0 +1,329 @@
# Crewing User Stories
Backlog for the Crewing Module, grouped into epics. Format:
*As a `<role>`, I want `<capability>`, so that `<value>`* + acceptance criteria
(AC). IDs are stable references for PRs/issues.
---
## Epic A — Requisitions (vacancy management)
**A1 — Auto-requisition on sign-off**
As an **MPO**, I want a requisition raised automatically when a crew member
signs off, so that no vacancy goes untracked.
- AC1: signing off an assignment creates a `Requisition(OPEN, autoRaised)` for
the same rank + vessel.
- AC2: the requisition records the reason (EOC / medical / termination / other).
- AC3: the MPO receives an email + in-app notification.
- AC4: a `CrewAction(REQUISITION_RAISED)` is written.
**A2 — Manual requisition**
As **site staff / MPO**, I want to raise a requisition manually (e.g. planned
leave), so that I can pre-empt a gap.
- AC1: I select vessel, rank, reason, needed-by date, and min-experience criteria.
- AC2: the requisition appears in the MPO shortlist queue.
**A3 — Requisition list & detail**
As an **MPO/Manager**, I want a filterable list of requisitions by
vessel/rank/status, so that I can prioritise sourcing.
- AC1: filters for status, vessel, rank, reason, needed-by.
- AC2: each row shows candidate count and days open.
**A4 — Cancel requisition**
As a **Manager**, I want to cancel a requisition with a reason, so that
withdrawn vacancies don't clutter the pipeline.
- AC1: only non-`FILLED` requisitions can be cancelled.
- AC2: reason is required and audited.
---
## Epic B — Candidate intake
**B1 — Public application with CV**
As a **candidate**, I want to apply on the Pelagia site and upload my CV, so
that I'm considered without re-typing everything.
- AC1: the form accepts personal details + CV upload (signed upload to storage).
- AC2: the CV is parsed for age, vessel type, current/past rank, experience.
- AC3: parsed fields pre-fill a draft I can correct before submitting.
- AC4: if parsing fails, I can enter all fields manually (intake never blocks).
- AC5: submission creates a `CrewMember(PROSPECT)` + application(s).
**B2 — MPO manual candidate upload**
As an **MPO**, I want to add a candidate and their CV directly, so that
walk-ins/referrals enter the same pipeline.
- AC1: same parse-or-manual flow as B1.
- AC2: the candidate is attachable to one or more open requisitions.
**B3 — Ex-hand recognition**
As an **MPO**, I want ex-hands flagged and preferred in shortlisting, so that
proven crew are surfaced first.
- AC1: candidates matching an existing `CrewMember(EX_HAND)` are tagged.
- AC2: ex-hands sort above new candidates by default.
- AC3: ex-hand experience/documents are pre-populated from history.
---
## Epic C — Recruitment pipeline (vetting)
**C1 — Shortlist to a requisition**
As an **MPO**, I want to shortlist candidates against a requisition, so that
vetting can begin.
- AC1: shortlisting moves the requisition to `SHORTLISTING` and the application
to `SHORTLISTED`.
**C2 — Competency check**
As an **MPO**, I want to record a competency result against the rank's minimum
criteria, so that unqualified candidates are filtered early.
- AC1: pass advances to document verification; fail → `REJECTED` + remarks.
**C3 — Document verification (vetting)**
As an **MPO**, I want to verify a candidate's seafarer documents during vetting,
so that only documented candidates proceed.
- AC1: missing/expired required docs (per rank) block advancement.
- AC2: result recorded as an `APPLICATION_GATE`.
**C4 — Experience check**
As an **MPO**, I want to verify experience (vessel type, time-in-rank, total
duration) against the requisition's criteria, so that the candidate fits the seat.
- AC1: criteria comparison shown; shortfall flagged.
**C5 — Reference check**
As an **MPO**, I want to record reference-check outcomes, so that prior conduct
is confirmed.
- AC1: at least one reference result recorded before proposal.
**C6 — Salary agreement**
As an **MPO**, I want to record an agreed salary structure (basic, victualing,
allowances), so that the proposal has terms.
- AC1: structure captured; visible only to office roles (not site staff).
**C7 — Proposal & interview**
As an **MPO**, I want to propose to the candidate and record an interview, so
that selection is informed.
- AC1: new candidates require an interview record.
- AC2: ex-hands can have the interview **waived** (`interviewWaived`).
**C8 — Selection / rejection with remarks**
As an **MPO/Manager**, I want to select or reject with remarks, so that the
decision and its reason are on record.
- AC1: rejection requires remarks.
- AC2: selection requires all prior gates cleared.
**C9 — Pipeline board**
As an **MPO**, I want a board showing each candidate's stage per requisition, so
that I can see the funnel at a glance.
- AC1: columns map to `ApplicationStage`; cards show candidate + last gate.
---
## Epic D — Onboarding
**D1 — Approve salary structure & candidate list**
As a **Manager**, I want to approve the proposed salary structure and selected
candidate, so that onboarding can proceed.
- AC1: onboarding is blocked until the structure is `MANAGER_APPROVED`.
**D2 — Joining formalities & onboard**
As an **MPO/Manager**, I want to complete joining formalities and onboard the
candidate, so that they become an employee.
- AC1: a contract letter is attached; salary confirmed.
- AC2: onboarding creates a `CrewAssignment(ACTIVE)`, assigns an `employeeId`,
and sets `CrewMember.status = EMPLOYEE`.
- AC3: the filled requisition flips to `FILLED`.
**D3 — Onboarding side-effects**
As the **system**, I want onboarding to start salary, victualing, attendance,
experience, PF and PPE in one transaction, so that nothing is missed.
- AC1: all six are initialised atomically.
- AC2: a single `CrewAction(ONBOARDED)` records the created IDs in `metadata`.
---
## Epic E — Crew records & documents
**E1 — Crew directory & profile**
As an **MPO**, I want a crew directory with a per-member profile, so that I can
find anyone's records.
- AC1: profile groups documents, bank, EPF, next-of-kin, emergency, PPE,
experience, appraisals.
**E2 — Upload seafarer documents**
As **site staff**, I want to upload role-based documents (STCW, Aadhaar, PAN,
passport, CDC, COC, photo, licence, medical fitness), so that the office can
verify them.
- AC1: required set is driven by rank (`RankDocRequirement`); driver requires a
driving licence.
- AC2: documents store issue/expiry dates; uploads start `PENDING`.
**E3 — Document expiry warnings**
As an **MPO**, I want upcoming-expiry warnings, so that crew don't sail on
expired documents.
- AC1: docs expiring within N days are flagged on the profile and a queue.
**E4 — Bank / EPF / next-of-kin / emergency**
As **site staff**, I want to record bank details, EPF (UAN/Aadhaar), next-of-kin
and emergency contacts, so that payroll and welfare are covered.
- AC1: bank account number stored masked/encrypted; site staff see it
**view-only**.
- AC2: contract letter is **view-only-except-salary** for site staff.
---
## Epic F — PPE issue register
**F1 — Capture sizes**
As **site staff**, I want to record boiler-suit and safety-shoe sizes, so that
the right kit is issued.
- AC1: size is captured on the relevant PPE issue.
**F2 — Issue PPE kit**
As **site staff**, I want to issue PPE items (boiler suit, shoes, helmet, vest,
gloves, mask, goggles, tiffin, torch, walkie-talkie), so that issuance is logged.
- AC1: each issue records item, quantity, size?, date, issuer.
- AC2: (flag on) optionally decrements site `ItemInventory`.
**F3 — Returns**
As **site staff**, I want to record PPE returns, so that the register is current.
- AC1: a `returnedDate` closes the issue.
---
## Epic G — Leave & attendance
**G1 — Site leave planner**
As **site staff / MPO / Manager**, I want a planner showing every crew member's
contract span and leaves on one timeline, so that I can plan cover.
- AC1: timeline per site with contract bars + leave bars.
- AC2: overlapping absences for the same rank are highlighted.
**G2 — Apply for leave**
As **site staff**, I want to apply for leave (apply-only), so that the office can
approve it.
- AC1: I cannot self-approve; status starts `APPLIED`.
**G3 — Decide leave**
As an **MPO**, I want to approve/reject leave with a note, so that the planner
stays accurate.
- AC1: approval moves the assignment to `ON_LEAVE` for the period.
**G4 — Record attendance**
As **site staff**, I want to record daily attendance, so that wages can be
computed.
- AC1: one row per assignment per day; statuses present/absent/half/leave/sign-off.
- AC2: no verification gate (operational).
---
## Epic H — Appraisal
**H1 — Raise appraisal**
As a **PM (site staff)**, I want to raise an appraisal for a crew member, so that
performance is recorded.
- AC1: appraisal starts `DRAFT`, submits to `SUBMITTED`.
**H2 — Verify appraisal**
As an **MPO**, I want to verify a submitted appraisal, so that the Manager
reviews vetted input.
- AC1: verify → `MPO_VERIFIED`; return → `REJECTED` + remarks.
**H3 — Approve appraisal**
As a **Manager**, I want to approve a verified appraisal, so that it's final.
- AC1: only `MPO_VERIFIED` appraisals can be approved.
---
## Epic I — Office verification
**I1 — MPO verification queue**
As an **MPO**, I want a queue of pending site records (documents, PPE, leave,
next-of-kin), so that I can verify everything except attendance.
- AC1: verify sets `VERIFIED` + me as `verifiedBy`; reject returns with remarks.
**I2 — Accounts bank/EPF verification**
As **Accounts**, I want to verify bank details and EPF (UAN/Aadhaar vs EPFO), so
that payments go to the right verified account.
- AC1: only Accounts can verify bank/EPF.
- AC2: EPFO check result is recorded.
---
## Epic J — Payroll / wage report
**J1 — Generate wage report**
As a **Manager (or month-end job)**, I want a per-site wage report
(`crew × salary × days-attended + victualing`), so that Accounts can pay.
- AC1: one `WageLine` per active assignment; `lineTotal` computed.
- AC2: attendance gaps flagged before generation.
**J2 — Approve & send**
As a **Manager**, I want to approve the report and send it to Accounts, so that
disbursement is authorised.
- AC1: `MANAGER_APPROVED``SENT_TO_ACCOUNTS`.
**J3 — Accounts view & export**
As **Accounts**, I want to view and export the wage report (XLSX/PDF), so that I
can disburse.
- AC1: export reuses the PO export mechanism.
---
## Epic K — Sign-off & experience
**K1 — Sign off crew**
As **site staff / MPO**, I want to sign off a crew member, so that the tour
closes cleanly.
- AC1: assignment → `SIGNED_OFF` with `signOffDate`.
- AC2: an `EXPERIENCE_RECORD` is appended (vessel type, rank, duration).
- AC3: a backfill requisition is auto-raised (links to A1).
**K2 — Experience history**
As an **MPO**, I want a crew member's accumulated experience on file, so that
ex-hand vetting is instant.
- AC1: internal tours appear automatically; declared experience can be added.
---
## Epic L — Reference data & admin
**L1 — Manage ranks**
As an **Admin/Manager**, I want to manage the rank hierarchy, so that
requisitions and the org chart are correct.
- AC1: ranks form a self-hierarchy with category + `isSeafarer`.
**L2 — Manage rank document requirements**
As an **Admin/Manager**, I want to set which documents each rank requires, so
that vetting and uploads enforce the right set.
- AC1: e.g. driver → driving licence; deck/engine → STCW/CDC/COC/medical.
---
## Epic M — Cross-cutting
**M1 — Crewing dashboard**
As a **Manager**, I want crewing stat cards (open requisitions, candidates in
pipeline, expiring documents, crew on leave), so that I see status at a glance.
**M2 — Notifications**
As any **actor**, I want email + in-app notifications on the events relevant to
me (requisition raised, proposal, appraisal verified, wage report ready), so
that I act promptly.
**M3 — Audit trail**
As an **Auditor**, I want every crewing transition recorded with actor/time/note,
so that the process is fully traceable.
- AC1: `CrewAction` rows for all lifecycle and verification events.
**M4 — Feature flag**
As an **operator**, I want the crewing surface behind `NEXT_PUBLIC_CREWING_ENABLED`,
so that it can be rolled out gradually.
---
### Suggested delivery order
1. Reference data (L1, L2) + data model + permissions.
2. Requisitions (A) + sign-off/experience (K).
3. Candidate intake (B) + pipeline (C) + onboarding (D).
4. Crew records (E), PPE (F), leave/attendance (G).
5. Verification (I), appraisal (H).
6. Payroll (J), dashboard/notifications (M).

291
Crewing-Workflows.md Normal file

@ -0,0 +1,291 @@
# Crewing Workflows
Every crewing lifecycle is enforced by a single state-machine module (the
`po-state-machine.ts` pattern) and recorded as `CrewAction` audit rows. This
page is the authoritative behaviour spec: **state diagrams**, **transition
tables**, and **sequence diagrams** for the end-to-end flows.
---
## 1. Requisition lifecycle
The vacancy. Raised automatically on sign-off / end-of-contract, or manually.
```mermaid
stateDiagram-v2
[*] --> OPEN : raise (auto on sign-off / manual)
OPEN --> SHORTLISTING : add candidates
SHORTLISTING --> PROPOSING : a candidate clears vetting
PROPOSING --> INTERVIEWING : interview scheduled
PROPOSING --> SELECTED : ex-hand, interview waived
INTERVIEWING --> SELECTED : candidate selected
INTERVIEWING --> SHORTLISTING : all rejected, re-source
SELECTED --> FILLED : onboarded (assignment created)
OPEN --> CANCELLED : vacancy withdrawn
SHORTLISTING --> CANCELLED
FILLED --> [*]
CANCELLED --> [*]
```
| From | Action | To | Roles | Side-effect |
|---|---|---|---|---|
| — | `raise` | OPEN | system (sign-off) / SITE_STAFF / MANNING | email MPO |
| OPEN | `start_shortlist` | SHORTLISTING | MANNING | — |
| SHORTLISTING | `propose` | PROPOSING | MANNING | email Manager |
| PROPOSING | `schedule_interview` | INTERVIEWING | MANNING | email candidate |
| PROPOSING | `select` (ex-hand) | SELECTED | MANNING, MANAGER | email candidate |
| INTERVIEWING | `select` | SELECTED | MANNING, MANAGER | email candidate |
| INTERVIEWING | `reject_all` | SHORTLISTING | MANNING | — |
| SELECTED | `onboard` | FILLED | MANNING, MANAGER | **onboarding side-effects** |
| OPEN/SHORTLISTING | `cancel` | CANCELLED | MANAGER | — |
---
## 2. Candidate pipeline (Application)
The per-candidate gated vetting from notebook page 2. Many applications run per
requisition; one ends in `ONBOARDED`. **Ex-hands skip the interview** (optional).
Any gate can end in **rejection with remarks**.
```mermaid
stateDiagram-v2
[*] --> SHORTLISTED
SHORTLISTED --> COMPETENCY_CHECK
COMPETENCY_CHECK --> DOC_VERIFICATION : pass
DOC_VERIFICATION --> REFERENCE_CHECK : pass
REFERENCE_CHECK --> SALARY_AGREEMENT : pass
SALARY_AGREEMENT --> PROPOSED : agreed
PROPOSED --> INTERVIEW : new candidate
PROPOSED --> SELECTED : ex-hand (interview waived)
INTERVIEW --> SELECTED : pass
SELECTED --> JOINING : joining formalities
JOINING --> ONBOARDED : contract + salary confirmed
COMPETENCY_CHECK --> REJECTED : fail (remarks)
DOC_VERIFICATION --> REJECTED : fail (remarks)
REFERENCE_CHECK --> REJECTED : fail (remarks)
SALARY_AGREEMENT --> REJECTED : not agreed
INTERVIEW --> REJECTED : fail (remarks)
ONBOARDED --> [*]
REJECTED --> [*]
```
Each transition writes an `APPLICATION_GATE` row (`gate`, `result`, `note`,
`decidedById`) so the office can see exactly who cleared what and why a
candidate was rejected.
---
## 3. Assignment lifecycle
An onboarded tour of duty.
```mermaid
stateDiagram-v2
[*] --> ACTIVE : onboard
ACTIVE --> ON_LEAVE : leave approved
ON_LEAVE --> ACTIVE : return
ACTIVE --> SIGNED_OFF : sign-off
ON_LEAVE --> SIGNED_OFF : sign-off (EOC / medical / etc.)
SIGNED_OFF --> [*]
```
`onboard` starts salary + victualing + attendance + experience + PF + PPE.
`sign-off` closes the tour, appends an `EXPERIENCE_RECORD`, and **auto-raises a
backfill Requisition** for the same rank/vessel.
---
## 4. Appraisal lifecycle
```mermaid
stateDiagram-v2
[*] --> DRAFT : PM starts
DRAFT --> SUBMITTED : PM submits
SUBMITTED --> MPO_VERIFIED : MPO verifies
SUBMITTED --> REJECTED : MPO returns (remarks)
MPO_VERIFIED --> MANAGER_APPROVED : Manager approves
MPO_VERIFIED --> REJECTED : Manager returns
MANAGER_APPROVED --> [*]
REJECTED --> DRAFT : revise
```
---
## 5. Wage report status
```mermaid
stateDiagram-v2
[*] --> DRAFT : month-end trigger
DRAFT --> GENERATED : lines computed
GENERATED --> MANAGER_APPROVED : Manager approves
MANAGER_APPROVED --> SENT_TO_ACCOUNTS : dispatched
SENT_TO_ACCOUNTS --> [*]
```
---
## 6. Sequence — sign-off → auto-requisition → fill
```mermaid
sequenceDiagram
actor SS as Site staff (PM)
participant SYS as App / state machines
actor MPO
actor MGR as Manager
participant DB as PostgreSQL
participant N as Notifier
SS->>SYS: sign off crew member (EOC)
SYS->>DB: assignment → SIGNED_OFF
SYS->>DB: append EXPERIENCE_RECORD
SYS->>DB: create Requisition (autoRaised, OPEN)
SYS->>N: notify MPO "vacancy: <rank> on <vessel>"
N-->>MPO: email + bell
MPO->>SYS: start shortlist, add candidates
SYS->>DB: applications (SHORTLISTED)
Note over MPO,SYS: candidate pipeline (see §7)
MPO->>SYS: select candidate → onboard
SYS->>SYS: onboarding side-effects
SYS->>DB: requisition → FILLED, assignment ACTIVE
```
---
## 7. Sequence — candidate self-apply + CV parse
```mermaid
sequenceDiagram
actor C as Candidate
participant WEB as /careers/apply (public)
participant API as /api/crewing/cv-parse
participant CVP as CvParseService
participant FS as File storage
participant DB as PostgreSQL
actor MPO
C->>WEB: fill form + upload CV
WEB->>FS: store CV (signed upload)
WEB->>API: parse CV
API->>CVP: extract age, vessel type, rank, experience
CVP-->>API: fields (best-effort)
alt extracted
API-->>WEB: pre-filled draft
else not extractable
API-->>WEB: empty draft (manual entry)
end
C->>WEB: confirm / correct details
WEB->>DB: CrewMember (PROSPECT) + Application(s)
WEB-->>MPO: appears in shortlist queue
```
Manual MPO upload follows the same path minus the public form: MPO uploads the
CV and confirms the parsed fields.
---
## 8. Sequence — recruitment pipeline (vetting → onboard)
```mermaid
sequenceDiagram
actor MPO
participant SYS as application-pipeline
actor MGR as Manager
actor C as Candidate
participant DB as PostgreSQL
MPO->>SYS: competency check
SYS->>DB: gate(competency)=VERIFIED
MPO->>SYS: document verification
SYS->>DB: gate(document)=VERIFIED
MPO->>SYS: reference check
SYS->>DB: gate(reference)=VERIFIED
MPO->>SYS: salary agreement (propose structure)
SYS->>DB: gate(salary)=VERIFIED, proposedSalary
MPO->>C: proposal
alt new candidate
MPO->>SYS: schedule + record interview
SYS->>DB: gate(interview)=VERIFIED
else ex-hand
Note over MPO,SYS: interview waived
end
MGR->>SYS: approve salary structure + select
SYS->>DB: application SELECTED
MGR->>SYS: onboard (joining formalities)
SYS->>DB: CrewAssignment ACTIVE, contract letter, employeeId
Note over SYS,DB: salary+victualing+attendance+experience+PF+PPE started
```
Any failed gate → `gate=REJECTED` + remarks → application `REJECTED`
(visible to the office with the reason).
---
## 9. Sequence — site data entry & office verification
```mermaid
sequenceDiagram
actor SS as Site staff
participant SYS as App
participant DB as PostgreSQL
actor MPO
actor ACC as Accounts
SS->>SYS: upload documents / PPE issue / leave / next-of-kin / EPF
SYS->>DB: records (verificationStatus=PENDING)
par MPO verifies (all except attendance)
MPO->>SYS: verify document / PPE / leave / NoK
SYS->>DB: VERIFIED (verifiedById=MPO)
and Accounts verifies bank + EPF
ACC->>SYS: verify bank detail
ACC->>SYS: verify EPF (UAN/Aadhaar vs EPFO)
SYS->>DB: VERIFIED (verifiedById=Accounts)
end
SS->>SYS: record attendance (no verify gate)
SYS->>DB: attendance rows
```
Site staff see contract letters **view-only except salary** and bank details
**view-only**; salary and full account numbers are gated.
---
## 10. Sequence — monthly wage report
```mermaid
sequenceDiagram
participant CRON as Month-end trigger
participant WR as wage-report.ts
participant DB as PostgreSQL
actor MGR as Manager
actor ACC as Accounts
participant EXP as /payroll/[id]/export
CRON->>WR: generate for site, period YYYY-MM
WR->>DB: read assignments + attendance + salary
WR->>DB: WageLine = days × dailyRate + victualing
WR->>DB: WageReport (GENERATED, totalAmount)
WR-->>MGR: notify "wage report ready"
MGR->>DB: approve → MANAGER_APPROVED
MGR->>DB: send → SENT_TO_ACCOUNTS
DB-->>ACC: appears in Accounts queue
ACC->>EXP: export XLSX/PDF for disbursement
```
---
## 11. Side-effects of onboarding (the one event that starts everything)
| Side-effect | What it creates / starts |
|---|---|
| Salary | activates the approved `SalaryStructure` |
| Victualing | begins per-day messing accrual |
| Attendance | opens daily attendance for the assignment |
| Experience | begins accrual on this vessel/rank (closed into `EXPERIENCE_RECORD` at sign-off) |
| PF | enables EPF tracking against the crew member's UAN |
| PPE | seeds the PPE issue checklist for the kit |
| Identity | assigns `employeeId`, sets `CrewMember.status = EMPLOYEE` |
| Requisition | flips the filled requisition to `FILLED` |
All recorded as a single `ONBOARDED` `CrewAction` with the created IDs in
`metadata` — the same "one transition, one audit row, declared side-effects"
discipline as PO approval.

36
README.md Normal file

@ -0,0 +1,36 @@
# Crewing wiki bundle
Design documentation for the **Crewing / Manning Module** of Pelagia Portal
(PPMS). Drop these pages into the Forgejo wiki repo root and push.
## Pages
| File | Wiki page | Contains |
|---|---|---|
| `Crewing-Module.md` | Crewing Module | Landing page / feature overview |
| `Crewing-Design-Document.md` | Crewing Design Document | Goals, scope, domain narrative, decisions, open questions |
| `Crewing-Architecture.md` | Crewing Architecture | System + **component diagram**, module layout |
| `Crewing-Data-Model.md` | Crewing Data Model | Enums, rank hierarchy, **ER diagram** |
| `Crewing-Workflows.md` | Crewing Workflows | **State machines** + **sequence diagrams** |
| `Crewing-Use-Cases.md` | Crewing Use Cases | **Use-case diagram** + catalogue |
| `Crewing-Roles-and-Permissions.md` | Crewing Roles and Permissions | Permission matrix extension |
| `Crewing-User-Stories.md` | Crewing User Stories | Backlog by epic with acceptance criteria |
## Diagrams
All diagrams are **Mermaid** fenced code blocks (` ```mermaid `), which Forgejo
renders natively — no screenshot pipeline needed (unlike the script-based
mockups). If a target renderer doesn't support Mermaid, the blocks degrade to
readable text. The `images/crewing/` folder is reserved for any future static
captures and currently empty.
## Cross-links
Pages link to each other and to existing wiki pages (`Architecture`,
`Data-Model`, `Roles-and-Permissions`, `PO-Lifecycle`, `File-Storage`,
`Inventory-and-Catalogue`) using Forgejo's `[Text](Page-Name)` wiki-link form.
## Source
Derived from the director meeting notes, two notebook pages (site-staff record
permissions; recruitment flow), and `Crewing.excalidraw` (rank hierarchy).