- 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>
Second slice of the Crewing module per wiki Crewing-Implementation-Spec §12
(build order item 2). Everything stays behind NEXT_PUBLIC_CREWING_ENABLED;
production is unchanged. Schema is added incrementally — this lands the
requisition lifecycle layer.
What's in
- Schema: Requisition (OPEN→SHORTLISTING→PROPOSING→INTERVIEWING→SELECTED→FILLED,
→CANCELLED), ReliefRequest, CrewAction (the POAction mirror) + their enums.
Migration crewing_requisitions.
- State machine: lib/requisition-state-machine.ts mirrors po-state-machine
(selection Manager-only; orthogonal cancel from OPEN/SHORTLISTING by
cancel_requisition holders, §6). Codes REQ-9000… via lib/requisition-number.ts.
- Actions: raise/cancel/transition + requestReliefCover/convertReliefToRequisition,
each guarding flag+permission+state, writing a CrewAction and notifying. Shared
autoRaiseRequisition() (lib/requisition-service.ts) is the backfill entry point
for sign-off / leave-clash (later phases).
- Notifier: notifyCrew() PO-independent path + CrewNotificationEvent.
- Screens: /crewing/requisitions (list + Raise modal + relief convert) and
/crewing/requisitions/[id] (detail). Requisitions added to the flag-gated
Crewing sidebar (Manager + MPO, §7).
Tests & docs
- Unit: requisition-state-machine.test.ts (11).
- Integration: requisitions.test.ts (15) — raise/cancel/transition, relief
request + convert, auto-raise, permission gating.
- CLAUDE.md "Crewing" section updated with the Phase 2 surface.
Deferred: sign-off/experience (Epic K, §12 item 2) depends on the crew/assignment
models from Phase 3/4; autoRaiseRequisition() is ready for it.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Uploaded signatures/stamps aren't always transparent PNGs, so an opaque stamp
overlapping the signature/name would cover them. Extract the signatory-block
geometry into a tested helper (signatoryLayout): the signature is centred over
the name and the stamp sits to its RIGHT with a 10px gap — never overlapping.
- lib/po-export-layout.ts (signatoryLayout) + unit test
- export route uses it instead of inline overlap math
Verified in a real export: signature 175-328px (centred), stamp 338-405px
(10px gap, no overlap), stamp drawn behind the signature.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
In the XLSX signatory block, place the approver signature centred over the
name and tuck the stamp to its right with a slight overlap. The stamp is now
drawn before the signature so it layers behind it (Excel z-order = add order).
Images are positioned by absolute pixels via native EMU offsets — ExcelJS's
fractional-column anchors don't map cleanly to pixels (the stamp was landing
on top of the signature centre instead of to its right). Verified in a real
export: signature centre 252px in the 503px A-D block (centred), stamp to the
right (305-372px), stamp drawn behind the signature.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The logo, signature, stamp and cancelled watermark were placed with ExcelJS
two-cell (tl/br) anchors, which stretch each image to fill a cell range —
distorting them and making the watermark text small/squished. The PDF looked
fine because CSS sizes by aspect.
- New lib/image-size.ts: getImageSize (PNG/JPEG/WebP header parse) + scaleToBox.
- Export route now places each image with a oneCell `tl` + pixel `ext`,
aspect preserved and matched to the PDF sizes (logo ≤96×52, signature ≤165×44,
stamp ≤80×66, watermark ≤880×720).
- Watermark regenerated as a landscape canvas with the text filling it, so it
spans the page like the PDF instead of sitting small in the centre.
- Unit test for getImageSize + scaleToBox.
Verified structurally: generated XLSX uses oneCellAnchors with fixed pixel
ext sizes (49×52 / 45×44 / 67×66 / 880×629), not stretched cell ranges.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
On /inventory/vendors, include vendorId in the search filter and render
it as a muted mono badge beside the vendor name. The vendorId data was
already passed to the client component, so this is a presentation/filter
change only. Unverified vendors (no vendorId) render unchanged.
Fixes#57
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Managers and superusers can cancel a PO from any state via a confirmation modal
that requires typing "cancel" and a mandatory reason. A cancelled PO becomes a
terminal CANCELLED state and drops out of every spend tracker/graph (those filter
on POST_APPROVAL_STATUSES / explicit whitelists, none of which include CANCELLED).
A cancelled PO may optionally be linked to the existing PO that supersedes it
(by PO number); the replacement shows the reciprocal "supersedes" link. No
vessel/account/vendor match is enforced and the link can be added any time.
Cancelled POs remain visible (greyed in history) and exportable, with a diagonal
"CANCELLED" watermark on both the PDF and XLSX exports.
- schema: POStatus CANCELLED; cancelledAt/cancellationReason; self-referential
supersededById relation; ActionType CANCELLED/SUPERSEDED (+ migration)
- state machine canCancel(); cancel_po permission (MANAGER + SUPERUSER)
- cancelPo / supersedePo server actions + PO_CANCELLED notification
- cancel modal + supersede form; cancelled banner with reciprocal links
- exhaustive CANCELLED entries in all status label/variant maps
- diagonal CANCELLED watermark embedded for PDF (CSS) and XLSX (image)
- integration tests (cancel from any state, reason/role guards, supersede)
Inventory reversal on cancel is deferred to #55 (inventory is feature-flagged off).
Closes#53
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- resubmit: updatePo distinguishes intent "resubmit" (from EDITS_REQUESTED)
from "submit" (from DRAFT); test now sends "resubmit" (makePoForm widened).
- payment: MANAGER now holds process_payment, so the "wrong permission"
negative test uses TECHNICAL (which lacks it).
- vendor: provideVendorId rejects on a missing vendorId *code*; seeded
unverified vendors carry codes, so create a genuinely code-less vendor.
Full integration suite: 108/108 passing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The integration suite had rotted against the app. Systematic fixes:
- seed refs: MV Ocean Pride/Sea Breeze/TECH-OPS → current seed entities
- helper appendLineItem set lineItems[i].description; createPo now keys on
lineItems[i].name → zero line items. Fixed to .name.
- vendor gating: lifecycle setups (approval/payment/receipt) now attach the
seeded verified vendor before approval.
- cleanup: POAction has no onDelete:Cascade, so deletePo(sByTitle) now removes
POAction rows before the PO.
- import-api: fixture committed to tests/fixtures/Sample_PO.xlsx (was an
absolute path to a non-existent dir).
- products-search: code search assertion .every → .some (search spans fields).
11 failures remain (behavioral drift — separate commit).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Satisfies the contribution-policy test gate for the add/edit-page refactor.
Covers: createCompany returns the new id, code upper-casing, duplicate-code
rejection, updateCompany in-place edit, and manage_vessels_accounts gating.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Overhaul the manager dashboard "Total Approved Spend" stat card per the
reporter's request:
- Swap the DollarSign lucide icon for IndianRupee (rupee symbol).
- Render the amount in the Indian short scale (lakh/crore) via a new
`formatCompactINR` helper, e.g. ₹2 Cr, ₹49 L, ₹75 K, instead of the full
₹49,00,000.00.
`formatCompactINR` rounds to at most 2 decimals, trims trailing zeros, keeps
the ₹ prefix and sign. The DollarSign icon is retained for the Accounts
"Payment Queue Value" card; the precise `formatCurrency` is kept for tables.
Adds unit tests covering crore/lakh/thousand/sub-thousand, boundaries, zero,
string input, negatives and non-finite input.
Fixes#50
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The company form outgrew the modal once the branding (logo/stamp) section
was added. Add/edit now live on their own routes:
- /admin/companies/new
- /admin/companies/[id]/edit
- createCompany returns the new id and the create flow lands on the edit
page so logo/stamp can be uploaded immediately
- list "+ Add Company" is a link; row "Edit" navigates to the edit page
- branding is its own card on the edit page (independent uploads)
- list page no longer mints a presigned URL per company (moved to edit)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Companies can upload a logo and a stamp/seal (Admin → Companies → Edit →
Branding); both render on exported PDF and XLSX purchase orders. A fixed
brand-colour bar (#92D050, matching the sample PO) runs along the bottom of
every export.
- Company.logoKey / stampKey + migration
- buildCompanyAssetKey() deterministic storage keys (overwrite-in-place)
- uploadCompanyAsset / removeCompanyAsset server actions (≤4MB PNG/JPG/WebP,
manage_vessels_accounts gated)
- CompanyBrandingUploader in the company edit dialog with live previews
- Export route embeds logo (top-left), stamp (signatory block) and brand bar
in both ExcelJS and print-HTML paths
- Unit test (storage keys) + integration test (branding actions)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The PO line-items Unit of Measure dropdown only offered hr/day among
time-based units. Add week, month and year so durations beyond days can
be selected, as requested. UOM_OPTIONS is the single source of truth and
`unit` is validated as a free-form string, so no schema/validation change
is needed.
Fixes#44
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Green-lights the test suite so the PR checks can enforce it:
- Fix the NextAuth v5 auth() mock typing across all integration tests (cast to a
simple async fn so mockResolvedValue accepts the session) — clears ~86 errors.
- Fix stale test values: intent 'resubmit'->'submit' / 'save'->'draft'; ParsedImportLine
.description -> .name; approvepo -> approvePo; add missing beforeEach/beforeAll imports.
- permissions: MANAGER *can* process_payment (intentional since e1340b9) — update the
stale assertion.
- po-import-parser: skip the Sample_PO.xlsx fixture tests when the file is absent (it
lives outside the repo); synthetic-workbook tests still cover the parser.
type-check is now 0 errors and unit tests pass (167 passed, 13 skipped). pr-checks.yml
flips type-check (whole project) and unit tests to HARD gates.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reflects this iteration's domain/feature changes across the docs set:
- Cost centre = Vessel only (labelled 'Cost Centre'); costCentreRef/Site removed
- Companies (multi-company invoicing) on POs and exports
- 3-level 6-digit accounting-code hierarchy; leaf-only PO selection
- Structured PO numbers COMPANY/VESSEL/ID/FY (ids from 9000)
- Compulsory payment date; editable poDate; export date = approval date
- Submitter vendor creation (unverified until proven); verifyVendor
- Import PO -> CLOSED with auto vendor/product creation
- Inventory flag; inventory added at approval; partial pay/receipt states
- Microsoft Entra SSO (nullable passwordHash); profile reachable by all roles
- README: roles, domain concepts, db:seed:prod, migrate-before-serve callout
- CHANGELOG: Added/Changed/Fixed for the above
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- App/README.md: add FORGEJO_*/NEXT_PUBLIC_ENV_LABEL env vars and an
'Operations & Automation' section pointing to automation/README.md.
- App/CLAUDE.md: complete the env var list (AZURE_AD_*, FORGEJO_*, GST_SERVICE_URL,
NEXT_PUBLIC_ENV_LABEL) and note the prod-mirror test DB used by autofix/staging.
- .env.example: document NEXT_PUBLIC_ENV_LABEL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The manager dashboard "Approved This Month" card only counted POs whose
current status is MGR_APPROVED, so approvals that had already moved on to
payment, delivery, or closure dropped out of the count. Managers could not
see what happened to the POs they approved this month.
- Count every PO whose `approvedAt` falls in the current month across all
post-approval statuses (MGR_APPROVED → ... → CLOSED). `approvedAt` is set
once at approval and persists, so it is the correct anchor.
- Introduce a shared `POST_APPROVAL_STATUSES` constant (includes the
previously-omitted PARTIALLY_CLOSED). This also fixes Total Approved Spend
and the vessel/monthly breakdowns, which were silently dropping
partially-received POs.
- Make the card a link into /history with an approval-date filter applied
(?approvedFrom=<startOfMonth>) so a click shows the full set with each PO's
current status, as requested.
- Add `approvedFrom`/`approvedTo` filtering to the history page, its filter
UI, and the reports export route so the deep-link and exports stay in sync.
Scope note: the count remains org-wide, consistent with every other card on
the manager dashboard.
Adds an integration test covering the moved-on case and the date window.
Fixes#32
Renders a thin fixed banner only when NEXT_PUBLIC_ENV_LABEL is set; production
leaves it unset so nothing shows. Used to mark the staging instance.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The PO history page previously allowed only a single status filter. This
enhances it to accept multiple statuses that are OR-ed together (e.g.
Closed + Approved shows all POs in either state), as requested.
- Status filter is now a multi-select checkbox dropdown that serialises
selections as repeated `status` query params.
- History page and the reports export endpoint read all `status` values
and query with `status: { in: [...] }` (OR semantics).
- Single-status and no-status cases remain unchanged.
Verified OR-query semantics against the test DB and confirmed both routes
compile and respond. type-check passes for the changed files.
Fixes#31
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All PO attachments are stored as PODocument rows whose lifecycle stage
(submission vs delivery) is encoded in the storageKey prefix. The PO
details screen previously listed them in a single flat "Attachments"
block, giving no indication of which were submission documents (invoice,
quotation) versus delivery receipts.
Add lib/attachments.ts to derive a user-facing group from the storageKey
prefix (submission / payment / delivery / other) and render each
non-empty group as a labelled subsection on the PO details screen, in
lifecycle order. Unknown prefixes fall back to an "Other" group so
nothing is ever hidden.
Fixes#10
Portal issues now file with only the 'portal' label. The watcher runs two phases:
1. Triage — Claude reads each untriaged 'portal' issue (analysis only), posts a
requirements-breakdown comment, and routes it to 'claude-queue' (auto-fixable)
or 'interactive' (needs human steering).
2. Fix — unchanged; processes 'claude-queue' issues into PRs.
The triage breakdown is posted without the bot marker so the fix stage reads it
back as refined requirements.
PS 5.1 fixes found while validating:
- Send API bodies as UTF-8 bytes (Invoke-RestMethod mangled non-ASCII, e.g. the
em-dash in Claude's breakdown, so Forgejo rejected the JSON)
- Build the labels array body by hand (ConvertTo-Json unwraps a single-element
array to a scalar, which Forgejo rejects)
- Triage output via two plain files (label + markdown) instead of one JSON blob
(embedded-newline markdown broke ConvertFrom-Json)
- Read triage files as UTF-8; additive label POST + a guard so Set-IssueLabels
can never wipe an issue's labels
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>