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.
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.
- 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>
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>
- 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>
- 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>
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>
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>
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>
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>
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>
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>
- 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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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.
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.
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.
link-document server action attaches an uploaded file to a PO after creation.
requirements.txt lists Python packages used for standalone PO generation scripts.
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.
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.
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.
Full audit list of all POs (latest 200). Filters: date range, vessel, status.
CSV export respects active filters. PDF export renders print-optimised HTML table.
My Orders: all submitter POs grouped as open/past with live status, manager note inline.
Receipt: upload receipt file, optional notes; confirms delivery and closes PO to CLOSED.
Step 1 (process): MGR_APPROVED → SENT_FOR_PAYMENT, notifies submitter and managers.
Step 2 (mark paid): SENT_FOR_PAYMENT → PAID_DELIVERED, stores paymentRef.
On mark paid: auto-updates Product.lastPrice and lastVendorId for any line items
linked to a product code; logs PRODUCT_PRICE_UPDATED action.
Approval queue: paginated list with search (PO number, vessel, submitter, date range).
Decision actions: approve, approve with note, reject (with reason), request edits,
request vendor ID.
Manager line edit: amend line items during review; original snapshot saved to audit
trail; diff shown with amber strikethrough on PO detail.
Detail: order info, vendor (address/GSTIN/contact), line items with GST breakdown,
structured T&C, attachments, activity trail, Export PDF/XLSX buttons.
Vendor ID form: inline on PO detail when status is VENDOR_ID_PENDING.
Edit: pre-populated form for DRAFT and EDITS_REQUESTED; resubmit transitions to MGR_REVIEW.
Form sections: order info, quotation reference (PI No/Date), requisition (No/Date),
place of delivery, line items (UoM dropdown, size, GST% per item), vendor, T&C, attachments.
Line items editor: add/remove rows, GST dropdown (0/5/12/18/28%, default 18%),
live taxable/GST/grand-total breakdown.
T&C: fixed line 1, individual inputs for Delivery, Dispatch, Inspection,
Transit Insurance, Payment Terms, Others.
Save as draft or submit directly for approval (→ MGR_REVIEW).
Submitter: open PO count, recent orders table, New PO CTA.
Manager: approvals count, approved PO listing, spend by vessel and month bar charts (Recharts).
Accounts: payment queue total value, ready-for-payment count.
Admin/Auditor: total PO count card.