# WFDASH — Business CRM A modern business CRM (customers, companies, suppliers, quotes, orders, driver collection pool, delivery notes, repairs) built as a Next.js 16 / PocketBase re-platform of the legacy Django app. - **Frontend**: Next.js 16 (App Router) + React 19 + TypeScript - **UI**: shadcn/ui (base-nova style), Tailwind CSS 4, Lucide icons, Sonner toasts - **Maps**: Leaflet + react-leaflet (clustering, marker edit, fullscreen, dark tiles) - **Backend**: PocketBase (database, auth, file storage, realtime) - **MCP**: in-app Model Context Protocol endpoint for n8n / external agents > **Note**: This is **not** the Next.js from older training data. APIs, conventions, and file structure differ. Read the relevant guide in `node_modules/next/dist/docs/` before writing code, and watch for deprecation notices. --- ## Prerequisites - **Node.js >= 20.9.0** (enforced by `package.json` `engines`) - **PocketBase** binary (the setup script connects to `http://127.0.0.1:8090`)but in my .env.local we have added the cloud url that its connecting to now at the moment --- ## Quick Start ```bash # 1. Install deps npm install # 2. Start PocketBase (in another terminal) ./pocketbase serve # 3. Configure env (see "Environment Variables" below) cp .env.local.example .env.local # or create manually # 4. Create collections in PocketBase npm run setup # base collections (customers, companies, suppliers, quotes, orders, …) npm run pb:migrate:orders-v2 # new order_items_v2 + collection_pool collections npm run pb:migrate:order-status-in-stock # switch orders.status to the operational enum + add order_items.in_stock # 5. Run the app npm run dev ``` App runs on http://localhost:3000. --- ## Environment Variables Create `.env.local` (gitignored) with: ```bash # Public — exposed to the browser NEXT_PUBLIC_POCKETBASE_URL=http://127.0.0.1:8090 # Server-only — admin used by setup / migration scripts and agent API PB_ADMIN_EMAIL=admin@example.com PB_ADMIN_PASSWORD=your_password # Agent / MCP API auth (see "API & MCP" below) AGENT_API_KEY=replace_with_secure_token # Switch which collection the order-items service writes to # Set to `order_items_v2` after running the migration ORDER_ITEMS_COLLECTION=order_items ``` > `scripts/setup-pocketbase.ts` currently has the admin email/password hard-coded — edit it before running, or set the matching env vars there. --- ## NPM Scripts | Script | Purpose | | --- | --- | | `npm run dev` | Start Next.js dev server (webpack) | | `npm run build` | Production build | | `npm run start` | Run the production build | | `npm run lint` | ESLint over the project | | `npm run setup` | Create base PocketBase collections from `scripts/setup-pocketbase.ts` | | `npm run pb:migrate:orders-v2` | Create `order_items_v2` + `collection_pool` collections | | `npm test` | Vitest in watch mode | | `npm run test:run` | Vitest single run (CI-friendly) | | `npm run test:coverage` | Vitest with V8 coverage | | `npm run e2e` | Playwright E2E suite (boots `next dev` on port 3100) | | `npm run e2e:ui` | Playwright UI mode | | `npm run e2e:install` | One-time: install Playwright browsers + OS deps | See **Testing** below. --- ## Features / Routes All routes are file-based under `src/app/`. ### Public (auth) - `/login` — sign in ### Protected (`(app)`) - `/dashboard` — overview stats + recent activity - `/customers`, `/customers/[id]` — CRUD, search, pagination, CSV export - `/companies`, `/companies/[id]` — CRUD with detail - `/suppliers` — CRUD with tag support - `/supplier-map` — Leaflet map: clustering, marker editing, fullscreen, dark tiles - `/quotes` — list with claim/process workflow - `/quotes/[id]` — detail - `/quotes/[id]/process` — process view - `/quotes/approvals` — approval queue - `/orders`, `/orders/[id]` — list + detail, PO generation by supplier - `/collection-pool` — driver work queue (pool of items to collect/deliver) - `/delivery-notes` — delivery note management - `/repairs` — repair requests ### Cross-cutting - Light/dark theme switcher - Collapsible sidebar, mobile card views - Service worker for offline (registered client-side) --- ## API & MCP All agent endpoints require `Authorization: Bearer `. | Method | Path | Notes | | --- | --- | --- | | `GET` | `/api/v1/health` | Liveness probe (no auth) | | `GET` | `/api/v1/orders?page=1&perPage=50&status=new` | List orders (expands `company,customer,quote`) | | `POST` | `/api/v1/orders/from-quote` | Body: `{ "quote_id": "..." }` — creates an order + copies `quote_items` into the configured `order_items` collection; sets `item_status=pending`, `drv_status=unassigned` | | `GET` | `/api/v1/collection-pool?page=1&perPage=50&status=queued` | List pool records | | `PATCH` | `/api/v1/order-items/[id]/status` | Body: `{ item_status, drv_status, notes, problem_notes }` (also accepts `driver_status` for backwards compat) | | `POST` | `/api/mcp` | JSON-RPC style. Methods: `tools/list`, `tools/call` | ### MCP tools exposed - `orders.list` - `orders.create_from_quote` - `collection_pool.list` - `order_items.update_status` See [docs/api-mcp-layer.md](docs/api-mcp-layer.md) for full payload details. --- ## PocketBase Collections Created by the setup / migration scripts and tracked in [docs/pocketbase/](docs/pocketbase/): - `customers` — customer database (incl. `alternative_email`, `buyer_email`, `coordinates`) - `companies` — company database - `suppliers` — supplier network (tags, coordinates, closing time) - `quotes`, `quote_items` — quotes with line items and status workflow - `orders`, `purchase_orders` — orders + per-supplier POs - `order_items` — legacy order items - `order_items_v2` — replacement schema (new `item_status` + `drv_status` model) - `collection_pool` — driver work queue - `delivery_notes`, `delivery_note_items` — delivery records - `repairs` — repair requests - `stock_items` — minimal stock tracking (folded into `order_items` for now) Workflow rules and parity targets with the old Django app live in: - [docs/core-recheck-matrix.md](docs/core-recheck-matrix.md) — Quotes / Orders / Company / Customer recheck matrix - [docs/django-nextjs-parity-backlog.md](docs/django-nextjs-parity-backlog.md) — migration status per module - [docs/order-workflow-rules.md](docs/order-workflow-rules.md) — status transitions - [docs/order-items-schema-standard.md](docs/order-items-schema-standard.md) — the v2 schema - [docs/orders-first-rollout.md](docs/orders-first-rollout.md) — orders-first rollout - [docs/migration-audit.md](docs/migration-audit.md) — field-by-field audit - [docs/api-mcp-layer.md](docs/api-mcp-layer.md) — API + MCP reference --- ## Project Structure ``` src/ ├── app/ │ ├── (auth)/login/ # Login page │ ├── (app)/ # Protected app pages │ │ ├── dashboard/ │ │ ├── customers/ (+[id]) │ │ ├── companies/ (+[id]) │ │ ├── suppliers/ │ │ ├── supplier-map/ │ │ ├── quotes/ # + [id], [id]/process, approvals │ │ ├── orders/ (+[id]) │ │ ├── collection-pool/ │ │ ├── delivery-notes/ │ │ └── repairs/ │ ├── api/ │ │ ├── mcp/route.ts # MCP endpoint │ │ └── v1/ # Agent REST endpoints │ │ ├── health/ │ │ ├── orders/ # + from-quote │ │ ├── order-items/[id]/status/ │ │ └── collection-pool/ │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components/ │ ├── ui/ # shadcn/ui (base-nova) │ ├── sidebar.tsx │ ├── auth-provider.tsx │ ├── theme-provider.tsx │ ├── data-table.tsx │ ├── line-items-table.tsx │ ├── customer-form.tsx │ ├── company-form.tsx │ ├── supplier-form.tsx │ ├── quote-form.tsx │ ├── order-form.tsx │ ├── delete-dialog.tsx │ ├── supplier-map.tsx │ └── service-worker-registration.tsx └── lib/ ├── pocketbase.ts # Client (singleton) + server instances ├── pb-helpers.ts ├── types.ts # TypeScript types for all collections ├── utils.ts ├── quote-status.ts └── server/ ├── agent-auth.ts ├── pocketbase-admin.ts ├── orders-service.ts └── order-item-status.ts scripts/ ├── setup-pocketbase.ts # base collections ├── import-data.ts └── migrations/ └── create-order-items-v2-and-collection-pool.ts e2e/ # Playwright E2E tests (see Testing) docs/ # parity matrix, rollout plans, MCP, schema standards vitest.config.ts # Vitest config (jsdom, native tsconfig paths) playwright.config.ts # Playwright config (boots next dev on :3100) Dockerfile # multi-stage Node 20 alpine build nixpacks.toml # sets Nixpacks Node version to 20 ``` --- ## Testing The project ships with two test suites, both wired into npm scripts. ### Vitest (unit + API route tests) - Framework: [Vitest](https://vitest.dev/) + jsdom + Testing Library - Config: [vitest.config.ts](vitest.config.ts) — path aliases match `tsconfig.json` - Test files live next to the code they cover: `src/**/*.{test,spec}.ts(x)` What gets covered: | File | What it tests | | --- | --- | | `src/lib/utils.test.ts` | `cn()` (clsx + tailwind-merge) | | `src/lib/quote-status.test.ts` | Status class lookup + format helper | | `src/lib/server/order-item-status.test.ts` | `normalizeOrderItemStatusInput` (every item/drv status, the legacy `driver_status` alias, invalid inputs) | | `src/lib/server/agent-auth.test.ts` | `assertAgentAuth` (bearer token validation, missing env, status code tagging) | | `src/lib/server/orders-service.test.ts` | `createOrderFromQuote` (idempotency, no-items error, payload shape, sequence increment, env-driven collection switch) and `listCollectionPool` (filter + pagination forwarding). PocketBase is mocked. | | `src/lib/server/pocketbase-admin.test.ts` | `getAdminPocketBase` env validation + auth call | | `src/app/api/v1/health/route.test.ts` | Health endpoint shape | | `src/app/api/v1/orders/route.test.ts` | Auth, status filter, pagination, error mapping | | `src/app/api/v1/orders/from-quote/route.test.ts` | Auth, 400 on missing `quote_id`, 201 vs 200 (idempotent), upstream error | | `src/app/api/v1/order-items/[id]/status/route.test.ts` | Auth, empty/invalid input, normalisation integration, env-driven collection | | `src/app/api/v1/collection-pool/route.test.ts` | Auth, param forwarding, error mapping | | `src/app/api/mcp/route.test.ts` | `tools/list`, every `tools/call` tool, missing-id guard, unknown-tool error, method-not-found | Run: ```bash npm run test:run # one-shot, CI-friendly npm test # watch mode npm run test:coverage # with v8 coverage ``` > The MCP `tools/call` auth path currently routes auth failures through the JSON-RPC catch (returns 500). The dedicated `/api/v1/*` endpoints properly return 401. Both are exercised in the tests. ### Playwright (E2E) - Framework: [Playwright](https://playwright.dev/) - Config: [playwright.config.ts](playwright.config.ts) — boots `next dev` on port **3100** by default (override with `E2E_BASE_URL`). - Tests live in `e2e/*.spec.ts` What's covered: | File | Specs | | --- | --- | | `e2e/health.spec.ts` | `GET /api/v1/health` returns the right JSON shape — **runs with no infra** | | `e2e/agent-api.spec.ts` | Agent auth (401 without bearer, 200 with valid token) — skips unless `E2E_AGENT_API_KEY` is set | | `e2e/auth.spec.ts` | Login form: happy path + wrong-password — skips unless `E2E_TEST_EMAIL` + `E2E_TEST_PASSWORD` are set | The skip behaviour is intentional: with no env vars, the suite reports a clean `1 passed, N skipped` so you can run it on a fresh checkout. Once you stand up PocketBase + seed a user, set: ```bash export E2E_BASE_URL=http://localhost:3100 export E2E_POCKETBASE_URL=http://127.0.0.1:8090 export E2E_TEST_EMAIL=ci@example.com export E2E_TEST_PASSWORD=correct-horse-battery-staple export E2E_AGENT_API_KEY=$AGENT_API_KEY # must match what next dev sees npm run e2e ``` First-time setup: `npm run e2e:install` (downloads Chromium + OS deps). ### Manual smoke-test checklist For things that aren't (yet) covered by automated tests, walk through this against a running dev server + PocketBase: 1. **Health** — `curl http://localhost:3100/api/v1/health` → `{ ok: true, service: "wfdash-api" }` 2. **Auth** — `/login`, sign in, get redirected into `(app)`. 3. **Customers** — `/customers` — CRUD, search, paginate, CSV export. 4. **Companies** — `/companies` → `/companies/[id]` — CRUD + relation on linked quotes/orders. 5. **Suppliers** — `/suppliers` — CRUD with tags; verify on `/supplier-map`. 6. **Supplier map** — `/supplier-map` — clustering, dark tiles, fullscreen, marker edit persists coords. 7. **Quotes** — `/quotes` — create, line items, claim, process, email metadata persists; approvals. 8. **Quote → Order (API)** — `POST /api/v1/orders/from-quote` with `{ quote_id }`; new order + copied `order_items`; quote status **unchanged** (regression from `core-recheck-matrix.md`). 9. **Orders** — `/orders/[id]` — per-supplier POs, item + drv status updates. 10. **Order item status (API)** — `PATCH /api/v1/order-items/{id}/status`; both `drv_status` and `driver_status` accepted. 11. **Collection pool** — `/collection-pool` + `GET /api/v1/collection-pool`; assignment updates stick. 12. **Delivery notes** — `/delivery-notes` — basic CRUD; no parity guarantees yet (see parity backlog). 13. **Repairs** — `/repairs` — placeholder (not migrated, per parity backlog). 14. **MCP** — `POST /api/mcp` with `{"method":"tools/list"}` returns 4 tools; `tools/call` works for each. 15. **Build & lint** — `npm run lint` clean; `npm run build` completes without type errors. --- ## Deployment A multi-stage `Dockerfile` is included (Node 20 alpine, non-root `nextjs` user, standalone output). A `nixpacks.toml` pins the Node version for platforms like Railway/Render. ```bash # Docker docker build -t wfdash . docker run --env-file .env.local -p 3000:3000 wfdash # Nixpacks / Railway / Render — just push; nixpacks.toml sets Node 20. ```