The vendor field on the PO forms was a plain native <select>, forcing
users to scroll the full vendor list. Mirror the item-search UX with a
searchable combobox that filters by vendor name and formal code
(vendorId), case-insensitively. The vendor list is already client-side,
so this is a pure in-memory filter — no API or DB change.
New VendorSelect component (components/ui/vendor-select.tsx) is a
self-contained portal-rendered combobox posting a hidden vendorId input,
so it drops into all three PO forms unchanged on the server:
- po/new/new-po-form
- po/[id]/edit/edit-po-form
- approvals/[id]/manager-edit-po-form
Preserves the optional field, "No vendor selected" empty option, and the
"{name} (CODE)" / "(unverified)" label. Unverified vendors (null code)
remain findable by name. Adds unit tests for the filter logic and
component behaviour.
Fixes#109
- tests/e2e/inventory → tests/e2e/catalogue (folder name only; playwright globs
./tests/e2e so nothing else changes).
- remove the next.config redirects from /inventory/{items,vendors}: the old
routes are intentionally left free for a future feature.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Renames the product-catalogue pages (items + vendors, incl. their [id] detail
pages) out of /inventory into /catalogue. /inventory/cart is unchanged. All
internal links, redirects, revalidatePath calls, sidebar nav, and tests are
updated; next.config redirects keep old /inventory/{items,vendors}[/...] URLs
working (permanent) so existing bookmarks don't 404.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Previously a PO's free-text line items only became reusable catalogue products
(/inventory/items) on full payment (markPaid → syncProductCatalog). An approved-
but-unpaid PO's items weren't selectable for further POs yet.
- extract syncProductCatalog into lib/product-catalog.ts (shared).
- call it from approvePo so approved items are immediately catalogued (create
product by name if unknown, link the line item, upsert last/per-vendor price);
payment still re-syncs to refresh prices. Idempotent.
- test: approving a PO with a free-text line creates + links the product and
records the per-vendor price.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Follow-up to the merged #11 PR (which shipped the enum-based catalogue): make
categories user-defined data and the PO T&C a dynamic editor.
- categories are a TermsCategory TABLE (not an enum) — admins add new ones;
- every PO T&C line is catalogued, incl. the previously-fixed boilerplate
(seeded under a "General" category) and an "Others" bucket;
- the PO form is a dynamic editor: "+ Add term", pick a category, type/pick a
clause (components/po/po-terms-editor.tsx), used by new/edit/manager-edit.
Migration: the already-released 20260624140000 migration is untouched; a new
20260624150000 FORWARD migration renames the enum, creates the table, migrates
existing enum clauses onto category rows, adds isDefault/sortOrder + the two
fixed lines under General, and adds PurchaseOrder.terms (JSON snapshot that
supersedes the legacy tc* columns for export/detail; old POs fall back to tc*).
Tests rewritten for category creation + catalogue/default helpers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per review: the five named PO T&C slots now allow a one-off custom clause as
well as picking a catalogued one. TermsField becomes a native <input list> +
<datalist> combobox (still plain FormData, no form/page changes). Any current/
custom value is preserved as the input value.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mirrors the Place-of-Delivery (#19) pattern: an admin clause library that feeds
the PO T&C fields as dropdowns. (No "work order" type — POs only, per steer.)
- schema + migration: TermsCondition (category enum + text + isActive); the
migration seeds the prior TC_DEFAULTS as the starting clauses.
- permission manage_terms (Manager + SuperUser + Admin).
- admin screen /admin/terms: table + Add/Edit dialogs + activate/deactivate +
delete (mirrors /admin/delivery-locations); sidebar link under Administration.
- PO forms (new / edit / manager-edit): the five named T&C slots (Delivery /
Dispatch / Inspection / Transit Insurance / Payment Terms) become a shared
<TermsField> select sourced from active clauses of that category; "Others"
stays free text; the fixed boilerplate lines are untouched.
- tc* columns stay free-text SNAPSHOTS (export/import unchanged); a current value
not among active clauses is preserved as a "(current)" option.
- tests: terms CRUD + permission guard + grouping helper (6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The /history page fetched a fixed first 200 POs in one flat table with no
way to page further and no control over page size. Replace that with real
pagination:
- page.tsx: read page/perPage from searchParams; clamp perPage to 25/50/100
(default 25) and page to [1, totalPages] via a new shared resolvePagination
helper. Swap the fixed take:200 for skip/take + a count() for totals.
Replace the "first 200 results" notice with a footer ("Showing X-Y of N",
Prev/Next, page indicator) that preserves all filters. Export PDF/CSV links
stay on the full filtered set.
- history-filters.tsx: add a Per-page dropdown; changing it or any filter
resets to page 1 while preserving perPage in the URL.
- lib/pagination.ts: dependency-free clamp/skip/take helper, unit-tested.
Verified: type-check clean, 272 unit tests pass (9 new), skip/take windows
and clamping checked against the test DB.
Fixes#104
Run #120 (v0.3.0 deploy) failed at the microservice step: every service folder
and ecosystem.config.js were "absent", and pm2 reported "File ecosystem.config.js
not found". Root cause: ~/pms on pms1 is a sparse checkout limited to App/, so
`git checkout -f $TAG` never materialised the service folders or the root
ecosystem.config.js. The app itself deployed fine (App/ is in the sparse set) and
prod stayed healthy.
- deploy.yml: before managing services, disable sparse-checkout (and clear the
legacy core.sparseCheckout config + .git/info/sparse-checkout), then re-checkout
the tag to materialise the full tree. Idempotent / no-op once expanded.
- Guard the pm2 call: if ecosystem.config.js is still absent, fail with a clear
diagnostic (+ sparse-checkout list) instead of the cryptic PM2 error.
- README: note the sparse-checkout expansion.
Needs a fresh tag (e.g. v0.3.1) to re-run the deploy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The v* tag deploy previously only updated the Next app (ppms); GstService /
EpfoService / PdfService were never built or restarted by automation. Now the
same deploy manages them.
- ecosystem.config.js (root): pm2 definitions for gst-service (3003) /
epfo-service (3004) / pdf-service (3005). Registers only services whose source
is checked out (keyed on package.json), so a not-yet-merged service is skipped
and adopted automatically once its PR lands. Secrets come from the env at pm2
invocation; ports are fixed here.
- deploy.yml: after the app restart, export the few service secrets out of
App/.env (never PORT or the ephemeral FORGEJO_TOKEN), npm install + playwright
install chromium + build each present service, then
`pm2 startOrReload ecosystem.config.js --update-env` (create on first release,
reload after) + pm2 save, and health-check :3003/:3004/:3005.
- automation/README.md: documents the flow + the one-time alignment for any
pre-existing differently-named pm2 process.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds an "Email to vendor" button on the PO detail (available once approved,
through CLOSED, and again after payment) that opens an Outlook draft addressed
to the vendor's primary contact with a time-limited PDF download link.
Since mailto: can't attach files, the PDF is rendered and stored, and the draft
carries a link (the approach chosen for this issue):
- PdfService/: new standalone Express + Playwright microservice (GstService/
EpfoService pattern) — POST /pdf { url } renders a page to a real PDF via
headless Chromium. SSRF-guarded (shared token + optional origin allowlist).
- export route: accepts a server-only `svc` token (PDF_SERVICE_TOKEN) so
PdfService can fetch /api/po/[id]/export?format=pdf without a user session;
`pdf=1` drops the print button + window.print() auto-trigger.
- lib/pdf-service.ts renderPoPdf(); prepareVendorEmail() server action renders →
uploads to R2 (po-pdf/…) → presigns a 7-day link → returns a mailto draft.
- po-detail: EmailVendorButton, shown when approved + vendor has a contact email.
- Gated by PDF_SERVICE_URL/PDF_SERVICE_TOKEN; friendly error if unconfigured.
- No DB model/migration. Tests: prepareVendorEmail (6, PdfService/storage mocked).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the free-text "Place of Delivery" with a dropdown sourced from a new
admin-managed Delivery Locations list (each = a Company FK + free-text address).
- schema + migration: new DeliveryLocation model (companyId, address, isActive).
- permission: manage_delivery_locations granted to Manager + SuperUser + Admin
(Manager-accessible, not admin-only, per the issue).
- admin screen /admin/delivery-locations: table + Add/Edit dialogs +
activate/deactivate + delete (mirrors /admin/sites); sidebar link under
Administration for Manager/SuperUser/Admin.
- PO forms (new / edit / manager-edit): shared <DeliveryLocationField> native
select populated from active locations, formatted "Company — address".
- PurchaseOrder.placeOfDelivery stays a free-text SNAPSHOT (no FK) — the dropdown
only changes how the value is picked, so export/import/historical POs are
unchanged, and an edit preserves a current value not in the list as a
"(current)" option. Deleting a location is therefore always safe.
- tests: delivery-location CRUD + permission guard (6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The structured payment-request lane (#91) should extend this column for the
ADVANCE/PART 'exact sum due', not add a parallel field.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The approving Manager decides how much of the PO is paid first, via a
0–100% slider on the approval card (default 100% = full). The slider is
convenience only — the resolved ABSOLUTE amount is stored on
PurchaseOrder.suggestedAdvancePayment (Decimal(12,2), nullable).
- schema + migration: add suggestedAdvancePayment (null = no explicit
advance ⇒ full payment, preserves legacy behaviour).
- approvePo(): accepts the amount, clamps to [0, totalAmount], persists
it, records it on the APPROVED audit row. Set once at approval; never
edited afterwards.
- approval-actions.tsx: whole-percent slider showing the resolved ₹
amount + remaining balance; value sent with Approve / Approve-with-Remarks.
- Accounts surface: payment queue + PO detail show the advance; it
prefills the FIRST payment amount (only when nothing is paid yet and
it is a true partial). Balance runs the normal PARTIALLY_PAID loop.
- Not shown on the exported PO/invoice (po-export-layout untouched).
- Tests: persist + audit metadata + clamp-to-total.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rank held applies to every candidate, not just ex-hands; it auto-updates
for returning crew on sign-off. Ex-hand designation is decoupled from the
Source dropdown and owned by the office:
- Candidate form: drop the EX_HAND source option, relabel "Rank held
(ex-hands)" to "Rank held". addCandidate always intakes NEW/CANDIDATE
(ex-hand recognition still reuses an existing EX_HAND row); updateCandidate
no longer rewrites type/status, so an admin-set EX_HAND or onboarded
EMPLOYEE is never clobbered by a candidate edit.
- Admin crew form: the type NEW/EX_HAND select becomes an "Ex-hand
(returning crew)" checkbox -- the only place ex-hand is tagged.
- List/detail ex-hand indicators key on type === EX_HAND (not source).
- Sign-off preserves the original recruitment source when flipping to EX_HAND.
- Tests seed EX_HAND rows directly; assert candidate intake stays NEW.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Purchasing, Crewing and Administration headings are now collapsible
buttons (chevron + aria-expanded/aria-controls) that collapse by
default. Single-open accordion: opening one heading collapses any
other open one. The section containing the active route auto-expands
on mount/navigation so the user is never stranded on a hidden link.
Adds a jsdom/Testing Library unit test covering default-collapsed,
toggle, single-open accordion, and active-route auto-expand.
Fixes#96
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
refresh-test-db.sh mirrored prod with `pg_dump --clean`, which only drops objects
that exist in prod. Objects created on pelagia_test by a previously-applied
UNRELEASED migration (e.g. crewing tables/types not yet released to prod) survived
as leftovers, then collided with the `migrate deploy` replay ("type already
exists", P3009) — blocking every later migration and 500-ing the staging app.
Drop+recreate `public` before the restore so the test DB starts from exactly
prod's objects and unreleased migrations apply cleanly. Adds a guard that refuses
to run unless the derived target is the expected test DB.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Extract EpfoService's pure stub + validation logic into a dependency-free
module (EpfoService/src/stub.ts); index.ts now uses it in its stub branches so
the tested logic IS the production stub behaviour.
- epfo.test.ts (App integration): the deterministic stub contract
(OTP 000000 → matched, UAN/OTP validation, session expiry) and the Next proxy
routes' verify_bank_epf gate — 401 unauthenticated, 403 for the MPO, Accounts
passes through to a mocked upstream, body validated before the upstream call.
No EPFO_LIVE, no running service.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- A3 AC2: each requisition row shows its candidate count (sourced via
_count.applications in the list query) alongside the existing days-open age.
- A3 AC1: add rank and reason filters (derived from the visible data, like the
existing vessel/site filter) on top of search + status + location.
requisitions.test.ts asserts the per-row candidateCount (2 vs 0) the page exposes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- AC1: addCandidate recognizes a returning hand re-entered as a fresh candidate
— matched to their existing EX_HAND pool record by email (preferred) or exact
name — and reuses that row instead of creating a duplicate, preserving tour
history/documents/bank. Audited CANDIDATE_UPDATED { exHandRecognized: true }.
- AC2: the Candidates list sorts ex-hands above new candidates by default
(stable, preserving createdAt order within each group).
- AC3: the candidate detail "Returning crew" callout now renders the matched
member's actual tour history (ExperienceRecord) and documents on file.
candidates.test.ts covers email/name recognition, the no-match path, and the
ex-hand-first page ordering.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit-trail & transaction consistency (spec §11 "one transition, one row"):
- Action types: returnSalary/returnSelection/declineInterviewWaiver no longer
mislabel a backward decision as its forward action. New CrewActionType members
SALARY_RETURNED / SELECTION_RETURNED / WAIVER_DECLINED; added RECORD_DELETED;
dropped the unused GATE_FAILED (migration recreates the enum).
- Deletions are audited: deleteDocument / deleteNextOfKin now write a
RECORD_DELETED CrewAction (PII removals are traceable).
- Atomicity: autoRaiseRequisition takes an optional tx so the leave-clash and
sign-off backfills are created INSIDE the approval/sign-off transaction; the
office notification (notifyAutoRaised) fires after commit. An approved leave or
a sign-off can no longer commit without its backfill requisition.
Tests assert the corrected action types (crewing-gates, crew-records) and the
existing clash/sign-off suites still pass with the in-transaction backfill.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- C5 (Epic C5 AC1): advanceStage("verify_competency") now requires ≥1
ReferenceCheck before leaving COMPETENCY_AND_REFERENCES.
- C3 (Epic C3 AC1): verifyDocuments blocks advancement when a mandatory document
for the seat's rank that the candidate holds is expired. Missing-document
presence stays enforced post-onboarding in the verification queue (seafarer
docs aren't collected pre-onboarding) — documented inline + in wiki Tech-Debt.
- C4 (experience): deferred with an inline note (Requisition has no
min-experience field yet — Epic A2 AC1).
applications.test.ts: reference-gate block/pass and expired-required-doc
block/renew-pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- D1: require a Manager-approved SalaryStructure before onboarding; a SELECTED
application with none is now blocked instead of silently binding zero salary
rows.
- D3 AC2: the CREW_ONBOARDED CrewAction records the created IDs
(assignmentId, employeeId, salaryStructureId) in metadata.
- Atomicity: the contract letter is uploaded before the transaction and its row
is created INSIDE it, so onboarding is one atomic write (no half-onboarded
crew member without a contract on failure).
onboarding.test.ts asserts the metadata and the new D1 block (no assignment, the
candidate stays a CANDIDATE).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The crew profile page passed SeafarerDocument.number to the client unmasked for
all roles and all doc types, exposing full Aadhaar/PAN identity numbers to MPO /
Manager / Site staff — contradicting the field's PII annotation and §6 /
Roles-and-Permissions §3 (Aadhaar/PAN are gated to Accounts/SuperUser, same as
the bank account number).
- crew-pii.ts: add documentNumberValue(number, docType, role) — masks AADHAAR /
PAN for non-privileged roles via the existing canViewFullBankEpf gate +
maskTail; non-identity docs (passport, CDC, STCW…) pass through; preserves the
string|null contract.
- crew/[id]/page.tsx: mask the number server-side before it crosses to the client.
- Tests: unit cases for the helper; an integration test that invokes the server
component and asserts the documents prop is masked for MANAGER/SITE_STAFF/MPO
and full for ACCOUNTS/SUPERUSER.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds two integration suites covering reconciliation rulings that the existing
crewing tests left on the happy path only:
- leave-clash.test.ts (R6/A5, §5.3): the cover-subtraction and date-overlap
paths in leaveCausesClash — a same-rank crew already on an *overlapping*
approved leave is not available cover (auto-raises), a non-overlapping leave
still counts (no raise), different-rank crew never count, and a configured
minStrength still met after the leave does not raise.
- crewing-gates.test.ts: salary/selection *returns* are Manager-only and
audited (R8); an interview waiver can never reach a NEW candidate by any path,
incl. the Manager (R2); bank reject requires remarks; PPE / next-of-kin verify
gates are MPO-only with remarks on reject (R11/§8.11); and a SUBMITTED
appraisal cannot be Manager-approved without MPO verification (H3).
Full suite: 245 unit + 225 integration green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Scaffolds EPFO/UAN verification the same way GST works — a standalone Playwright
proxy microservice + an /api proxy + an assisted affordance that records the
result. Aadhaar stays manual (UIDAI-restricted). Stacks on the follow-ups branch.
Behind NEXT_PUBLIC_CREWING_ENABLED.
What's in
- EpfoService/ (new microservice, GstService pattern): Express + Playwright.
POST /otp {uan} → session + OTP request; POST /verify {sessionId,uan,otp} →
member record; GET /health. EPFO is OTP-gated (no anonymous captcha lookup like
GST), so the handshake is two steps. Live portal navigation is gated behind
EPFO_LIVE (default STUB: OTP 000000 → matched) until real selectors/OTP are
validated. README documents the differences + that Aadhaar is out of scope.
- App: /api/epfo/otp + /api/epfo proxies (gated by verify_bank_epf) to
EPFO_SERVICE_URL. EpfDetail += epfoMemberName + epfoCheckedAt (migration
crewing_epfo_check). recordEpfoCheck action persists the EPFO result + audit.
- UI: an "EPFO check" affordance on the verification EPF rows — request OTP →
enter OTP → matched member → record. Aadhaar noted as manual-only.
Tests & docs
- Integration: verification.test.ts gains recordEpfoCheck (records name+timestamp,
Accounts-only gating). type-check clean; full unit (245) + integration (213)
green (RESEND_API_KEY unset).
- .env.example (EPFO_SERVICE_URL/EPFO_LIVE), CLAUDE.md, EpfoService/README.
Note: the EpfoService live portal selectors/OTP are stubbed and must be validated
against a real EPFO session before enabling EPFO_LIVE.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>