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>
46 lines
1.7 KiB
TypeScript
46 lines
1.7 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { maskTail, canViewFullBankEpf, canViewSalary, bankEpfValue } from "@/lib/crew-pii";
|
|
|
|
// PII visibility rules for the crew profile (Crewing-Implementation-Spec §6/§8.8).
|
|
describe("crew PII masking", () => {
|
|
describe("maskTail", () => {
|
|
it("keeps the last 4 by default", () => {
|
|
expect(maskTail("123456789")).toBe("•••• 6789");
|
|
});
|
|
it("renders — for empty values", () => {
|
|
expect(maskTail(null)).toBe("—");
|
|
expect(maskTail("")).toBe("—");
|
|
});
|
|
it("fully masks values at or under the visible length", () => {
|
|
expect(maskTail("12")).toBe("••••");
|
|
expect(maskTail("1234")).toBe("••••");
|
|
});
|
|
});
|
|
|
|
describe("canViewFullBankEpf", () => {
|
|
it("only Accounts and SuperUser see full bank/EPF", () => {
|
|
expect(canViewFullBankEpf("ACCOUNTS")).toBe(true);
|
|
expect(canViewFullBankEpf("SUPERUSER")).toBe(true);
|
|
expect(canViewFullBankEpf("MANAGER")).toBe(false);
|
|
expect(canViewFullBankEpf("MANNING")).toBe(false);
|
|
expect(canViewFullBankEpf("SITE_STAFF")).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("canViewSalary", () => {
|
|
it("hides salary from site staff only", () => {
|
|
expect(canViewSalary("SITE_STAFF")).toBe(false);
|
|
expect(canViewSalary("MANAGER")).toBe(true);
|
|
expect(canViewSalary("ACCOUNTS")).toBe(true);
|
|
expect(canViewSalary("MANNING")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("bankEpfValue", () => {
|
|
it("shows full to Accounts, masked to others, — when empty", () => {
|
|
expect(bankEpfValue("123456789", "ACCOUNTS")).toBe("123456789");
|
|
expect(bankEpfValue("123456789", "MANAGER")).toBe("•••• 6789");
|
|
expect(bankEpfValue(null, "ACCOUNTS")).toBe("—");
|
|
});
|
|
});
|
|
});
|