Increase the Prisma interactive transaction timeout from the default 5s
to 30s so that the four sequential nullification + delete queries complete
reliably on a seeded database (P2028 timeout was the root cause).
Wrap the transaction in a try/catch so that if a timeout does still occur
the user sees "Delete timed out — please try again." instead of an
unhandled 500 that previously manifested as the misleading "referenced in
submitted or active purchase orders" error message.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
test-report-2026-05-17.md — Run summary for the 2026-05-17 session:
64 passed, 3 flaky, 2 skipped, 61 failed (all in pre-existing specs
with legacy selectors). Includes per-spec results, root causes for
every failure category, and recommended fixes.
e2e-test-framework.md — Developer reference covering stack, directory
layout, playwright.config.ts rationale (workers: 2, why bcrypt
floods the server), shared helpers, selector conventions (PO form
has no htmlFor bindings), mobile viewport pattern, and future
improvements including auth state sharing.
e2e-test-plan.md — Feature coverage matrix mapping all 21 user story
groups to their spec files and roles, individual test case tables,
regression trigger checklist by code area, gap analysis, and
planned CI configuration.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Covers 21 user story groups extracted from the last 15+ feature commits:
- rebrand.spec.ts — PPMS branding on login page, sidebar, and tab title
- dashboard/po-status-badges.js — color-coded status badges for submitter & manager
- po-submit-button.spec.ts — Submit for Approval button visibility on DRAFT POs
- notification-bell.spec.ts — in-app bell icon, unread badge, and panel open
- export-gate.spec.ts — export buttons gated on MGR_APPROVED+ status; 403 on pre-approval
- payment-history.spec.ts — /payments/history accessible to ACCOUNTS; redirects others
- partial-receipt.spec.ts — per-item delivery tracking UI on paid POs
- vendor-auto-verify.spec.ts — vendor verification status visible in admin
- admin-bordered-buttons.spec.ts — Edit/Deactivate/Delete have border classes on admin pages
- profile.spec.ts — profile page loads for all roles; signature section for MANAGER/SUPERUSER
- inventory/items-tags.spec.ts — Cheapest/Closest tags and auto-sort by distance
- inventory/cart-icon.spec.ts — cart header icon with badge; item/vendor detail pages
- mobile/desktop-required.spec.ts — Desktop Required overlay for non-mobile roles + sign-out
- mobile/manager-approvals.spec.ts — mobile card layout; edit form hidden; action buttons visible
- mobile/accounts-payments.spec.ts — ACCOUNTS payments queue and buttons on mobile
- mobile/bottom-nav.spec.ts — Home/Approvals/Profile tabs for MANAGER; Home/Payments/Profile for ACCOUNTS
- approvals-edit-highlight.spec.ts — diff indicators on resubmitted POs
Also adds shared helpers/login.ts with USERS constants, login(), createDraftPo(),
and submitPo() — all using name-attribute selectors since PO form labels have no
htmlFor binding. Playwright config updated: workers capped at 2 locally (was
unlimited) to prevent auth concurrency failures, and retries set to 1 locally.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prepend a Home tab (linking to /dashboard) to the bottom navigation bar
for both Manager/SuperUser and Accounts roles, giving one-tap access to
the dashboard from any mobile screen.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Users on mobile who see the "Desktop Required" wall had no way to log out.
Added a Sign out button using next-auth signOut (requires "use client").
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ACCOUNTS to MOBILE_ROLES so the desktop-required wall no longer
blocks them on small screens.
- MobileBottomNav is now role-aware: ACCOUNTS gets Payments + Profile tabs;
MANAGER/SUPERUSER keep Approvals + Profile. Role prop threaded from
layout → MobileBottomNav.
- PaymentActions (both MGR_APPROVED and SENT_FOR_PAYMENT states) stacks
vertically on small screens — input takes full width, button below it —
then reverts to the horizontal inline layout at sm+ breakpoint.
- Payments page card bottom row (status badge + action) stacks on mobile
(flex-col sm:flex-row) so the reference input isn't squashed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a responsive mobile experience scoped to MANAGER and SUPERUSER roles:
Layout:
- Sidebar hidden on small screens (md: breakpoint)
- New MobileHeader (logo + notification bell + sign out) visible on mobile
- New MobileBottomNav (Approvals + Profile tabs) pinned to bottom on mobile
- New DesktopRequired overlay shown to all other roles on small screens —
a fixed full-screen message directing them to use a desktop browser
Approvals queue:
- Desktop: existing table layout (hidden md:block)
- Mobile: tap-to-review card stack (md:hidden) — shows PO number, title,
submitter, cost centre, amount, and a full-width Review button
PO approval detail:
- ManagerEditPoForm (direct field editing) hidden on mobile; still available
on desktop (direct edits not required per spec)
- ApprovalActions buttons stack full-width on mobile, row on sm+
- Paddings reduced on small screens throughout
PO detail component:
- Order Details and Vendor grids switch from grid-cols-2 → grid-cols-1
on small screens (sm:grid-cols-2 restores two columns at 640px+)
- Section padding reduced on mobile (p-3 md:p-6, p-4 md:p-6)
- Line items table already had overflow-x-auto — no change needed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Edit, Deactivate/Activate, and Delete actions in all admin table rows
were styled as plain text links (coloured text, no border or background).
Replaced with small pill-shaped bordered buttons that have a clear visual
affordance as interactive controls:
- Edit → blue tinted border/bg (primary-50 / primary-200)
- Deactivate → red tinted border/bg (danger-50 / danger-200)
- Activate → green tinted border/bg (success-50 / success-200)
- Delete → white bg, red border; confirm state = solid red
- Cancel → white bg, neutral border
Applies to: accounts, cost centres (vessels), users, vendors, products,
and the shared ConfirmDeleteButton component.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When accounts confirms a payment (SENT_FOR_PAYMENT → PAID_DELIVERED),
set Vendor.isVerified = true for the PO's vendor. The field already
exists in the schema (default false); this closes the loop so vendors
who have transacted at least once are marked verified automatically
without manual admin intervention.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Rename the application to PPMS across all user-facing surfaces:
- Browser title / metadata
- Login page (with tagline: "PMS — it runs the ship.")
- Sidebar logo text
- Email header, footer, and body copy in all transactional emails
- PDF/XLSX workbook creator field
- Reports export page title and heading
- Notifier FROM display name and default email domain
Legal company name ("Pelagia Marine Services Pvt. Ltd.") on PO documents
is unchanged — that is the issuing entity, not the application brand.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Export API returns 403 for any PO not yet approved (DRAFT, SUBMITTED,
MGR_REVIEW, EDITS_REQUESTED, VENDOR_ID_PENDING, REJECTED) — only
MGR_APPROVED, SENT_FOR_PAYMENT, PAID_DELIVERED, PARTIALLY_CLOSED and
CLOSED are exportable.
- The submitter's name is no longer used as a signatory fallback; since
export is blocked until after manager approval an approver always exists.
- Export PDF / Export XLSX buttons are hidden in po-detail for pre-approval
statuses, so users never encounter the 403 through normal UI flows.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Logging (GstService):
- JSON-structured log lines: { ts, level, msg, ...ctx } — one per line,
machine-parseable by any log aggregator (datadog, loki, etc.)
- LOG_LEVEL env var (DEBUG|INFO|WARN|ERROR, default INFO) — set DEBUG to
see every captcha fetch, raw GST response body, and page console event
- WARN and ERROR lines go to stderr; INFO/DEBUG go to stdout so process
supervisors can separate them
- Every log line carries relevant context: reqId, sessionId, gstin, ms, etc.
- errCtx() helper extracts errName, errMsg, and first 6 stack frames from
any thrown value — no more bare String(e)
- elapsed() helper records wall-clock ms for every expensive step:
browser launch, page navigation, captcha fetch, GST API call
- Request/response middleware: every HTTP request logs method, path,
reqId, status, and duration; status >= 500 logs at ERROR, >= 400 at WARN
- Playwright page listeners: console errors/warnings, pageerror,
requestfailed, and HTTP 4xx/5xx on GST portal endpoints
- process.on(uncaughtException) and process.on(unhandledRejection) so
unexpected crashes surface in logs instead of silently dying
- Browser "disconnected" event logged; _browser reset so next request
auto-relaunches without manual restart
- SESSION_TTL_MS configurable via env (default 3 min)
- closeSession() logs the reason (success / errorCode / exception / etc.)
- GET /health now returns browserConnected, per-session captchaCount,
expiresInMs, and lastUsedMsAgo for operational visibility
Multiple captchas per session:
- Session now holds captchas: CaptchaEntry[] (ordered oldest→newest)
so every image fetched in a session is kept for traceability
- GET /captcha/:sessionId — new endpoint that calls /services/captcha
again within the SAME browser context (no page reload, ~200ms vs ~5s)
and appends a new CaptchaEntry; resets TTL; returns totalCaptchas
- POST /search on SWEB_9034 (wrong captcha) no longer closes the session —
returns { canRefresh: true, sessionId } so the caller can hit
GET /captcha/:sessionId for a fresh image and retry immediately
- All other error paths (SWEB_9000, network error, no data) still close
the session as before
Next.js proxy (app/api/gst/captcha/route.ts):
- GET /api/gst/captcha?refresh=<sessionId> proxies to the new
GET /captcha/:sessionId endpoint on GstService
- Plain GET /api/gst/captcha still creates a new session as before
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Notification links now route each recipient to the page where they
need to take action, not just the PO detail:
Manager / SuperUser:
PO_SUBMITTED, VENDOR_ID_PROVIDED → /approvals/[id] (approval queue)
Accounts:
PO_APPROVED / PO_APPROVED_WITH_NOTE → /payments (payment queue)
Submitter:
EDITS_REQUESTED → /po/[id]/edit (open the edit form directly)
PAYMENT_SENT → /po/[id]/receipt (open the receipt confirmation)
All other events / recipients → /po/[id]
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A draft PO had no way to be submitted without creating a brand new PO.
Two surfaces are now fixed:
Edit page (/po/[id]/edit):
- updatePo action now accepts intent "submit" (alongside "save" and
"resubmit"), which transitions a DRAFT PO to MGR_REVIEW, creates a
SUBMITTED action, and fires notifications — identical behaviour to
the new-PO submit flow
- EditPoForm shows a primary "Submit for Approval" button when the PO
is in DRAFT status; "Resubmit for Approval" remains for EDITS_REQUESTED
PO detail page (/po/[id]):
- New submitDraftPo server action in po/[id]/actions.ts handles the
same DRAFT → MGR_REVIEW transition without requiring the full edit form
- New SubmitDraftButton client component renders next to the Edit link
in the detail header for DRAFT POs owned by the current user
- Button order: Edit · Submit for Approval · Discard
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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>
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>
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>