diff --git a/App/CLAUDE.md b/App/CLAUDE.md index af0739a..6037430 100644 --- a/App/CLAUDE.md +++ b/App/CLAUDE.md @@ -49,17 +49,20 @@ Internal purchase order management system for a maritime company. Full-stack Nex ``` DRAFT → SUBMITTED → MGR_REVIEW → MGR_APPROVED → SENT_FOR_PAYMENT → PAID_DELIVERED → CLOSED - ↓↑ - EDITS_REQUESTED / REJECTED / VENDOR_ID_PENDING + ↓↑ ↕ ↕ + EDITS_REQUESTED / REJECTED PARTIALLY_PAID PARTIALLY_CLOSED + / VENDOR_ID_PENDING ``` -Every status change is validated against the state machine and recorded as a `POAction` row (audit trail). +Partial payments (`PARTIALLY_PAID`) and partial receipts (`PARTIALLY_CLOSED`) loop until the full amount/quantity is settled. Imported POs are created directly in `CLOSED`. Every status change is validated against the state machine and recorded as a `POAction` row (audit trail). ### Role-Based Permissions -`lib/permissions.ts` defines `hasPermission(role, permission)` and `requirePermission(role, permission)`. Roles: `TECHNICAL`, `MANNING`, `ACCOUNTS`, `MANAGER`, `SUPERUSER`, `AUDITOR`, `ADMIN`. +`lib/permissions.ts` defines `hasPermission(role, permission)` and `requirePermission(role, permission)`. Roles: `TECHNICAL`, `MANNING`, `ACCOUNTS`, `MANAGER`, `SUPERUSER`, `AUDITOR`, `ADMIN`. Permissions include (non-exhaustive): `create_po`, `approve_po`, `process_payment`, `confirm_receipt`, `create_vendor`, `manage_vendors`, `manage_products`, `manage_sites`, `manage_vessels_accounts`, `manage_users`. `create_vendor` is held by submitters too; `manage_*` by Manager/Admin. -**Pattern:** Server Actions call `requirePermission()` at the top before any DB write. +**Pattern:** Server Actions call `requirePermission()` (or `hasPermission()`) at the top before any DB write. + +**Auth:** NextAuth v5 with a Microsoft Entra SSO provider **and** a credentials provider. SSO-only users have no `passwordHash` (it is nullable) — the profile page lets them optionally set one, and is reachable by every role. Only approvers (`approve_po`) can upload a signature. ### Key Directories @@ -74,15 +77,46 @@ Every status change is validated against the state machine and recorded as a `PO ### Cost Centre Model -A PO's "cost centre" is either a **Vessel** or a **Site**. `PurchaseOrder` has both `vesselId String?` (nullable) and `siteId String?` — exactly one is set. +A PO's **cost centre is a Vessel** (the `Vessel` model). `PurchaseOrder.vesselId` is **required**. POs no longer reference a Site as a cost centre — that earlier dual Vessel-or-Site design was removed. -**Form encoding:** All PO creation/edit forms use a `costCentreRef` field with values `v:` (vessel) or `s:` (site). Server actions parse this to set the correct FK. +**Form field:** PO create/edit/import forms use a plain `vesselId` select (no more `costCentreRef` encoding). -**Display pattern:** `po.vessel?.name ?? po.site?.name ?? "—"` everywhere a cost centre name is shown. +**Display pattern:** `po.vessel?.name ?? "—"`. -**URL pre-select:** `/po/new?costCentreRef=v:` or `?costCentreRef=s:`. +**URL pre-select:** `/po/new?vesselId=`. -**Terminology:** Admin pages use the real entity names (Vessel Management, Sites). PO-facing pages use "Cost Centre" for the combined concept. Budget heads are labelled "Accounting Code" (not "Account"). +**Terminology:** "Vessel" is surfaced as **"Cost Centre"** everywhere in the UI, including the admin page (`/admin/vessels` → "Cost Centre Management"). `Site` still exists as a separate construct (used for vendor-distance and inventory), but is not a PO cost centre. Budget heads are labelled "Accounting Code" (not "Account"). + +### Accounting Code Hierarchy + +`Account` is a self-referential 3-level tree via `parentId` (`AccountHierarchy` relation): **Top Category (6-digit, e.g. `100000`) → Sub-Category (`100100`) → Leaf Item (`100101`)**. Codes are 6-digit numeric strings. Seed data lives in `prisma/accounting-codes-data.ts`. + +- **Only leaf items** (accounts with no children) are selectable on a PO. +- PO forms group leaf codes by their sub-category in a searchable dropdown (`components/ui/searchable-select.tsx`, a portal-rendered combobox used in the line-items editor and the main accounting-code field). + +### Companies (multi-company invoicing) + +`Company` represents the sister company a PO is billed under (`PurchaseOrder.companyId`, optional). Fields: `name`, `code` (unique short code, e.g. `PMS`), `gstNumber`, `address`, `telephone`, `mobile`, `email`, `invoiceEmail`, `invoiceAddress`. Managed at `/admin/companies`. The selected company's details populate the **exported PO header / invoice block** (falling back to hardcoded Pelagia defaults when no company is linked). + +### PO Numbering (`lib/po-number.ts`) + +Structured format: **`COMPANY/VESSEL/PO_ID/FY`** — e.g. `PMS/HNR1/9000/2024-25`. The financial year is Indian (Apr–Mar) rendered `YYYY-YY`. System-generated `PO_ID` starts at **9000** to avoid clashing with historical numbers. **Imported POs keep their original PO number** verbatim; `parsePoNumber()` extracts the company/vessel/id parts on import. + +### Payments + +When Accounts records a payment, a **compulsory payment date** is captured (`PurchaseOrder.paymentDate`) — the input defaults to today and rejects future dates (validated in `processPaymentSchema` and `markPaid`). There is also an editable **`poDate`** field; the exported PO "Date" shows `poDate ?? approvedAt ?? createdAt` (i.e. the approval date once approved, not creation). + +### Vendors + +`Vendor` carries `isVerified`, `gstin`, `pincode` + `latitude`/`longitude` (geocoded for vendor-distance sorting from a Site), and a `VendorContact[]` list. **Submitters can create vendors** (permission `create_vendor`) but they are created **unverified**; a vendor becomes verified when a PO is closed/paid with it, on import, or when a Manager/Accounts/Admin runs `verifyVendor`. Only `manage_vendors` holders may assign a `vendorId` (the formal verified code). + +### Inventory (feature-flagged) + +Inventory (`ItemInventory`, keyed by `productId` + `siteId`) is **incremented at PO approval** — not on close — for the ordered quantities, when the PO has a `siteId`. The whole inventory surface (site stock, consumption) is gated by `NEXT_PUBLIC_INVENTORY_ENABLED` (see `lib/feature-flags.ts`); the vendor/product catalogue used for PO creation stays available regardless. + +### Import → Closed + +`/po/import` parses a Pelagia-format Excel PO and saves it **directly as `CLOSED`** (historical record, bypasses approval). It auto-detects the company (by header/code), auto-matches the vessel by code, **auto-creates the vendor and any unknown products**, and upserts per-vendor prices. ### GST Calculation diff --git a/App/README.md b/App/README.md index 6baba01..669d6f6 100644 --- a/App/README.md +++ b/App/README.md @@ -133,6 +133,8 @@ FORGEJO_TOKEN= pnpm db:migrate:deploy # runs prisma migrate deploy (safe for production) ``` +> **Always run migrations before the new build serves traffic.** `pnpm build` only runs `prisma generate` (which updates the TypeScript client) — it does **not** apply migrations. Deploying new code whose client expects a column the DB doesn't have yet produces `P2022 … column does not exist` errors at runtime. The release workflow (`.forgejo/workflows/deploy.yml`) runs `migrate deploy` as part of the deploy; for manual deploys, run it (and restart) before/with the swap. + ### 3. Build and start ```bash @@ -172,7 +174,8 @@ and `staging-tunnel.cmd` (Windows tunnel launcher). | `pnpm db:migrate` | Create and run a new migration (dev only) | | `pnpm db:migrate:deploy` | Apply pending migrations without prompting (CI/production) | | `pnpm db:push` | Push schema changes without a migration file (prototyping only) | -| `pnpm db:seed` | Seed sample data | +| `pnpm db:seed` | Seed sample/demo data (dev) | +| `pnpm db:seed:prod` | Seed real production reference data — users, companies, cost centres, sites, and the full accounting-code hierarchy (idempotent) | | `pnpm db:studio` | Open Prisma Studio GUI | | `pnpm db:reset` | Drop and recreate the database, then re-seed (dev only) | @@ -235,12 +238,21 @@ pelagia-portal/ | Role | Description | |---|---| -| Technical | Deck/engine crew — create and submit POs, confirm receipt | +| Technical | Deck/engine crew — create and submit POs, confirm receipt, add (unverified) vendors | | Manning | Crew-management staff — same as Technical | -| Manager | Review, approve, reject, request edits | -| Accounts | Process payment for approved POs | +| Manager | Review, approve, reject, request edits; manage cost centres, items, vendors | +| Accounts | Process payment for approved POs (records payment reference + date); manage vendors | | SuperUser | Combined Technical + Manning + Manager authority | | Auditor | Read-only access to all records and reports | -| Admin | Manage users, vessels, accounts, and vendors | +| Admin | Manage users, companies, accounting codes, cost centres, sites, items, and vendors | -User accounts are provisioned by an Admin; there is no self-registration. +User accounts are provisioned by an Admin (or via Microsoft Entra SSO); there is no self-registration. SSO-only users have no password and may optionally set one from their profile. + +## Domain Concepts + +- **Cost Centre** — a PO is raised against a **Vessel** (surfaced as "Cost Centre" in the UI). Required on every PO. +- **Company** — the sister company a PO is billed under (e.g. PMS, HNR, DEI). Its GST/address details appear on the exported PO. +- **Accounting Code** — a 3-level hierarchy of 6-digit codes (Top Category → Sub-Category → Leaf). Only leaf codes are selectable on a PO. +- **PO Number** — auto-formatted `COMPANY/VESSEL/ID/FY` (e.g. `PMS/HNR1/9000/2024-25`); imported POs keep their original number. +- **Vendors** — submitters can add vendors; they stay *unverified* until a PO closes with them or a Manager/Accounts/Admin verifies them. +- **Import PO** (Manager/SuperUser) — uploads a Pelagia-format Excel PO straight into `CLOSED`, auto-creating the vendor and any new items. diff --git a/CHANGELOG.md b/CHANGELOG.md index cb4b0d2..855b5f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,29 @@ ### Added +- **Companies (multi-company invoicing)** — new `Company` model and `/admin/companies` CRUD. A PO is billed under a selected company (name, short `code`, GST number, address, phone/mobile, contact + invoice email, invoice address). The company's details populate the exported PO header / invoice block. +- **Structured PO numbers** (`lib/po-number.ts`) — `COMPANY/VESSEL/ID/FY` (e.g. `PMS/HNR1/9000/2024-25`); Indian financial year; system-generated IDs start at 9000. Imported POs keep their original number. +- **3-level accounting-code hierarchy** — `Account.parentId` self-relation (Top Category → Sub-Category → Leaf), 6-digit numeric codes seeded from `prisma/accounting-codes-data.ts`. Only leaf codes are PO-selectable, via a searchable, portal-rendered combobox. +- **Compulsory payment date** — `PurchaseOrder.paymentDate` captured when Accounts records a payment; defaults to today, rejects future dates. Backfilled for existing POs from `paidAt` / the first payment action. +- **Editable PO date (`poDate`)** — the exported PO "Date" now shows `poDate ?? approvedAt ?? createdAt` (approval date once approved, not creation). +- **Submitter vendor creation** — `create_vendor` permission lets Technical/Manning add vendors; they are created **unverified** and become verified when a PO closes/pays with them, on import, or via Manager/Accounts/Admin (`verifyVendor`). +- **Import PO → Closed** — `/po/import` saves a parsed Excel PO directly as `CLOSED`, auto-detecting the company, matching the vessel by code, and auto-creating the vendor, products, and per-vendor prices. +- **Inventory feature flag** (`NEXT_PUBLIC_INVENTORY_ENABLED`) — site stock/consumption surfaces are gated; the vendor/item catalogue for PO creation stays available. Inventory is incremented at **PO approval** (not on close). +- **Dashboards** — Accounts gains a "Payments Completed This Month" card. - **Automated issue-to-deploy pipeline** — end-to-end flow from a user-reported bug to a production fix without manual intervention on the developer's part: - **Report Issue button** (`App/components/layout/report-issue-button.tsx`) — any signed-in user can file a bug from the portal header; the server action (`report-issue-actions.ts`) calls the Forgejo API and attaches `portal` + `claude-queue` labels. - **Claude issue watcher** (`automation/claude-issue-watcher.ps1`) — a Windows Scheduled Task (`PelagiaClaudeIssueWatcher`) polls Forgejo every 10 minutes, picks up `claude-queue` issues, and runs Claude Code headlessly to implement and verify a fix. On success the watcher pushes a `claude/issue-N` branch and opens a PR; on failure it posts a comment and labels the issue `claude-failed`. - **Tag-triggered deploy workflow** (`.forgejo/workflows/deploy.yml`) — pushing a `v*` semver tag triggers the `host` Forgejo runner on pms1, which checks out the tag, runs `pnpm install`, builds the app, applies Prisma migrations, and restarts the pm2 process `ppms`. + +### Changed + +- **Cost centre is now a Vessel only.** The earlier Vessel-or-Site cost-centre model was removed: `PurchaseOrder.vesselId` is required, the `costCentreRef` encoding is gone, and `Vessel` no longer links to a `Site`. Vessels are surfaced as **"Cost Centre"** throughout the UI (`/admin/vessels` → "Cost Centre Management"). +- **Closed Purchase Orders** list: submitters see only their own `CLOSED` POs; Managers/SuperUsers see all `CLOSED` POs. +- **Sidebar** reorganised into **Purchasing** and **Administration** sections (role-aware); "Inventory" renamed to "Purchasing". +- **Items**: `/admin/products` is the editable catalogue; `/inventory/items` is read-only; both link to a shared item detail page. +- **Profile** page is reachable by every role (incl. SSO-only / no-password users, with an email fallback lookup); only approvers can upload an approval signature. +- **Manager dashboard** "Approved This Month" now counts by `approvedAt` (no longer undercounts once a PO progresses past `MGR_APPROVED`). + +### Fixed + +- Production `P2022 … column does not exist` after deploy — caused by shipping code whose Prisma client expected a column before `migrate deploy` had run. Migrations must be applied before the new build serves traffic (now documented in the README). diff --git a/Docs/01-design-document.md b/Docs/01-design-document.md index 7de700f..fbb0e09 100644 --- a/Docs/01-design-document.md +++ b/Docs/01-design-document.md @@ -1,5 +1,18 @@ # Pelagia Portal — Design Document +> **Note — this is the original design spec.** The shipped product has evolved; where they +> differ, the code, `App/CLAUDE.md`, `Docs/02-architecture.md`, and the `CHANGELOG.md` are +> authoritative. Key amendments since this document was written: +> - **Cost centre is a Vessel only** (Vessels are labelled "Cost Centre" in the UI). "Account +> Management / cost centres" below refers to budget **Accounting Codes**, now a 3-level +> hierarchy of 6-digit codes — not cost centres. +> - **Companies** were added (a PO is billed under a sister company; details appear on the +> exported PO). +> - **Submitters can create vendors** (created unverified until approved or proven by a closed PO). +> - **Accounts capture a compulsory payment date**; PO numbers are auto-formatted +> `COMPANY/VESSEL/ID/FY`; POs can be **imported** straight to `CLOSED`. +> - Auth supports **Microsoft Entra SSO** (passwordless users); partial payment/receipt states exist. + ## 1. Overview Pelagia Portal is an internal purchase order (PO) management web application for a maritime / vessel-operations company. It digitises the entire PO lifecycle — from a crew member raising a requisition, through manager approval and vendor validation, to accounts payment processing and final receipt confirmation — replacing ad-hoc email chains and spreadsheets with a single, auditable system. diff --git a/Docs/02-architecture.md b/Docs/02-architecture.md index f00147d..4f98bbc 100644 --- a/Docs/02-architecture.md +++ b/Docs/02-architecture.md @@ -127,6 +127,24 @@ pelagia-portal/ ## 4. Data Model +> **Source of truth:** `App/prisma/schema.prisma`. The excerpt below is an illustrative +> overview and may lag the schema. Notable evolutions since the original design: +> - **Cost centre is a Vessel only** — the PO `vesselId` is required; the earlier +> Vessel-or-Site cost-centre design was dropped. `Vessel` no longer links to a `Site`. +> In the UI a Vessel is labelled **"Cost Centre"**. +> - **`Account` is a 3-level hierarchy** (self-relation via `parentId`): Top Category → +> Sub-Category → Leaf item, with 6-digit numeric codes; only leaf codes are PO-selectable. +> - **`Company`** added — the sister company a PO is billed under (`PurchaseOrder.companyId`). +> - **`PurchaseOrder`** gained `companyId`, `siteId?`, `paidAmount`, `paymentDate`, `poDate`, +> and the quotation / requisition / terms-and-conditions fields; `currency` defaults to `INR`. +> - **`Vendor`** gained `gstin`, `address`, `pincode`, `latitude`/`longitude` (geocoded for +> distance) and a `VendorContact[]` list. Submitters can create vendors (created +> *unverified*); a vendor is verified when a PO closes/pays with it, on import, or by a +> Manager/Accounts/Admin. +> - Inventory/catalogue models added: `Site`, `ItemInventory`, `ItemConsumption`, +> `ProductVendorPrice`. Auth: `User.passwordHash` is **nullable** (SSO users) and `User` +> has a `signatureKey`. New statuses `PARTIALLY_PAID` / `PARTIALLY_CLOSED`. + ### 4.1 Entity Relationship (Prisma Schema) ```prisma @@ -151,7 +169,9 @@ enum POStatus { REJECTED MGR_APPROVED SENT_FOR_PAYMENT + PARTIALLY_PAID PAID_DELIVERED + PARTIALLY_CLOSED CLOSED } @@ -187,23 +207,45 @@ model User { notifications Notification[] } +// Cost centre. Surfaced as "Cost Centre" in the UI. model Vessel { id String @id @default(cuid()) name String + code String @unique isActive Boolean @default(true) - siteId String? - site Site? @relation(fields: [siteId], references: [id]) - purchaseOrders PurchaseOrder[] } +// 3-level hierarchy via self-relation: Top Category → Sub-Category → Leaf item. +// 6-digit numeric codes; only leaf accounts (no children) are selectable on a PO. model Account { - id String @id @default(cuid()) - code String @unique + id String @id @default(cuid()) + code String @unique // e.g. 100000 / 100100 / 100101 name String description String? - isActive Boolean @default(true) + isActive Boolean @default(true) + + parentId String? + parent Account? @relation("AccountHierarchy", fields: [parentId], references: [id]) + children Account[] @relation("AccountHierarchy") + + purchaseOrders PurchaseOrder[] +} + +// Sister company a PO is billed under; its details populate the exported PO header. +model Company { + id String @id @default(cuid()) + name String + code String? @unique // short code used in PO numbers, e.g. PMS + gstNumber String? + address String? + telephone String? + mobile String? + email String? + invoiceEmail String? + invoiceAddress String? + isActive Boolean @default(true) purchaseOrders PurchaseOrder[] } @@ -239,15 +281,19 @@ model Product { model PurchaseOrder { id String @id @default(cuid()) - poNumber String @unique @default(cuid()) // formatted in app layer + poNumber String @unique // COMPANY/VESSEL/ID/FY, formatted in lib/po-number.ts title String status POStatus @default(DRAFT) totalAmount Decimal @db.Decimal(12, 2) - currency String @default("USD") + paidAmount Decimal? @db.Decimal(12, 2) // accumulates across partial payments + currency String @default("INR") + poDate DateTime? // editable PO date (export "Date" = poDate ?? approvedAt ?? createdAt) dateRequired DateTime? projectCode String? managerNote String? paymentRef String? + paymentDate DateTime? // compulsory when Accounts records a payment; no future dates + // + piQuotationNo/Date, requisitionNo/Date, placeOfDelivery, tc* (terms & conditions) fields submittedAt DateTime? approvedAt DateTime? paidAt DateTime? @@ -257,10 +303,14 @@ model PurchaseOrder { submitterId String submitter User @relation("Submitter", fields: [submitterId], references: [id]) - vesselId String + vesselId String // cost centre (required) vessel Vessel @relation(fields: [vesselId], references: [id]) + siteId String? // optional delivery site (drives inventory) + site Site? @relation(fields: [siteId], references: [id]) accountId String account Account @relation(fields: [accountId], references: [id]) + companyId String? + company Company? @relation(fields: [companyId], references: [id]) vendorId String? vendor Vendor? @relation(fields: [vendorId], references: [id]) diff --git a/Docs/DESIGN.md b/Docs/DESIGN.md index 9261099..4a3d0ac 100644 --- a/Docs/DESIGN.md +++ b/Docs/DESIGN.md @@ -3,6 +3,14 @@ Internal purchase-order management system for a maritime company. This document describes every feature, page, workflow, and user story to guide UI/UX design. +> **Note — original UI/UX spec; the shipped product has evolved.** For current behaviour see +> `App/CLAUDE.md`, `Docs/02-architecture.md`, and `CHANGELOG.md`. Notably: a PO's cost centre +> is a **Vessel** (labelled "Cost Centre"); a PO is billed under a **Company** (shown on the +> exported PO); **Accounting Codes** are a 3-level 6-digit hierarchy; PO numbers are +> `COMPANY/VESSEL/ID/FY`; submitters can add (unverified) vendors; Accounts capture a +> compulsory **payment date**; POs can be **imported** directly to `CLOSED`; and login +> supports **Microsoft Entra SSO**. + --- ## 1. Purpose