# 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) | | Email | 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: ```bash 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 ```bash pnpm install ``` ### 2. Configure environment Copy the example file and fill in the two required values: ```bash cp .env.example .env.local ``` Minimum `.env.local` for development: ```env NEXTAUTH_SECRET= 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: ```bash pnpm db:migrate # runs prisma migrate dev pnpm db:seed # seeds vessels, accounts, vendors, and demo users ``` To inspect the database with a GUI: ```bash pnpm db:studio # opens Prisma Studio at http://localhost:5555 ``` ### 4. Start the dev server ```bash pnpm dev ``` The app will be available at [http://localhost:3000](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: ```env # Auth NEXTAUTH_SECRET= NEXTAUTH_URL=https://your-domain.com # Database DATABASE_URL=postgresql://:@:/ # Cloudflare R2 R2_ACCOUNT_ID= R2_ACCESS_KEY_ID= R2_SECRET_ACCESS_KEY= R2_BUCKET_NAME=pelagia-portal R2_PUBLIC_URL=https://..r2.cloudflarestorage.com # Email RESEND_API_KEY=re_ 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= # 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 ```bash 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 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`](../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-queue` or `interactive`), 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`, pm2 `ppms-staging` on port 3200, SSH-tunnel only) runs the latest `master` against a daily **prod-mirror test DB** (`pelagia_test`) for smoke testing before tagging a release. Operational scripts live under [`../automation/`](../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 ```bash 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 ```bash 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.