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>
Partial-receipt flows call confirmReceipt multiple times. The nested
`create` on the Receipt relation threw a unique-constraint error on the
second call when both confirmations supplied notes, preventing any
delivery from completing and blocking attachment uploads.
Changed to `upsert` so subsequent confirmations update the existing
Receipt row's notes instead of failing.
Adds integration tests covering full receipt, partial receipt, the
upsert scenario (two confirmations each with notes), and permission guards.
Fixes#9
The POLineItem model has both a required `name` and an optional `description`
field. The export was only rendering `name` (with description as a fallback),
dropping the optional description entirely when a name was present.
- PDF: renders description in smaller italic text below the item name
- XLSX: appends description on a new line within the cell (wrapText enabled)
- Row height in XLSX now accounts for the extra line when description exists
Fixes#8
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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
Managers and superusers were silently filtered to only their own
submitted POs because submitterId: userId was applied unconditionally.
Submitters were also shown MGR_APPROVED, SENT_FOR_PAYMENT,
PAID_DELIVERED and REJECTED orders alongside CLOSED ones.
Fix: managers/superusers see all CLOSED POs (no submitterId filter);
submitters see only their own CLOSED POs.
Fixes#6
Moves the ItemInventory upsert from confirmReceipt (CLOSED) to approvePo
(MGR_APPROVED) so site inventory is visible as soon as a purchase order
is manager-approved, without waiting for full closure.
- approvePo: fetch lineItems, upsert ItemInventory per site PO line item
that has a productId; revalidate the site admin path.
- confirmReceipt: remove the now-redundant inventory update block.
- Rename approvepo → approvePo for consistency (fixes import mismatch
in the existing integration test file).
- Add three integration test cases covering: site PO inventory increment,
line items without productId are skipped, vessel-only POs are untouched.
Fixes#7
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
- Report Issue button in portal header files a Forgejo issue (portal + claude-queue labels)
- Windows scheduled watcher runs headless Claude Code on queued issues and opens a PR
- .forgejo/workflows/deploy.yml deploys v* release tags via the pms1 host runner (pm2 restart ppms)
Co-Authored-By: Claude Fable 5 <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>
Profile (fixes Safari/SSO no-password redirect):
- User lookup falls back to email when JWT id is stale (SSO users)
- generateDownloadUrl wrapped in try/catch so storage never crashes the page
- Signature gate now uses approve_po permission (approvers only)
- SSO/no-password users see a Set Password form (current-password field hidden)
Vendors:
- New create_vendor permission for all PO roles incl. submitters
- Submitters create UNVERIFIED vendors (no Vendor ID); simple form mode
- verifyVendor action + Verify menu item (manage_vendors)
- Vendors auto-verify when a PO closes with them (receipt confirm + import)
- Add Vendor button on /inventory/vendors
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PURCHASING_MGMT roles narrowed to MANAGER only, so the entire Purchasing
section disappears for ADMIN (they never needed the /inventory/ browse links).
Cost Centres (/admin/vessels) added to the top of ADMIN_ITEMS.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds scroll (capture) and resize listeners so the fixed-position dropdown
tracks its trigger as the page scrolls.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/admin/products/[id] requires manage_products, shows Edit + Toggle
/inventory/items/[id] accessible to all, cart only, no edit controls
ProductsTable gains detailBase prop so both list pages link correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- /inventory/items uses same ProductsTable as /admin/products
- canManage driven by manage_products permission on both pages
- /inventory/items/[id] is the canonical detail page (same content,
breadcrumb back to /inventory/items)
- /admin/products/[id] redirects to /inventory/items/[id]
- All ProductsTable name links point to /inventory/items/[id]
- Old items-table.tsx (cart-based browse) retired in favour of shared table
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Any user with manage_products permission (including MANAGER)
can now edit, deactivate, and delete products.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
For each line item with a price and a resolved vendor, upsert a
ProductVendorPrice record (productId + vendorId → price). This keeps
the per-vendor price list in the item catalogue up to date from imports.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>