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>
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>
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 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>
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>
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>
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
The PO date field was only displayed when a submitter explicitly set it.
For approved POs without an explicit date, the approval date is now shown.
Precedence: submitter-set poDate → approvedAt → createdAt (matches the
export route which already used this logic).
Fixes#5
Add an optional PO Date field to the create and edit PO forms.
Submitters can pick any date (back-dated or forward-dated). If left
blank, the exported PO document falls back to the approved date, then
to the creation date.
Changes:
- Prisma schema: add `poDate DateTime?` to PurchaseOrder
- Migration 20260616000000_add_po_date: ALTER TABLE to add the column
- createPoSchema: add optional `poDate` string field
- new-po-form, edit-po-form: add PO Date picker in Order Information
- create/edit actions: persist poDate to DB
- edit action resubmit snapshot: track poDate changes for manager diff
- po-detail: show PO Date in Order Details; include in resubmit diff banner
- export route: use poDate ?? approvedAt ?? createdAt as the date on
the exported PDF/XLSX document
- validations.test: fix pre-existing costCentreRef→vesselId mismatch
and add poDate test cases
Fixes#4
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New PurchaseOrder.paymentDate field (migration 20260531000002)
- Backfill: existing POs use paidAt, else the earliest payment action date
- Accounts must enter a payment date with the payment reference
- Date input pre-selected to today, max=today (no future dates)
- Validated server-side (required + not in future) in processPaymentSchema
- paymentDate stored on both full and partial payments; paidAt set from it
- Shown on PO detail (Payment Date) and payment history (prefers paymentDate)
- Integration tests updated; added future-date rejection test
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Cost Centre on PO forms now shows only Vessels (plain vesselId field).
Sites are a separate concept and not selectable as cost centres.
- PurchaseOrder.vesselId is required again (NOT NULL restored)
- Vessel.siteId and vessel->site relation removed from schema
- DB migration: drops Vessel.siteId column, restores PO.vesselId NOT NULL
- All PO forms (new/edit/import/manager-edit): plain vessel <select> with
code-prefixed labels (e.g. "HNR1 — HNR 1")
- History, approvals, dashboard, my-orders, payments: back to vesselId
filter params and po.vessel.name display
- Admin vessels: removed Site column and site-assignment dropdown
- Admin sites detail page: removed "Assigned Vessels" section
- Sites table: removed Vessels count column (no longer linked)
- seed-prod.ts and seed.ts: vessels created without siteId
- SearchableSelect accounting code picker retained from previous commit
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Accounting Code search (new/edit/import/manager-edit PO forms):
- New SearchableSelect component (components/ui/searchable-select.tsx):
type-to-filter by code or name, results grouped by sub-category with
sticky headers, highlighted selected item, clear button, Escape/outside-click
to dismiss
- Replaces the plain <select> for the main Accounting Code field on all PO forms
- LineItemsEditor per-row account column also uses SearchableSelect (compact size)
when multi-account mode is active
Cost Centre dropdown reorganised by site:
- New type CostCentreGroup replaces flat CostCentreOption
- Each site becomes an <optgroup> label (unselectable); the site itself is the
first selectable option inside ("Haldia (Site)"), followed by its vessels
- Vessels with no site assigned appear under an "Unassigned Vessels" group
- Shared helpers buildCostCentreGroups() and buildAccountGroups() in
lib/cost-centre-groups.ts — used by all four PO form pages
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Undo Vessel→Cost Centre rename in admin (admin shows "Vessel Management" again)
- Sidebar: "Cost Centres"→"Vessels", "Accounts"→"Accounting Codes"
- PO forms (new/edit/import/manager-edit) now show both Vessels (with code) and Sites in the
Cost Centre dropdown, encoded as v:<id> / s:<id> via a costCentreRef field
- vesselId on PurchaseOrder is now nullable; siteId is set when a site is the cost centre
- History, approvals, dashboard, my-orders, payments display vessel.name ?? site.name as Cost Centre
- History and approvals cost centre filters use costCentreRef URL param supporting both types
- Admin vessel form: adds Site assignment dropdown
- Admin accounts: renamed to "Accounting Code" throughout (pages, forms, sidebar)
- PO detail and exports: "Account" label renamed to "Accounting Code"
- Site detail: "Assigned Vessels (Cost Centres)" heading; vessel detail breadcrumb fixed
- Create PO links from vessel/site detail use ?costCentreRef= param
- Export routes handle costCentreRef filter param (with legacy vesselId fallback)
- DB migration: ALTER TABLE PurchaseOrder ALTER COLUMN vesselId DROP NOT NULL
- CLAUDE.md updated with Cost Centre Model documentation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Derives the author from the most recent EDITS_REQUESTED / REJECTED /
APPROVED action that carries a note. PO detail banner now shows 'Note from
[name]', edit-page banner shows 'Edits requested by [name]', and the
closed-orders list prefixes the truncated note with the author's name.
No schema changes required - uses the already-fetched actions with actor.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Allow accounts to record partial/advance payments against a PO before
full delivery. A new PARTIALLY_PAID status tracks in-progress payment;
paidAmount accumulates across multiple markPaid calls. PO only closes
when both paidAmount >= totalAmount AND all line items are delivered.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
qtyChanged/priceChanged/nameChanged were tracked for the diff view but
gstChanged was missing, so an edited GST % showed only the new value
with no strikethrough of the old one. Added gstChanged detection and
two-line rendering (old % struck through, new % in amber) to match the
other diff columns.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
parseFloat('0') is falsy in JS so `|| 0.18` silently discarded the user's
explicit 0% selection. Replaced with an explicit empty-string guard.
Adds e2e spec gst-rate.spec.ts covering all five GST rates (0/5/12/18/28%).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>