Commit graph

83 commits

Author SHA1 Message Date
91f369da9e fix(my-orders): surface PARTIALLY_CLOSED POs in Open Orders
PARTIALLY_CLOSED was missing from the open-filter so affected POs
disappeared from the submitter''s My Orders view entirely, making it
impossible to confirm remaining deliveries.
Also hardens confirmReceipt() against negative delivery quantities
and extends partial-receipt.spec.ts with US-8c/8d/8e covering the
full PARTIALLY_CLOSED revisit flow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 03:32:26 +05:30
1c5727850a fix(gst): 0% GST rate no longer falls back to 18%
parseFloat('0') is falsy in JS so `|| 0.18` silently discarded the user's
explicit 0% selection. Replaced with an explicit empty-string guard.
Adds e2e spec gst-rate.spec.ts covering all five GST rates (0/5/12/18/28%).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-27 00:00:51 +05:30
5f874cf2a2 pnpm workspace allow build scripts 2026-05-22 17:17:15 +05:30
934979750f test(e2e): harden PO form selectors 2026-05-22 17:15:17 +05:30
32ea27331c fix(po): tighten filters and export data 2026-05-22 17:15:03 +05:30
2c39f0225f feat(vessels): remove IMO number tracking 2026-05-22 17:14:40 +05:30
1ea22df2f7 fix(auth): stabilize login page rendering 2026-05-22 17:14:21 +05:30
d689ef8893 fix(vendors): fix transaction timeout and misleading error on vendor delete
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>
2026-05-18 23:34:35 +05:30
19029a5a77 chore: restructure repo — flatten App/pelagia-portal to App, rename Prototype→Wireframe and Spec→Design
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-18 23:18:58 +05:30
26211e898d test(e2e): add comprehensive Playwright test suite for all recent features
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>
2026-05-17 18:25:42 +05:30
13b8bcd38a feat(mobile): add Home/Dashboard tab to mobile bottom nav
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>
2026-05-17 01:58:32 +05:30
7ae1189042 fix(mobile): add sign-out button to DesktopRequired screen
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>
2026-05-16 21:32:50 +05:30
f60f249c96 feat(mobile): extend mobile experience to Accounts role for payment actions
- 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>
2026-05-16 21:31:57 +05:30
cfb16600d7 feat(mobile): manager approval queue and PO review on small screens
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>
2026-05-16 21:27:43 +05:30
3646af5c64 style(admin): replace text-link button styles with proper bordered buttons
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>
2026-05-16 21:18:49 +05:30
891e854c7c feat(vendors): auto-verify vendor on first successful payment
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>
2026-05-16 21:11:25 +05:30
a5fb7d088c rebrand: Pelagia Portal → PPMS (Pelagia Payment Management System)
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>
2026-05-16 21:00:15 +05:30
8322f33880 fix(export): gate PDF/XLSX on manager-approved status; drop submitter-name fallback
- 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>
2026-05-16 20:50:26 +05:30
340a3dcce0 feat(gst-service): structured logging, request tracing, and per-session captcha refresh
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>
2026-05-16 16:44:22 +05:30
e8041a8230 fix(notifications): role-aware deep-links instead of always /po/[id]
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>
2026-05-16 16:35:14 +05:30
d297fd044f fix(po): add Submit for Approval button on draft PO detail and edit pages
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>
2026-05-16 16:32:44 +05:30
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
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