Fix inconsistent line endings across all project files for cross-environment consistency. Add complete unit test suite covering core utilities, API routes, and server services. Add Playwright E2E test setup with smoke, auth, and agent API tests. Update all project configuration files to match standardized formatting. Fix MCP API error handling to properly propagate HTTP status codes and use correct JSON-RPC error codes. Add VSCode workspace configuration, extensions, and example MCP config files. Update .gitignore to exclude new test and build artifacts.
339 lines
14 KiB
Markdown
339 lines
14 KiB
Markdown
# 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`)
|
|
|
|
---
|
|
|
|
## 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
|
|
|
|
# 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
|
|
- `/quotes/archived` — archived quotes
|
|
- `/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 <AGENT_API_KEY>`.
|
|
|
|
| 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, archived
|
|
│ │ ├── 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, archived.
|
|
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.
|
|
```
|