Commit graph

64 commits

Author SHA1 Message Date
48de2d08a2 fix(notifications): drop redundant "PO" prefix, link all notifications to /po/[id]
In-app notification bodies now lead directly with the PO number
(e.g. "PO-2024-12345 approved" → "PO-2024-12345 approved") without the
word "PO" repeated before the identifier, since the ID already makes
the context obvious.

All notification links now route to /po/[id] so clicking any
notification takes the user straight to the relevant PO detail page,
regardless of their role or the event type.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 16:23:21 +05:30
f0b49c4b96 feat(notifications): in-app bell with real-time badge and per-recipient messages
Schema (migration: 20260516104351_notification_isread_link):
- Notification.isRead Boolean @default(false) — tracks unread state
- Notification.link String? — deep-link URL for each notification

lib/notifier.ts:
- buildInAppBody(): per-recipient message text, context-aware
  e.g. managers see "Maria Santos submitted PO-2024-12345 for your review"
       submitters see "Your PO-2024-12345 has been approved"
       accounts see "PO-2024-12345 approved — ready for payment"
- buildInAppLink(): routes to the correct page per recipient role
  (submitter → /po/[id] or /po/[id]/receipt; manager → /approvals/[id];
   accounts → /payments; etc.)
- Notifications written with both body and link on every event

API:
- GET /api/notifications — returns { unreadCount, notifications[] } for
  the session user; last 20 ordered by sentAt desc
- PATCH /api/notifications/read — marks all (or specific ids) as read

NotificationBell (components/layout/notification-bell.tsx):
- Bell icon in header with red unread count badge
- Polls /api/notifications every 30 seconds
- When unread count increases vs previous tick: bounces the bell icon
  and pulses the badge for 3 seconds to signal a new arrival
- Click opens dropdown panel:
  - Unread items highlighted with blue-dot indicator and bolder text
  - Each item is a Link to the notification's deep-link URL
  - "Mark all read" button in panel header
  - Auto-marks all as read when panel opens (optimistic update + API call)
  - Closes on outside click
  - "Showing 20 most recent" footer when list is at limit

Header: receives initialUnreadCount and initialNotifications as props
Portal layout: fetches initial notification data server-side to avoid
a loading flash on first render; dates serialised to ISO strings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 16:16:06 +05:30
3556b1425f feat(profile): user profile page, manager signature, and SuperUser access requests
Schema (migration: 20260516103515_user_profile_signature):
- User.signatureKey String? — storage key for the manager's approval signature
- New RequestStatus enum (PENDING / APPROVED / DENIED)
- New SuperUserRequest model — tracks access-escalation requests from users

Profile page (/profile) — all roles:
- Account info panel (name, email, employee ID, role — read-only)
- Change password form (validates current password, bcrypt hash on save)
- Signature uploader (MANAGER / SUPERUSER only) — PNG/JPG/WebP up to 2 MB;
  previews before save; can remove existing signature
- SuperUser access request form — textarea for reason, shows current request
  status (pending / approved / denied) after submission

Signature gate on approval page (/approvals/[id]):
- Server checks if the current manager has uploaded a signatureKey
- If missing: shows an amber warning banner with a deep-link to /profile
  instead of rendering the approval action buttons; managers cannot
  approve, reject, or request edits without a signature on file

PDF and XLSX exports:
- Fetches the approver's signature image from storage after identifying
  the approval action
- PDF: embeds as base64 data URI <img> above the approver name in the
  left signature block
- XLSX: inserts the image into the sig-row cells via ExcelJS addImage;
  adds a name row below the image for legibility

SuperUser requests admin page (/admin/superuser-requests):
- Pending requests listed with user info, role, reason, and Approve/Deny buttons
- Approve: sets user.role = SUPERUSER and closes the request
- Deny: marks request DENIED, user role unchanged
- Resolved history table below

Admin user management updates (/admin/users):
- "SuperUser" button (ShieldCheck icon) on every non-superuser, non-admin row
- Directly grants SUPERUSER role and auto-closes any open request for that user

lib/storage.ts:
- buildSignatureKey(userId, ext) helper
- uploadBuffer(key, buffer, contentType) — server-side write to dev-uploads or R2
- downloadBuffer(key) — server-side read from dev-uploads or R2 presigned URL

Sidebar:
- "My Profile" link (UserCircle) visible to all roles
- "SuperUser Requests" link (ShieldCheck) in admin section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 16:09:30 +05:30
3b3a26eafe feat(receipt): allow partial receipt confirmation with per-item delivery tracking
Submitters can now mark individual item quantities as received when
confirming delivery, rather than treating a PO as all-or-nothing.

Schema (migration: 20260516103013_partial_receipt):
- POStatus: new PARTIALLY_CLOSED value between PAID_DELIVERED and CLOSED
- ActionType: new PARTIAL_RECEIPT_CONFIRMED value
- POLineItem: new deliveredQuantity Decimal? field — accumulates delivered qty
  across multiple receipt events

State machine:
- PAID_DELIVERED → confirm_partial_receipt → PARTIALLY_CLOSED (new)
- PARTIALLY_CLOSED → confirm_receipt → CLOSED (all delivered)
- PARTIALLY_CLOSED → confirm_partial_receipt → PARTIALLY_CLOSED (more partial)

Receipt page / form:
- Loads line items with ordered qty, previously delivered qty, and remaining qty
- Per-row numeric input for "receiving now" defaulting to all remaining
- "Mark all remaining" shortcut
- Dynamic button: "Confirm Partial Receipt" vs "Confirm Receipt & Close PO"
- Info banner telling user if the PO will stay open or close

Receipt action:
- Accumulates deliveredQuantity per line item
- If all lines fully delivered → CLOSED + fires notifications + updates inventory
- If any line still outstanding → PARTIALLY_CLOSED (no notifications yet)
- Inventory auto-update runs per-event for the delivered quantities only

Dashboard & PO detail:
- Open Orders count now includes PARTIALLY_CLOSED
- "Confirm Receipt" CTA in po-detail handles PARTIALLY_CLOSED with
  distinct amber styling and "Confirm Remaining" label
- Activity log shows PARTIAL_RECEIPT_CONFIRMED with appropriate label
- PARTIALLY_CLOSED gets warning (amber) badge variant

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 16:02:44 +05:30
b2bfa63f61 fix(export): show approver as signatory on PDF and XLSX reports
The left signature block on generated PO documents (PDF and XLSX) was
showing the submitter's name as the authorized signatory. The signatory
should be the manager who approved the PO, since they are the one
authorizing the expenditure on behalf of the company.

The approvedBy name was already extracted from the APPROVED / APPROVED_WITH_NOTE
action at the top of the export route — it just wasn't being used in the
signature block. Now both the PDF and XLSX templates use approvedBy for
the left sig block, falling back to the submitter only if the PO has not
yet been approved (e.g. when exporting a draft).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 15:57:39 +05:30
4c1a41fe61 feat(accounts): add payment history page at /payments/history
Accounts users had no way to review POs that had already been processed
through the payment pipeline. The existing /history page requires the
export_reports permission which accounts does not hold.

New page at /payments/history:
- Scoped to PAID_DELIVERED and CLOSED statuses only
- Gated on view_all_pos permission (held by ACCOUNTS, MANAGER, SUPERUSER, etc.)
- Filterable by paid-date range (paidAt) and cost centre
- Columns: PO number, title, cost centre, vendor, submitter, status badge,
  payment ref, amount, paid date
- Summary bar at top showing total paid amount and order count
- 200-row soft limit with prompt to refine filters

Sidebar:
- Added "Payment History" link (Receipt icon) visible to ACCOUNTS and SUPERUSER
- Removed ACCOUNTS from the /history nav item since that page requires
  export_reports which accounts does not have (fixes the dead sidebar link)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 15:56:07 +05:30
0252e8eab4 feat(approvals): highlight submitter's edits to manager on resubmission
When a submitter edits and resubmits a PO after the manager requested edits
(EDITS_REQUESTED → MGR_REVIEW), the manager now sees exactly what changed.

Changes:
- edit/actions.ts: before mutating the PO, snapshot the current state
  (line items + header fields incl. vessel/account/vendor names) into the
  SUBMITTED action's metadata as { editSnapshot: { lineItems, fields } }.

- po-line-items-editor: add `originalItemsLabel` prop so the diff banner
  message can be context-specific (manager edit vs. submitter resubmit).

- po-detail: detect the resubmit snapshot from the most recent SUBMITTED
  action with editSnapshot metadata; show a "Changes from last review"
  amber table listing every header field the submitter changed (title,
  cost centre, account, vendor, project code, date required, place of
  delivery); pass the resubmit snapshot as originalItems to LineItemsEditor
  so changed line items are highlighted with strikethrough on prior values.
  Resubmit snapshot takes priority over manager-line-edit diff.
  Panel is only visible to MANAGER / SUPERUSER when status is MGR_REVIEW.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 04:17:51 +05:30
4737edcee9 fix(dashboard): use color-coded PoStatusBadge on submitter and manager dashboards
The Technical/Manning dashboard's Recent Orders table was rendering all PO
statuses as a hardcoded gray pill span. The Manager dashboard's Recent
Approved Orders table was similarly hardcoded to success-green for every row,
regardless of actual state (Approved vs Sent for Payment vs Paid).

Replace both with the existing PoStatusBadge component which maps each
PO status to the correct CVA variant (success/warning/danger/default/outline)
per the design-system colour palette defined in PO_STATUS_VARIANTS.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 04:11:22 +05:30
c97e4597dd TODO update 15/05 2026-05-16 03:12:01 +05:30
bf6058fcdd docs: add Playwright test design doc with scripts for inventory sort and tag fixes 2026-05-16 02:43:29 +05:30
55065eafe3 feat(items): show Cheapest and Closest tags independent of sort order
Previously tags were tied to the active sort (only Closest showed when
sorting by distance, only Cheapest when sorting by price). Now both are
computed independently — Cheapest = vendor with min price, Closest =
vendor with min distanceKm — and shown simultaneously when a site is
selected, regardless of which sort is active.

Verified with Playwright: both tags present under distance sort, price
sort, and absent when no site is selected.
2026-05-16 02:29:14 +05:30
f1640c238a fix(items): auto-sort by distance when site selected or changed
useEffect keyed on currentSiteId resets sortBy to 'distance' whenever
the site selection changes, overcoming useState's one-time initialisation
which didn't fire on soft navigations (state preserved across router.push).

Verified with Playwright: no-site→price, select-site→distance auto,
manual price switch works, switch-site→distance resets.
2026-05-16 01:58:36 +05:30
42c58d8c15 fix(po): pre-fill vendor from cart when all items share the same vendor 2026-05-16 00:09:15 +05:30
d769cae71e chore(inventory): remove item detail page; move SiteSelect to shared components
- Delete /inventory/items/[id] — items expand inline in the list
- Move SiteSelect from deleted [id] folder to components/inventory/site-select
- Fix admin product detail page import to use new shared path
- Fix items-table: Fragment key prop, restore Link import, plain text item names
- Fix vendor-items-table: remove broken link to deleted item detail page

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 00:07:04 +05:30
bfdf5e73eb fix(items-table): use Fragment with key prop to suppress React list key warning 2026-05-15 23:57:45 +05:30
2fcf35235a feat(cart): header cart icon with badge + fix PO pre-population from cart
Cart icon:
- CartIcon component in header: listens to cart-updated events, shows
  item count badge, navigates to /inventory/cart on click
- Visible for TECHNICAL, MANNING, SUPERUSER, MANAGER roles only

PO pre-population:
- /po/new page reads ?cart= searchParam, parses CartItem[] into
  LineItemInput[], passes as initialLineItems prop to NewPoForm
- NewPoForm accepts initialLineItems and seeds useState from them
  instead of always starting with one blank row
- Cart is cleared immediately when "Create Purchase Order" is clicked
  so re-visiting cart doesn't re-submit the same items

Cart fixes:
- Empty state and "Add more items" links corrected from /admin/products
  to /inventory/items and /inventory/vendors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 23:56:38 +05:30
8750a459f5 feat(inventory): item & vendor detail pages at /inventory/items/[id] and /inventory/vendors/[id]
- New /inventory/items/[id]: vendor price table with distance sorting via
  SiteSelect client component (?siteId= URL param), price chart, stock
  by site, Add to Cart per vendor
- New /inventory/vendors/[id]: contacts panel + searchable items table
  with Add to Cart, links back to /inventory/items/[id]
- SiteSelect: reusable client component (useRouter.push, configurable
  param key) used by both inventory and admin detail pages
- items-table: item names link to /inventory/items/[id]; vendor names
  in expanded rows link to /inventory/vendors/[id]
- vendors-table: vendor names link to /inventory/vendors/[id]
- Fix admin product detail page: replace illegal Server Component
  onChange handler with SiteSelect client component

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 23:46:13 +05:30
8e57381b06 chore(seed): rewrite for current schema with 10+ entries per table
- Vendors: remove deprecated contactName/Email/Mobile fields; use
  VendorContact nested creates with primary flag, role, mobile, email
- Add pincode + lat/lng to all vendors for distance calculations
- Sites: 12 Indian port locations with lat/lng
- Vessels: 11, linked to sites
- Accounts: 12 covering all spend categories
- Users: 12 across all roles
- ProductVendorPrice: 51 entries linking vendors to products
- ItemInventory: 15 stock records across sites
- ItemConsumption: 14 daily consumption records
- PurchaseOrders: 12 spanning full status lifecycle
- Seed is idempotent (all upserts, safe to re-run)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 12:12:22 +05:30
dacd688ebe feat(inventory): vendor browse page with site selector for TECH/MANNING/SUPERUSER
Adds /inventory/vendors with distance-sorted vendor list and URL-param
site selector (?siteId=). Wires Items + Vendors + Cart into sidebar for
TECH/MANNING/SUPERUSER roles; MANAGER keeps admin management views.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 12:01:29 +05:30
e887502e27 refactor(inventory): site selector via URL param (?siteId=) — no localStorage, no user preference
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 11:57:53 +05:30
4919b1d4e4 feat(inventory): inline site selector above items table
Items page now fetches all active sites and passes them alongside
preferredSiteId to ItemsTable. A "Working Site" row appears at the
top of the table — selecting a site calls setPreferredSite, revalidates
the page, and shows distances in the vendor sub-rows. A status hint
("Distances shown from selected site") appears when a site is active;
"No site selected — distances hidden" is the empty-state label.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 00:54:54 +05:30
79897c5b06 feat(vendor): multiple contacts, vendor detail page, and table-based items catalog
VendorContact model
- New VendorContact table (name, role, mobile, email, isPrimary) with
  cascade delete from Vendor
- Old single contactName/Mobile/Email fields removed from Vendor model
- Migration: 20260514191638_vendor_contacts

Vendor form
- ContactsEditor: dynamic list of contact rows — add, remove, mark primary
- Each contact row: name, role, mobile, email, primary checkbox
- Serialised as contacts[i].field form fields; existing single-contact
  section removed

Vendor actions
- parseContacts() reads indexed contacts from FormData
- createVendor creates VendorContact rows in a nested write
- updateVendor deletes all contacts then re-creates them in a transaction

Vendor list page
- Contact column shows primary contact name + email; "+N more" badge

Vendor detail page
- Two-column layout: Vendor Details card + Contacts card
- Contacts displayed with avatar initials, role badge, primary badge
- VendorItemsTable client component: inline search (name, code,
  description), tabular layout with links to item detail

Inventory items page (/inventory/items)
- Rebuilt as a searchable table (ItemsTable client component)
- Columns: Item name/description, Code, Vendor count, Lowest price
- Click any row to inline-expand vendor sub-rows for that item
- Vendor sub-rows: vendor name, verified badge, price, distance (if site
  selected), + Cart button with "Added ✓" feedback
- Sort toggle (Distance / Price) shown in toolbar when a row is open

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 00:50:39 +05:30
902bd5f048 refactor(labels): rename Vessel→Cost Centre and Account/Cost Centre→Account
All user-facing strings updated across 22 files. Backend field names
(vesselId, vessel relation, Vessel model) unchanged.

Vessel → Cost Centre
- Page titles: Vessel Management, Vessel Detail
- Sidebar nav item
- Form labels in New PO, Edit PO, Import PO, Manager Edit, Approvals Search
- Table column headers in Approvals, Dashboard, History, My Orders
- Filter dropdowns ("All vessels" → "All cost centres")
- vessel-form dialogs: Add/Edit/Create
- sites/[id] stat card label
- sites/page column header
- XLSX export row 7 label; PDF HTML label; CSV header
- "Vessel/Office Requisition Number" → "Cost Centre/Office Requisition Number"
- Breadcrumb in vessel detail; "Home port" → "Home site"
- spend-charts heading
- Validation error message

Account / Cost Centre → Account
- po-detail "Account / Budget Head"
- manager-edit-po-form "Account / Cost Centre"
- import-form "Account / Cost Centre"
- accounts/page heading "Account / Cost Centre Management"
- XLSX export "Budget head"; PDF HTML "Budget head"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 00:39:00 +05:30
e9ed0f8eb0 feat(po): multi-account support per line item
Schema
- POLineItem.accountId String? — optional per-row account override
- Account.lineItems back-relation added
- Migration: 20260514185519

Line Items Editor
- New props: multiAccount, accounts, defaultAccountId
- When multiAccount=true: Account column appears in the editable table,
  each row pre-selected with defaultAccountId, independently changeable
- Read-only view: Account column shown when any line item carries an
  accountId (displays account code)

New & Edit PO forms
- "Multiple accounts" checkbox (default off) sits inline with the
  Account label
- When checked: label changes to "Default Account / Cost Centre";
  per-row dropdowns appear in the line items table pre-filled with
  the chosen default account
- Edit form: checkbox initialises to checked when the PO already has
  per-line accounts; existing accountId values are pre-populated
- accountId per line item is included in form data and persisted

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 00:30:23 +05:30
9d1496a3bf fix(po): split T&C Others into editable + read-only fixed line
TC_FIXED_LINE_2 added for the packaging/asbestos clause, rendered as
read-only item 8 (same style as item 1) in both the New PO and Edit PO
forms. Others (item 7) now initialises empty instead of carrying the
fixed text.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 00:21:18 +05:30
c06745a9f9 feat(admin): add delete with guard rails to all entity tables
Adds deleteX server actions and ConfirmDeleteButton to the Items,
Vessels, Sites, Users, and Accounts tables.

Guard rails per entity:
- Items: blocked by non-draft PO line items; nulls draft references,
  cascades inventory, consumption, and vendor prices
- Sites: blocked by non-draft POs; nulls draft PO siteId and vessel
  siteId, cascades inventory and consumption
- Vessels, Accounts: blocked by any PO (FK is non-nullable)
- Users: blocked by any submitted PO; cascades notifications

UI: inline two-step confirm ("Delete X? Confirm / Cancel") using the
shared ConfirmDeleteButton component; errors surface inline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:08:51 +05:30
392bad7549 feat(vendor): pincode-based geocoding, vendor delete, and GST captcha auto-fill
- Replace manual lat/lng fields in the vendor form with a single
  Pincode field; location is auto-filled from the GST GSTIN lookup
- On save the Server Action geocodes the pincode via Nominatim and
  caches lat/lng in the DB so distance queries stay fast
- Add deleteVendor action: blocked by non-draft POs; nulls out draft
  PO vendorId and Product.lastVendorId; cascades ProductVendorPrice
- Add ConfirmDeleteButton shared component (inline two-step confirm)

Migration: 20260514091124_vendor_pincode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:07:43 +05:30
2e6678f829 fix(gst): correct microservice default port and captcha field name
Default port changed 3002 → 3003 in the GstService and both proxy
routes.  The vendor-form was reading `captchaB64` from the API
response but the GstService returns `captchaBase64`, so the CAPTCHA
image was never displayed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 17:07:31 +05:30
f372fae953 feat(gst): replace API-key lookup with Playwright microservice
Problem: GST portal's public taxpayer search (services.gst.gov.in/
searchtp) now requires human CAPTCHA verification but no login.
The BIG-IP WAF blocks direct Node.js HTTP clients via TLS
fingerprinting; Playwright (real Chromium) bypasses it successfully.
Confirmed working: GSTIN 27AAHCP5787B1Z6 → full PELAGIA MARINE
SERVICES data including address, jurisdiction, filing status.

GstService/ (new standalone microservice):
- src/index.ts: Express + Playwright singleton browser
  GET  /health  → { ok: true }
  GET  /captcha → launches browser, loads GST portal, fetches
                  CAPTCHA image from same origin (sets CaptchaCookie),
                  stores BrowserContext in session map (3 min TTL)
                  → { sessionId, captchaBase64 }
  POST /search  → { sessionId, gstin, captcha } → submits form
                  via page.evaluate fetch() using live browser session,
                  closes context, returns parsed taxpayer data
- package.json, tsconfig.json, npm install
- src/test-lookup.ts: interactive CLI test (prompted user for captcha)

App changes:
- Remove playwright dep from Next.js app (was incorrectly added)
- Remove lib/gst-lookup.ts (sandbox.co.in placeholder — unused)
- Remove lib/gst-browser.ts (Playwright singleton — moved to service)
- app/api/gst/captcha/route.ts: thin proxy → GST_SERVICE_URL/captcha
- app/api/gst/route.ts: thin proxy POST → GST_SERVICE_URL/search
- vendor-form.tsx: two-step captcha UI
    Step 1: "Look up" → calls /api/gst/captcha → shows PNG inline
    Step 2: user types 6 digits → "Verify" → calls /api/gst → fills
            form (name, address, lat/lng from Nominatim geocoding)
    Wrong captcha → SWEB_9034 error with retry option
- .env.example: GST_SERVICE_URL=http://localhost:3002

Start the microservice: cd GstService && npm run dev

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 13:27:15 +05:30
bea798324c feat(inventory): Sites, GST lookup, distance sorting, cart, inventory tracking
Schema:
- Site model (name, code, address, lat/lng, isActive)
- ItemInventory model (quantity per product per site, unique[productId,siteId])
- ItemConsumption model (daily usage per product per site)
- Vendor: add latitude, longitude
- Vessel: add siteId (home port)
- PurchaseOrder: add siteId (delivery site)

Permissions: add manage_sites to MANAGER and ADMIN

Sidebar: Inventory section (Vendors, Items, Vessels, Sites, Cart)
for MANAGER and ADMIN; old admin items reorganised

Lib:
- lib/geo.ts: Haversine distance + Nominatim pincode geocoding
- lib/gst-lookup.ts: AbhiAPI GSTIN lookup (ABHIAPI_KEY env var)
- lib/cart.ts: localStorage cart (add/remove/clear + cart-updated event)

API: GET /api/gst?gstin= — validates GSTIN, fetches via AbhiAPI,
  geocodes pincode via Nominatim, returns name/address/lat/lng

Vendor form: GSTIN "Look up" button auto-fills name, address,
  and lat/lng; lat/lng fields editable as override

Sites: full CRUD at /admin/sites; detail page with inventory
  table, consumption recording form, BarChart (stock) +
  LineChart (30-day consumption), linked vessels, recent POs

Vessels: detail page at /admin/vessels/[id] with "Create PO"
  button, PO history and spend summary; accessible to MANAGER

Items detail: price comparison BarChart; site distance filter
  (dropdown → re-render sorted by Haversine distance); "Add to
  Cart" per vendor row; stock-by-site section

Cart: /inventory/cart — localStorage CartView; qty edit, remove,
  clear; "Create PO →" encodes cart into /po/new?cart=...

Receipt/delivery: confirmReceipt now upserts ItemInventory
  (increment qty) for each linked line item, using PO siteId or
  vessel home site as the delivery location

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-14 11:50:11 +05:30
1c7d0b8901 feat(catalog): vendor & item detail pages; enable for MANAGER role
Permissions:
- Add manage_products to MANAGER (alongside existing manage_vendors)

Sidebar:
- Add Items link for MANAGER under main nav (alongside Vendors)

Vendor list (/admin/vendors):
- Name is now a link to /admin/vendors/[id]
- Show item count column

Vendor detail (/admin/vendors/[id]):
- Vendor info card (GSTIN, address, contact)
- Items Supplied table: name (links to item detail), code, last price, updated
- Recent Purchase Orders table

Item list (/admin/products):
- Name is now a link to /admin/products/[id]
- Show vendor count column; reorder columns (name first)
- Add/Toggle buttons shown only for ADMIN

Item detail (/admin/products/[id]):
- Price summary cards (vendor count, lowest price, highest price)
- Available From table: vendor (links to vendor detail), vendor ID,
  verified badge, price (lowest highlighted in green), last updated

Both detail pages cross-link to each other.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 04:25:30 +05:30
f4e0d8ae63 feat(catalog): track per-vendor prices; auto-sync catalog on payment
Schema:
- Add ProductVendorPrice table (productId, vendorId, price, updatedAt)
  with unique constraint on (productId, vendorId)

Payment action (markPaid):
- Auto-create Product for any unlinked line item (matched by name
  case-insensitively, or created fresh with auto-generated code)
- Link POLineItem.productId for newly matched/created products
- Upsert ProductVendorPrice for the PO vendor + unit price
- Always update Product.lastPrice / lastVendorId as denormalized cache

Search API:
- Include vendorPrices[] in results (vendorId, vendorName, price)

Editor dropdown:
- Show per-vendor prices below item name when available

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 03:47:39 +05:30
f95b3279c8 feat(line-items): separate name (mandatory, searchable) from description (optional)
- Add POLineItem.name column; migrate existing description→name; description is now optional
- NameCell component: name input with fuzzy product search, description input stacked below
- Read-only view shows name prominently, description in subdued text below
- All server actions (create, edit, manager edit, import) updated to read/write name
- ParsedImportLine.description renamed to .name throughout import parser and form
- Seed data updated; CLAUDE.md added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 03:37:53 +05:30
2b5e125260 feat(export): match Sample_PO.xlsx formatting in XLSX and PDF exports
XLSX (ExcelJS replaces bare SheetJS):
- Full cell-level styling: bold fonts, gray fills, thin/medium borders,
  cell alignment, wrap-text — all matching the sample PO layout
- Correct merge map across all sections:
  header block (A1:I4), PO meta row (A5:B5, C5:G5), PI/Quotation row
  (A6:B6, C6:F6, G6:H6), Vessel/Budget/Requested row (A7:B7, D7:E7,
  H7:I7), Requisition row (A8:B8, D8:E8, H8:I8), Place of Delivery
  spanning 2 rows (A9:B10, C9:I10), Invoice Details spanning 2 rows
  (A11:B12, C11:I11, C12:I12), Vendor block (A13:B13, D13:I13, A14:B14,
  C14:I14), line item Description column (B:C per row), totals labels
  and values (F:G and H:I), instructions header (A:I), T&C text (B:I
  per row), dual signature blocks
- Description column spans B:C in header and every item row
- Minimum 7 body rows reserved; alternating row fills
- Totals section: gray fill, right-aligned, grand total darker gray
- Instructions header with distinct fill; numbered T&C with B:I merge
- Paired signature blocks (submitter left, vendor right) with borders
- Column widths and row heights tuned to the sample dimensions
- Page setup: A4 portrait, fit-to-width

PDF (HTML print page):
- Typography matches sample: Arial 8.5pt body, 13pt bold company name,
  11pt underlined PURCHASE ORDER heading
- All meta tables use gray (f2f2f2) label cells with borders
- Place of Delivery and Invoice Details use rowspan for correct layout
- Line items table: dark gray (d8d8d8) header, 1px borders, alternating
  row fills, minimum 7 blank rows reserved
- Totals table (55% width, right-aligned) with gray rows and darker
  grand total
- Instructions: distinct header fill, clean numbered layout
- Signature blocks: flex-spaced bordered boxes with submitter/vendor
- Print CSS: A4 page size, no-print class for the print button

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 00:28:34 +05:30
48e1f19e58 test: add comprehensive tests for all new features + test plan
Parser extraction:
- Move parseSheet/parseWorkbook/cellStr/cellNum to lib/po-import-parser.ts
  so they can be unit-tested without HTTP overhead
- Route now re-exports types and delegates to the lib

Unit tests (165 total, all passing):
- permissions.test.ts: +15 cases covering MANAGER create_po/submit_po/
  manage_vendors, ACCOUNTS manage_vendors, AUDITOR all-denied, ADMIN
  operational denial, SUPERUSER no manage_vendors
- po-state-machine.test.ts: +12 cases covering MANAGER submit from DRAFT
  and EDITS_REQUESTED, ACCOUNTS provide_vendor_id, AUDITOR/ADMIN denied
  on all transitions
- po-import-parser.test.ts (new, 32 cases): cellStr/cellNum edge cases;
  parseSheet against real Sample_PO.xlsx (1 line item, correct values,
  T&C not included, vendor/quotation/T&C extraction); synthetic sheet
  edge cases (GST normalisation, INSTRUCTIONS stop, zero-price skip,
  empty rows); parseWorkbook happy path and empty-workbook

Integration tests (new files):
- discard-po.test.ts: owner/MANAGER/SUPERUSER can discard; ACCOUNTS and
  non-owners denied; status guard blocks non-DRAFT; cascade cleanup of
  POActions and POLineItems verified in DB
- vendor-approval.test.ts: approval blocked without vendor; approval
  succeeds with vendor; ACCOUNTS can provideVendorId; unverified vendor
  rejected; AUDITOR denied; wrong-status denied
- manager-po-creation.test.ts: MANAGER creates DRAFT and submits; stores
  correct submitterId; can discard own draft; ACCOUNTS denied; unauth
  returns Unauthorized
- products-search.test.ts: 401 unauth; min-length validation; search by
  name/code/description; case-insensitive; max 10 results; lastPrice as
  number; inactive products excluded
- import-api.test.ts: 401 unauth; 403 for TECHNICAL and ACCOUNTS; 400
  no file; 400 invalid binary; 200 for MANAGER with Sample_PO.xlsx;
  correct line item values; T&C absent from results; vendor/PI extracted

Spec/TEST_PLAN.md (new):
- Testing strategy, stack, and environment setup
- Coverage matrix across unit/integration/E2E layers
- Permission test matrix for all 7 roles × 15 operations
- Feature-level scenario index (F-01 through F-06) with IDs mapping to test files
- Known gaps and out-of-scope items
- Authoring conventions (PREFIX isolation, negative-first, no any)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 19:15:58 +05:30
4a848f50cf fix(import): stop parsing line items at T&C section
Row 26 ("INSTRUCTIONS TO VENDORS") had an empty col-1 so the loop
continued into the numbered T&C rows (27-33), which all have text in
col-1 and were mistakenly added as line items.

Two guards added:
1. Break immediately when col-0 contains "INSTRUCTION" (catches the
   section header even though col-1 is empty).
2. Skip any row where both qty and unitPrice are 0 (belt-and-suspenders
   for T&C rows that might slip through in other PO layouts).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 19:00:20 +05:30
96314d89f8 feat: discard draft POs
Adds a "Discard" button to the PO detail header for any DRAFT PO.
Submitters, managers, and superusers can discard. The action deletes
the PO and its related actions/notifications, then redirects to
/my-orders. Non-cascade child records (POAction, Notification) are
explicitly deleted in a transaction before the PO row is removed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 18:57:01 +05:30
43f0861591 feat: manager PO creation, vendor management, import from Excel, item fuzzy search
Permissions & access:
- MANAGER gains create_po, submit_po, edit_own_draft_po, view_own_pos, manage_vendors
- ACCOUNTS gains manage_vendors
- ACCOUNTS added to provide_vendor_id state transition
- MANAGER added to DRAFT/EDITS_REQUESTED submit allowed roles
- canProvideVendorId now includes ACCOUNTS and any MANAGER/SUPERUSER

Vendor required for approval:
- approvepo() now returns error if po.vendorId is null
- Approval page shows danger banner when vendor is missing

Navigation:
- MANAGER gets "New PO", "My Purchase Orders", "Import PO", "Vendors" nav items
- ACCOUNTS gets "Vendors" nav item

Seed data:
- Vendors: 12 total (up from 3), with GST, address, contact details
- Products: 25 total (up from 4), with lastPrice pre-populated

Product fuzzy search in line items editor:
- Typing ≥2 chars in description fetches /api/products/search?q=
- Dropdown shows code, name, description, last price
- Selecting a product auto-fills description and unit price
- Linked items show a "✓ linked" indicator
- productId passed through FormData to createPo action and stored on POLineItem

Excel PO import (/po/import):
- MANAGER, SUPERUSER, ADMIN can access
- Uploads .xlsx file to /api/po/import which parses the Pelagia PO format
- Extracts vendor, line items, quotation ref, T&C, delivery address
- Preview step: user selects vessel + account, auto-matches vendor by name
- Confirmed import creates PO in DRAFT status

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 18:52:51 +05:30
17586e6ea1 fix: serialize Prisma Decimal fields before server→client boundary
Convert quantity, unitPrice, totalPrice, gstRate, and totalAmount to
plain numbers in server pages before passing to client components,
preventing Next.js serialization errors on Decimal objects.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 17:51:54 +05:30
dd7a40e523 fix(ui): remove font-mono from line items numeric cells; revert formatCurrency
font-mono (monospace) renders optically larger than the surrounding
sans-serif at the same em size. Removing it from quantity, unit price,
taxable, GST% and total cells in both read-only and edit mode makes
all numbers visually consistent with the rest of the page.

formatCurrency reverted to Intl.NumberFormat with style:currency (the
original implementation); the spacing issue the user saw was font-related,
not symbol-related.
2026-05-06 00:50:32 +05:30
6f536734bd fix(ui): cohesive currency symbol and vendor field styling
formatCurrency: manually prepend Rs symbol instead of using Intl currency
style, which could inject a non-breaking space between the symbol and number.

po-detail vendor section: replace font-mono on Vendor ID with font-medium
text-neutral-900 so it matches all other detail fields. Not assigned
indicator changed from italic warning text to a small pill badge.
GSTIN keeps mono (it is a standardised code) but now has correct text colour
and tracking for legibility.
2026-05-06 00:45:39 +05:30
2ca1861226 feat(approvals): manager can edit all PO fields during review
Replaces the line-items-only editor with a full ManagerEditPoForm that
covers every field: title, vessel, account, vendor, project code, delivery
date, PI/Quotation No+Date, Requisition No+Date, place of delivery, all
structured T&C fields, and line items (with GST rate).

Edit toggle is amber-styled to distinguish manager changes from submitter
input. On save, a complete snapshot of original values is written to the
audit trail (MANAGER_LINE_EDIT action with metadata.original).

managerEditPo server action: validates manager permission, checks
status == MGR_REVIEW, recalculates totalAmount as grand total including
GST, and persists all updated fields.

Approval detail page now fetches vessels/accounts/vendors to populate the
edit form dropdowns.
2026-05-06 00:38:14 +05:30
ac584bce2f fix: totalAmount always stores grand total including GST
manager-line-edit-actions: add gstRate to lineItemSchema; recalculate
newTotal as sum(qty * unitPrice * (1 + gstRate)) and persist gstRate
on recreated line items.

seed: patch existing POs totalAmount to grand total (taxable * 1.18):
  PO-2026-00001: 8450 -> 9971
  PO-2026-00002: 3200 -> 3776
  PO-2026-00003:  950 -> 1121
2026-05-06 00:30:49 +05:30
bde7fc9842 fix(seed): patch existing PO currency from USD to INR on re-seed 2026-05-06 00:26:59 +05:30
67beccc16d fix(seed): upsert POs so re-running seed does not fail on duplicate poNumber; currency USD -> INR 2026-05-06 00:21:37 +05:30
7c31b0e838 feat(storage): shared link-document action and Python package requirements
link-document server action attaches an uploaded file to a PO after creation.
requirements.txt lists Python packages used for standalone PO generation scripts.
2026-05-06 00:17:56 +05:30
e07ce9bd02 test: unit, integration and E2E test suite (110 unit tests passing)
Unit (Vitest + jsdom):
  po-state-machine.test.ts   21 tests — all transitions and helpers
  permissions.test.ts        11 tests — all 7 roles
  utils.test.ts              17 tests — formatCurrency INR, formatDate, status labels
  validations.test.ts        24 tests — createPoSchema, lineItemSchema, TC defaults
  po-status-badge.test.tsx   17 tests — all 10 statuses
  po-line-items-editor.test.tsx 20 tests — add/remove, GST calc, read-only, diff mode

Integration (Vitest + real DB, mocked auth/notifier):
  create-po.test.ts          — S-01 create, S-02 draft, S-03 submit
  approval-actions.test.ts   — M-02 approve, M-03 reject, M-04 edits/vendor-id, S-06/S-07
  payment-actions.test.ts    — A-01 queue, A-02 mark paid with reference

E2E (Playwright):
  auth.spec.ts               — login, role nav, sign out
  submitter-journey.spec.ts  — S-01 to S-08
  manager-approvals.spec.ts  — M-01 to M-04
  accounts-payment.spec.ts   — A-01, A-02
  po-export.spec.ts          — export buttons, endpoint responses, PDF content
2026-05-06 00:16:10 +05:30
5cb8b228b1 feat(export): individual PO export as PDF and XLSX matching Sample_PO format
GET /api/po/[id]/export?format=pdf — HTML print page; company header, PO meta grid,
vendor block with GSTIN, line items table with taxable/GST%/total columns,
totals (taxable subtotal, GST, grand total), numbered T&C list, dual signature block.
GET /api/po/[id]/export?format=xlsx — SheetJS workbook matching Sample_PO.xlsx column layout.
Export PDF / Export XLSX buttons added to PO detail header.
2026-05-06 00:15:57 +05:30
0d053d9bd4 feat(products): admin product catalogue with auto price update on payment
Products: code, name, description; lastPrice and lastVendor are read-only.
On markPaid: for each line item linked to a product, Product.lastPrice and
lastVendorId are updated automatically and logged as PRODUCT_PRICE_UPDATED.
2026-05-06 00:15:47 +05:30
446c226c77 feat(admin): user, vendor, vessel and account management
Users: CRUD with role assignment, bcrypt password, cannot deactivate own account.
Vendors: CRUD with address, GSTIN, contact mobile; isVerified set when vendorId provided.
Vessels: CRUD with IMO number uniqueness check.
Accounts: CRUD with unique account code.
2026-05-06 00:15:41 +05:30