Commit graph

14 commits

Author SHA1 Message Date
3edd1ffcc5 feat(po): email PO to vendor — PDF link in an Outlook draft (#14)
All checks were successful
PR checks / checks (pull_request) Successful in 43s
PR checks / integration (pull_request) Successful in 31s
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>
2026-06-24 02:45:48 +05:30
65a9335de1 fix(po): keep the export stamp clear of the signature (no overlap)
All checks were successful
PR checks / checks (pull_request) Successful in 35s
PR checks / integration (pull_request) Successful in 26s
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>
2026-06-21 15:35:09 +05:30
610c9aa56d fix(po): centre signature over name; stamp to its right and behind it
All checks were successful
PR checks / checks (pull_request) Successful in 34s
PR checks / integration (pull_request) Successful in 25s
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>
2026-06-21 13:45:56 +05:30
6677ef4fcf fix(po): size XLSX export images by pixels (aspect preserved)
All checks were successful
PR checks / checks (pull_request) Successful in 34s
PR checks / integration (pull_request) Successful in 26s
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>
2026-06-21 13:27:15 +05:30
0b10ba5e54 feat(po): cancel POs (manager/superuser) + optional supersede link (#53)
All checks were successful
PR checks / checks (pull_request) Successful in 32s
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>
2026-06-21 12:20:54 +05:30
1071cb226f feat(po): per-company logo, stamp & brand bar on exported POs
All checks were successful
PR checks / checks (pull_request) Successful in 31s
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>
2026-06-21 01:17:23 +05:30
Claude (auto-fix)
d7be141589 fix(export): include optional line item description in PDF and XLSX exports
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>
2026-06-19 03:43:49 +05:30
Claude (auto-fix)
25d1164d34 feat(po): allow submitter to set an optional PO date
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>
2026-06-16 13:06:12 +05:30
6763a60421 fix(export): widen columns and increase row heights to prevent cell cutoff
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 02:29:33 +05:30
4cb927cbd0 feat(companies): add invoiceEmail field separate from contact email
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-31 01:32:17 +05:30
e308d86e93 feat: Companies — multi-company PO support with admin CRUD and export integration
Schema:
- New Company model (name, gstNumber, address, telephone, mobile, email, invoiceAddress, isActive)
- PurchaseOrder.companyId FK (optional, SET NULL on company delete)
- Migration: 20260530000003_add_company

Admin:
- /admin/companies page with full CRUD (create, edit, deactivate, delete)
- Companies table shows name, GST, contact details, status
- Companies link added to Admin section of sidebar (Briefcase icon)

PO forms (new / edit / import / manager-edit):
- Company dropdown appears at the top of Order Information when companies exist
- Pre-populated with first active company; selection persisted to DB via companyId

Import form:
- parseSheet() now extracts companyName from Excel row 1 (col A)
- Import preview auto-matches detected company name against known companies
- Shows detected name as a hint; user can override before saving

Export (PDF + XLSX):
- Company constants (CO_NAME, CO_ADDR, CO_TEL, INV_ADDR, INV_GST) are now
  derived from the linked Company record when present, falling back to the
  original Pelagia Marine hardcoded defaults when no company is set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 19:31:34 +05:30
cc7251e6b7 feat: Cost Centre covers vessels and sites, vessel codes, Accounting Code rename, vessel-site assignment
- 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>
2026-05-30 03:04:29 +05:30
32ea27331c fix(po): tighten filters and export data 2026-05-22 17:15:03 +05:30
19029a5a77 chore: restructure repo — flatten App/pelagia-portal to App, rename Prototype→Wireframe and Spec→Design
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 23:18:58 +05:30