Commit graph

25 commits

Author SHA1 Message Date
47ac2c7813 feat(reports): weekly granularity, custom compare, line-item allocation
All checks were successful
PR checks / checks (pull_request) Successful in 48s
PR checks / integration (pull_request) Successful in 31s
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>
2026-06-24 11:25:05 +05:30
8c6bbd8304 feat(reports): Purchasing spend analytics (Cost Centres + Accounting Codes)
All checks were successful
PR checks / checks (pull_request) Successful in 46s
PR checks / integration (pull_request) Successful in 31s
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>
2026-06-24 07:52:23 +05:30
3babfe26ef feat(po): user-defined T&C categories + dynamic PO terms editor (#11)
All checks were successful
PR checks / checks (pull_request) Successful in 45s
PR checks / integration (pull_request) Successful in 32s
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>
2026-06-24 04:43:24 +05:30
8206483f88 Merge remote-tracking branch 'origin/master' into feat/email-po-to-vendor
All checks were successful
PR checks / checks (pull_request) Successful in 43s
PR checks / integration (pull_request) Successful in 30s
# Conflicts:
#	App/app/api/po/[id]/export/route.ts
2026-06-24 02:50:32 +05:30
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
6da6c277ad Merge remote-tracking branch 'origin/master' into feat/submitter-view-all
All checks were successful
PR checks / checks (pull_request) Successful in 43s
PR checks / integration (pull_request) Successful in 30s
# Conflicts:
#	App/CLAUDE.md
#	App/components/layout/sidebar.tsx
#	App/lib/feature-flags.ts
2026-06-23 21:50:08 +05:30
e193e26368 feat(crewing): EPFO/UAN assisted verification (GstService pattern, flagged)
All checks were successful
PR checks / checks (pull_request) Successful in 40s
PR checks / integration (pull_request) Successful in 30s
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>
2026-06-22 22:43:24 +05:30
da2d856b73 feat(po): submitter view-all of POs + History + export (feature-flagged)
All checks were successful
PR checks / checks (pull_request) Successful in 34s
PR checks / integration (pull_request) Successful in 26s
Gated behind NEXT_PUBLIC_SUBMITTER_VIEW_ALL_ENABLED (opt-in, "true").
When on, submitter roles (TECHNICAL/MANNING) get read-only access to every
PO: the History page + report export, any other user's PO detail page, and
the per-PO Export PDF/XLSX buttons. No approval/payment/edit rights are added.

- lib/feature-flags.ts: SUBMITTER_VIEW_ALL_ENABLED flag
- lib/permissions.ts: isSubmitterRole / submitterCanViewAll / canViewAllPos
- po/[id] page + export route: gate via canViewAllPos
- history page + reports/export route: OR submitterCanViewAll into export_reports
- sidebar: show History to submitters when flag on
- tests: permission helpers, both flag states
- docs: .env.example, CLAUDE.md (wiki updated separately)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 04:57:11 +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
791e99f3fd Merge commit '859be8c8d0' into fix-pr34
# Conflicts:
#	App/app/(portal)/history/history-filters.tsx
2026-06-19 12:35:35 +05:30
Claude (auto-fix)
fdc3ebdac9 fix(dashboard): count all POs approved this month, not just current MGR_APPROVED
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
2026-06-19 12:07:53 +05:30
Claude (auto-fix)
e94c7f99a3 feat(history): allow filtering PO history by multiple statuses
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>
2026-06-19 11:53:42 +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
280966a369 refactor: revert cost centre to vessels only, remove vessel-site link
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>
2026-05-30 18:14:24 +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