Reflects this iteration's domain/feature changes across the docs set: - Cost centre = Vessel only (labelled 'Cost Centre'); costCentreRef/Site removed - Companies (multi-company invoicing) on POs and exports - 3-level 6-digit accounting-code hierarchy; leaf-only PO selection - Structured PO numbers COMPANY/VESSEL/ID/FY (ids from 9000) - Compulsory payment date; editable poDate; export date = approval date - Submitter vendor creation (unverified until proven); verifyVendor - Import PO -> CLOSED with auto vendor/product creation - Inventory flag; inventory added at approval; partial pay/receipt states - Microsoft Entra SSO (nullable passwordHash); profile reachable by all roles - README: roles, domain concepts, db:seed:prod, migrate-before-serve callout - CHANGELOG: Added/Changed/Fixed for the above Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9.6 KiB
Pelagia Portal
An internal purchase order management system for a maritime vessel-operations company. Digitises the full PO lifecycle — from crew requisition through manager approval, vendor validation, accounts payment, and receipt confirmation — replacing ad-hoc email chains and spreadsheets with a single auditable workflow.
Tech Stack
| Layer | Technology |
|---|---|
| Framework | Next.js 15 (App Router) |
| Language | TypeScript 5 (strict) |
| Database | PostgreSQL 16 via Prisma 5 |
| Auth | NextAuth.js v5 (credentials) |
| Styling | Tailwind CSS v4 + shadcn/ui |
| File Storage | Cloudflare R2 (production) / local filesystem (development) |
| Resend (production) / console log (development) |
Prerequisites
| Tool | Required Version |
|---|---|
| Node.js | >= 20.11.0 LTS |
| pnpm | >= 9.0.0 |
| PostgreSQL | >= 16 (local or Docker) |
Install pnpm if you don't have it:
npm install -g pnpm
Development Setup
In development mode the app requires only a database and auth secret — Cloudflare R2 and Resend are not needed. File uploads are saved to .dev-uploads/ on your local machine, and emails are printed to the terminal instead of being sent.
1. Install dependencies
pnpm install
2. Configure environment
Copy the example file and fill in the two required values:
cp .env.example .env.local
Minimum .env.local for development:
NEXTAUTH_SECRET=<generate with: openssl rand -base64 32>
NEXTAUTH_URL=http://localhost:3000
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/pelagia_portal"
The R2 and Resend variables are not needed in development and can be left as placeholders.
3. Set up the database
Create the database (if it doesn't exist yet), run migrations, and seed sample data:
pnpm db:migrate # runs prisma migrate dev
pnpm db:seed # seeds vessels, accounts, vendors, and demo users
To inspect the database with a GUI:
pnpm db:studio # opens Prisma Studio at http://localhost:5555
4. Start the dev server
pnpm dev
The app will be available at http://localhost:3000.
Email behaviour in dev: all notification emails are logged to the terminal in place of actual delivery. Look for lines starting with 📧 [DEV EMAIL].
File upload behaviour in dev: uploaded files are written to .dev-uploads/ at the project root. This directory is git-ignored.
Serving in Production
Production requires all environment variables to be set, including Cloudflare R2 credentials and a Resend API key.
1. Configure environment
Set the following variables in your hosting platform (Vercel, etc.) or in .env.local for a self-hosted deploy:
# Auth
NEXTAUTH_SECRET=<strong random secret>
NEXTAUTH_URL=https://your-domain.com
# Database
DATABASE_URL=postgresql://<user>:<password>@<host>:<port>/<db>
# Cloudflare R2
R2_ACCOUNT_ID=<your cloudflare account id>
R2_ACCESS_KEY_ID=<r2 access key>
R2_SECRET_ACCESS_KEY=<r2 secret key>
R2_BUCKET_NAME=pelagia-portal
R2_PUBLIC_URL=https://<bucket>.<account>.r2.cloudflarestorage.com
# Email
RESEND_API_KEY=re_<your key>
EMAIL_FROM=noreply@yourdomain.com
EMAIL_FROM_NAME="Pelagia Portal"
# Report Issue button -> files a Forgejo issue (optional; token needs write:issue)
FORGEJO_URL=https://git.example.com
FORGEJO_REPO=owner/repo
FORGEJO_TOKEN=<forgejo access token>
# Non-prod banner (leave UNSET in production). When set, a fixed
# "INTERNAL DEV / STAGING - NOT PRODUCTION" banner is shown.
# NEXT_PUBLIC_ENV_LABEL="INTERNAL DEV / STAGING - NOT PRODUCTION"
2. Run database migrations
pnpm db:migrate:deploy # runs prisma migrate deploy (safe for production)
Always run migrations before the new build serves traffic.
pnpm buildonly runsprisma 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 producesP2022 … column does not existerrors at runtime. The release workflow (.forgejo/workflows/deploy.yml) runsmigrate deployas part of the deploy; for manual deploys, run it (and restart) before/with the swap.
3. Build and start
pnpm build
pnpm start
The app listens on port 3000 by default. Point your reverse proxy (nginx, Caddy, etc.) or hosting platform to that port.
Operations & Automation
This repo carries its own self-hosted issue-to-deploy pipeline (Forgejo + Claude Code
on the pms1 server). The full design and runbook live in
../automation/README.md. In short:
- Report Issue button (portal header) files a Forgejo issue tagged
portal. - A watcher triages each issue (Claude posts a requirements breakdown and routes it
to
claude-queueorinteractive), then for queued issues implements a fix and opens a PR. - Merging a PR and pushing a release tag (
vX.Y.Z) triggers a Forgejo Actions runner that deploys to production. - A staging instance (
automation/staging-up.sh, pm2ppms-stagingon port 3200, SSH-tunnel only) runs the latestmasteragainst a daily prod-mirror test DB (pelagia_test) for smoke testing before tagging a release.
Operational scripts live under ../automation/: claude-issue-watcher.sh
(watcher), refresh-test-db.sh (nightly test-DB refresh), staging-up.sh (staging),
and staging-tunnel.cmd (Windows tunnel launcher).
Database Management
| Command | Purpose |
|---|---|
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/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) |
Testing
pnpm test # unit + integration tests (Vitest)
pnpm test:watch # watch mode
pnpm test:e2e # end-to-end tests (Playwright)
pnpm test:e2e:ui # Playwright with interactive UI
Other Scripts
pnpm lint # ESLint
pnpm type-check # tsc --noEmit
pnpm email:preview # live-preview email templates at http://localhost:3001
Project Structure
pelagia-portal/
├── app/ # Next.js App Router pages and API routes
│ ├── (auth)/ # Login
│ ├── (portal)/ # Authenticated shell (sidebar + header)
│ │ ├── dashboard/
│ │ ├── po/ # PO creation, detail, edit
│ │ ├── approvals/ # Manager approval queue
│ │ ├── payments/ # Accounts payment queue
│ │ ├── history/ # Audit trail
│ │ └── admin/ # User, vessel, account, vendor management
│ └── api/
│ ├── auth/ # NextAuth endpoints
│ ├── files/sign/ # Generate presigned upload URL (production)
│ ├── files/dev/ # Local file upload/download handler (dev only)
│ └── reports/export/ # CSV / PDF export
├── components/ # Shared UI components (shadcn/ui + custom)
├── lib/ # Business logic
│ ├── po-state-machine.ts # All PO state transitions enforced here
│ ├── permissions.ts # Role → allowed-action map
│ ├── notifier.ts # Email dispatch (Resend in prod, console in dev)
│ ├── storage.ts # File storage (R2 in prod, local in dev)
│ └── validations/ # Zod schemas
├── emails/ # React Email templates
├── prisma/ # Schema and migrations
└── tests/ # Vitest unit/integration + Playwright E2E
Roles
| Role | Description |
|---|---|
| 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; 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, companies, accounting codes, cost centres, sites, items, and vendors |
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.