Sibling to claude-issue-watcher.sh: polls open Claude-raised PRs (head
branch under claude/, or labelled claude-pr) for review comments carrying
the marker `claude-review:` — in the PR conversation, review summaries, or
inline on-file comments — and runs headless Claude Code on the PR's own
branch to address them, pushing the follow-up commit(s) to the same branch.
- Authorization gate: only repo collaborators (write access) + the owner can
trigger it; the bot's own comments are ignored.
- Idempotent: handled comments are tracked by a hidden marker on the bot's
acknowledgements, so the 10-min poll never redoes a comment.
- Own clone (~/pelagia-pr-review), config, and lock so it never races the
issue watcher. Token needs write:repository + write:issue.
Adds the script, an example config, .gitignore entries for the live
config/lock, and an automation/README.md section with deploy + cron steps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The comparison charts (and detail-page breakdown swatches) rendered every
series in recharts' default colour instead of the per-item palette.
Root cause: `SERIES_COLORS` was defined in `components/reports/charts.tsx`,
which is a "use client" module. The report **pages are server components** and
imported the palette from it. A plain value imported from a client module into
a server component is a client-reference proxy, not the real array — so
`SERIES_COLORS[i % SERIES_COLORS.length]` was `SERIES_COLORS[NaN]` → undefined,
every line got `stroke={undefined}`, and recharts fell back to #3182bd. (The
literal `strokeWidth={2}` still applied, which is why only the colour was wrong.
It passed jsdom tests because those import the array directly, not across the
RSC boundary.)
Fix: move the palette to a dependency-free shared module `lib/report-colors.ts`
(no "use client", no server-only imports) that resolves to the real array in
both server and client graphs. `charts.tsx` and all four report pages import it
from there. It can't live in `lib/reports.ts` (that imports Prisma `db`, which
must not enter the client bundle).
Verified in a real browser: line strokes now cycle the 10-colour palette
(#2563eb, #16a34a, …) instead of a uniform #3182bd.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a Playwright suite (App/tests/staging/) that logs into the running staging
instance (ppms-staging, :3200) and verifies each closed portal issue is actually
fixed — feature level, driving the real UI, one spec per issue.
To make credential login possible against the prod-mirror pelagia_test (which only
holds real, mostly SSO-only users), prisma/seed-test-users.ts idempotently seeds one
known-password @pelagia.local user per role, and automation/refresh-test-db.sh runs
it after every daily refresh so the logins persist on staging.
Result against staging: 41 passed, 1 skipped (#10 — no attachment data on staging).
Two closed issues were found NOT fixed and are recorded as documented test.fail():
- #13 Accounts "payments completed this month" card is absent.
- #24/#40 logout tooltip still reads "Sign out" (pipeline test issues).
Docs/TESTING.md documents the suite, the seeded users, how to run it against
staging, and the full issue -> script mapping.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The monthly/weekly comparison already drew one colour per item (series =
items). Yearly mode instead coloured by financial year (series = FYs, items on
the x-axis), so multiple cost centres / accounting codes in the same yearly
graph shared colours. Unify all three granularities to series = items: the
x-axis is months / weeks / FYs and each item keeps its own distinct colour
(yearly becomes grouped bars per item rather than per year).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Picks up the three pieces deferred from the initial reports PR:
#3 Line-item account allocation — allocatePoSpend() splits each PO across the
accounting codes its line items carry (line accountId, falling back to the
PO-level account), proportionally so per-PO rows sum back to totalAmount. The
accounting-code report now attributes multi-account POs correctly. SpendRow
gains poId; poCount is now distinct POs, not row count.
#2 Custom "Add to graph" — tick rows on either index (SelectCheckbox links
write ?sel=id1,id2), then "Compare selected" (?cmp=1) shows a custom comparison
of just those entities. Fully server-rendered + shareable; export honours sel.
#1 Weekly granularity — a third Granularity that focuses one FY month and
buckets spend by week-of-month (W1–W5) from approvedAt, with a Month picker in
the toolbar. Real buckets (not the mockup's synthetic split).
All three are URL-driven like the rest, so no client fetching. Charts/KPIs/
detail trends all branch on the new mode.
Tests: +8 unit cases (allocation proportional/fallback/empty, weekly buckets,
sel parse/toggle, month + granularity parsing); fixture updated for poId/week.
Full unit suite 311 green; tsc clean. Smoke-tested weekly + custom-compare +
exports end-to-end (all 200). Docs + wiki updated to mark them implemented.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implements the wiki "Reports Mockup" as a Reports → Purchasing sidebar section,
wired to real approved-PO spend.
Two report families, each index → drill/detail:
- Cost Centres (/reports/cost-centres) — spend compared across vessels; row
opens a cost-centre report with a Top-accounting-codes breakdown re-pivotable
by tier (Heading/Sub/Leaf) + Top-N.
- Accounting Codes (/reports/accounting-codes) — drills the Account tree
(headings → sub → leaves) via ?parent=; a leaf opens its report broken down by
cost centre (or, for a non-leaf, by sub-account).
Shared: a pinned filter toolbar (Granularity Monthly/Yearly, Financial Year,
Show Top5/Top10/Bottom5/All) whose values live in the URL query so the server
component re-renders — no client fetching. KPI tiles, recharts comparison/trend/
breakdown charts, per-row trend sparklines, and CSV export (/api/reports/spend).
- lib/reports.ts: the pure, unit-tested aggregation core. Spend = a PO once it
reaches POST_APPROVAL_STATUSES, dated by approvedAt, valued at totalAmount
(the dashboard's basis); Indian Apr–Mar FY; each PO's leaf accountId rolled up
to parents. One query in getReportDataset(), everything else pure.
- Sidebar: new collapsible "Reports" section with a "Purchasing" subheading
(subgroup support added to the Section model). Gated by view_analytics
(Manager/SuperUser/Auditor/Admin); export by the same.
Deferred (documented): synthetic Weekly granularity, the "Add to graph" custom
multi-select, and line-item-level account allocation (v1 uses the PO-level
account). Sites are not cost centres — only vessels.
Tests: 11 unit cases for the aggregation core + 3 sidebar cases for the Reports
section. Full unit suite 303 green; tsc clean. Smoke-tested all routes end to
end against seed data (index/drill/detail/export 200; non-analytics role 307/403).
Wiki: "Reports Mockup" marked implemented; "Pages and Navigation" lists the new
routes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes#18. Navigating away from a PO create/edit screen with unsaved
changes could silently lose in-progress work. The forms now track a dirty
flag and guard both navigation paths:
- Hard navigations (refresh / tab close / external link) → the browser's
native "Leave site?" prompt via beforeunload.
- In-app navigations (sidebar / header / any internal link) → a capture-phase
click interceptor opens a modal offering Save as draft / Discard changes /
Stay on page. Save as draft runs the form's existing draft save (which
redirects to the PO); Discard continues to the intended destination.
The guard (components/po/unsaved-changes-guard.tsx) is reusable and wired into
both new-po-form and edit-po-form. dirty is cleared before a successful submit
so saving never trips the prompt. SPA back-button (popstate) is left to
beforeunload only; the manager inline-edit panel is out of scope (saves in
place, no draft concept).
Tests: 7 new unit cases for the guard (intercept-when-dirty, no-op-when-clean,
external links pass through, Stay/Discard/Save actions, beforeunload arming).
Unit suite 296 green; tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The GSTIN lookup rendered its CAPTCHA (image + 6-digit input + Verify /
New image) inline inside the Add/Edit Vendor dialog. AdminDialog has no
internal scroll and is vertically centred, so the taller form pushed its
footer (Cancel / Create Vendor / Save) off-screen and out of reach.
Extract the CAPTCHA into a dedicated popup (CaptchaPopup) overlaid on the
vendor form at z-[60] with an explicit Cancel button and a ✕ close
control. It handles Escape on the capture phase so dismissing the CAPTCHA
does not also close the underlying form. In-flight CAPTCHA errors now
show inside the popup (it stays open so the user can retry / get a new
image); the success line still lands on the main form. The form footer is
never displaced.
Adds a unit test covering popup open on Look up, Cancel closing only the
popup, and a successful verify populating the fields.
Fixes#114
Move New PO, Closed POs, Import PO and History into the Purchasing
section and rename them: "New PO" to "New Purchase Order", "Import PO"
to "Import Purchase Order", "History" to "Purchase Order History".
Per-role visibility is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fully expand the five abbreviated rank names so the canonical seed (which
upserts ranks by code and overwrites name) matches the names loaded into prod:
- PM → Project Manager
- Assistant PM → Assistant Project Manager
- Sr. Dredge Operator → Senior Dredge Operator
- Jr. Dredge Operator → Junior Dredge Operator
- Sr. Fabricator → Senior Fabricator
Hierarchy, codes, category, isSeafarer and grantsLogin are unchanged. (The
prod Rank table was seeded with these 19 ranks out-of-band; this keeps the
source of truth in sync so a future seed won't revert the names.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>