306 lines
17 KiB
Markdown
306 lines
17 KiB
Markdown
|
|
# WFDASH — Westcott Fasteners Dashboard
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
WFDASH is a Next.js (App Router) + PocketBase application for **Westcott Fasteners**, a fastener supply business. It manages the complete **quote-to-cash lifecycle** for industrial fastener orders — from inbound RFQ emails through pricing, supplier procurement, collection, delivery, and invoicing.
|
||
|
|
|
||
|
|
The system is operator-centric, built for the office team who process dozens of quotes and orders daily. It replaces manual spreadsheet/email workflows with structured pipelines, real-time dashboards, and automation via n8n workflows.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Core Domains
|
||
|
|
|
||
|
|
### 1. **Quotes Pipeline** (`/quotes`, `/sprint`)
|
||
|
|
**Inbound RFQ → Processing → Approval → Send → Follow-up**
|
||
|
|
|
||
|
|
| Feature | Description |
|
||
|
|
|---------|-------------|
|
||
|
|
| **Email Intake** | n8n webhook receives inbound RFQ emails, extracts items via AI, creates quotes in PocketBase. Idempotent on `Message-ID`. |
|
||
|
|
| **Sprint (/sprint)** | Item-level work queue. Operators **claim individual line items** (30-min soft TTL), price them, and save — the item then drops off the list. Replaces "claim whole quote" bottleneck. |
|
||
|
|
| **Categories** | Admin-managed categories (JEI, TOOLS, FASTENERS, OIL, HUSQVARNA, UNKNOWN) with brand/supplier/keyword match rules. Items auto-bucket or land in UNKNOWN for review. |
|
||
|
|
| **AI Pricing** | n8n AI agent reads supplier price lists, matches SKUs, writes `cost_price` + `selling_price` with `pricing_source="ai"` or `"catalogue"`. Operator reviews before saving. |
|
||
|
|
| **Quote Statuses** | `new → claimed → processed → approval_pending → approved → emailed → complete` (also `rejected`, `problem`, `cancelled`, `mail_failed`, `follow_up`) |
|
||
|
|
| **Approvals** | Dedicated `/quotes/approvals` page for quotes needing sign-off. |
|
||
|
|
| **PDF Generation** | Branded quote PDFs (CNL / ISHERWOOD letterhead) with supplier authorization letters. |
|
||
|
|
| **Email Send** | Modal with CC/BCC, custom subject/body, attachment picker. Status flips to `sending` → n8n sends via SMTP → callback records `emailed` or `mail_failed`. Retry supported. |
|
||
|
|
| **Follow-ups** | Per-quote `follow_up_due` date; dashboard widget flags overdue. |
|
||
|
|
| **Customer Replies** | Inbound replies tracked via `quote_replies` collection; shows "Replied" chip on quote row. |
|
||
|
|
| **Templates** | Reusable quote scaffolding; "Create from template" spawns a real quote. |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 2. **Orders** (`/orders`, `/orders/new`, `/orders/[id]`)
|
||
|
|
**Quote → Order → Line Items → Delivery → Invoice**
|
||
|
|
|
||
|
|
| Feature | Description |
|
||
|
|
|---------|-------------|
|
||
|
|
| **Create from Quote** | One-click converts a quote to an order, copying all line items. Atomic order number generation (`ORD-YYYY-MM-NNN`) via server-side counter — continues sequence across month boundaries, concurrency-safe. |
|
||
|
|
| **Order List** | Collapsible cards grouped by **item status tabs**: New, Processing, Collection Pool, Collected, Delivered, Cancelled. Search spans order #, company, customer, item description, or price. |
|
||
|
|
| **Item Status Lifecycle** | `pending → processed → po_generated → collection_pool → assigned → collected → stock_verified → picking → ready_delivery → delivered` (terminal). Also `ready_invoice`, `invoiced`, `stock_booked`, `cancelled`. |
|
||
|
|
| **Quote Matching** | Auto-links order to quote via exact match on quote_number / reference / RFQ / external_number. Suggests a "potential quote" when fuzzy. |
|
||
|
|
| **Repair Orders** | `is_repair_order` flag for non-standard jobs. |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 3. **Operations / Collection** (`/collection-pool`, `/collection-map`, `/collection-confirm`)
|
||
|
|
**Supplier Collection → Driver Assignment → Confirmation**
|
||
|
|
|
||
|
|
| Page | Purpose |
|
||
|
|
|------|---------|
|
||
|
|
| **Collection Pool** | Supplier-grouped view of items in `collection_pool` or `assigned` status. Per-row "Assign driver" + per-supplier bulk assign. Shows PO #, supplier, age, ready-for-collection flag. |
|
||
|
|
| **Collection Map** | Leaflet map with supplier markers clustered by proximity. Click a cluster → see items → assign driver inline. |
|
||
|
|
| **Collection Confirm** | Driver-facing (mobile) — scan/confirm collection, marks items `collected`. |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 4. **Invoicing** (`/invoicing`)
|
||
|
|
**Delivery Note → QuickBooks Invoice → Print**
|
||
|
|
|
||
|
|
| Feature | Description |
|
||
|
|
|---------|-------------|
|
||
|
|
| **To Invoice Tab** | Orders grouped by order number where at least one item is `ready_invoice`. Per-line qty/price overrides for the QB payload. |
|
||
|
|
| **QuickBooks Integration** | Modal picks QB company (CNL / ISHERWOOD), searches QB customers via n8n webhook, builds invoice payload, POSTs to n8n → QB. Returns PDF URL stored on `order_items.qb_invoice_pdf`. |
|
||
|
|
| **Delivery Notes** | Parallel flow: mark lines `delivery_note` status, auto-generates `DN{YYMMDD}NN` number. Same line editor (include/exclude, qty/price override). |
|
||
|
|
| **Invoiced Tab** | Two sections: **Today's Unprinted** (grouped by invoice #, print badges) + **All Invoiced** (searchable history). Print tracking (printedAt, printedBy, printCount) stored in `order_item_details.print` JSON. |
|
||
|
|
| **PDF Print** | Server-rendered React-PDF → streamed back. One-click print for single or bulk (today's batch). |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 5. **Stock Management** (`/stock`, `/stock/[id]`, `/stock-orders`)
|
||
|
|
**Inventory → Surplus Booking → Stock Orders → Ledger**
|
||
|
|
|
||
|
|
| Feature | Description |
|
||
|
|
|---------|-------------|
|
||
|
|
| **Stock Items** | SKU, name, unit, location, current_qty, min_qty, supplier. Status badge: OK / Low / Critical / Inactive. |
|
||
|
|
| **Stock Ledger** | Immutable append-only ledger (`stock_ledger`) with signed `change_qty` and `reason` (`purchase_surplus`, `stock_order`, `customer_fulfillment`, `adjustment`, `return`). `current_qty` denormalized for fast reads. |
|
||
|
|
| **Surplus Booking** | When a supplier delivers more than ordered, operator splits the line: customer qty → order, surplus qty → stock. Creates `stock_ledger` entry (`reason=purchase_surplus`) + links `order_item.surplus_qty` + `stock_booked_from`. |
|
||
|
|
| **Stock Orders (v2)** | Pure stock replenishment: `SP-YYYYMMDDNN` number, multi-line, status `draft → sent → received → cancelled`. Receive flow books ledger + updates `current_qty` per line. |
|
||
|
|
| **Fulfilment from Stock** | `/api/v1/stock/fulfill` — books out qty from stock for a customer order, creates ledger entry (`reason=customer_fulfillment`). |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 6. **Pricelist / Catalogue** (`/pricelist`)
|
||
|
|
**Raw Uploads → AI Extraction → Structured Catalogue**
|
||
|
|
|
||
|
|
| Tab | Purpose |
|
||
|
|
|-----|---------|
|
||
|
|
| **Raw Uploads** | Drag-drop PDF/CSV/TXT/images (≤50 MB). Optional supplier link, status, **free-text notes for AI** (e.g. "Brand is JEI. Apply 30% markup. ZAR excl. VAT."). Upload creates `raw_price_list` row; n8n workflow picks up, extracts, writes to `price_list`. |
|
||
|
|
| **Catalogue** | Searchable `price_list` table: SKU, normalised SKU, description, category, brand, supplier, cost/selling/markup, stock_qty, rich `specifications` JSON (diameter, length, material, synonyms). `search_text` column enables fuzzy/synonym search. |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 7. **Purchase Orders** (embedded in Orders flow)
|
||
|
|
**PO Generation → Email → Supplier Reply Tracking**
|
||
|
|
|
||
|
|
- Created from order items grouped by supplier.
|
||
|
|
- Versioned PDFs stored on `purchase_orders.documents` array + `documents_history`.
|
||
|
|
- Email modal (to/cc/bcc, HTML+text body, attachment picker) → n8n sends → callback records `email_message_id`, `last_reply_at`, `supplier_reply_preview`.
|
||
|
|
- Stale PO detection (`purchase-orders/stale` endpoint).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 8. **Delivery Notes** (`/delivery-notes`)
|
||
|
|
**Generated from delivered order items**
|
||
|
|
|
||
|
|
- Auto-numbered `DN{YYMMDD}NN`.
|
||
|
|
- Status: `draft → ready → delivered → cancelled`.
|
||
|
|
- Linked to order + company + customer.
|
||
|
|
- Printable PDF (shares invoice print pipeline).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 9. **Repairs** (`/repairs`)
|
||
|
|
**Lightweight repair job tracker**
|
||
|
|
|
||
|
|
- Repair number auto-generated.
|
||
|
|
- Status: `new → assessing → quoted → approved → in_progress → completed → cancelled`.
|
||
|
|
- Links to customer / company / order (optional).
|
||
|
|
- Item description + fault description + notes.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 10. **Companies / Customers / Suppliers** (`/companies`, `/customers`, `/suppliers`)
|
||
|
|
**Master data management**
|
||
|
|
|
||
|
|
- **Companies**: name, address, email, tel, VAT, reg number, **geo-coordinates** (for map).
|
||
|
|
- **Customers**: name, emails (primary/alt/buyer), company relation, rep (user), delivery coordinates, description.
|
||
|
|
- **Suppliers**: name, email, number, address, supply_tags (for category matching), coordinates, closing time.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 11. **Dashboard** (`/dashboard`)
|
||
|
|
**Real-time KPIs & Charts**
|
||
|
|
|
||
|
|
| Widget | Data |
|
||
|
|
|--------|------|
|
||
|
|
| KPI Cards | Customers, Active Quotes, Orders This Month, Revenue (sparkline per card) |
|
||
|
|
| Concentric Rings | Quote pipeline depth (New / Claimed / Approved / Processed) |
|
||
|
|
| Orders Trend | New vs Fulfilled (8 weeks, stacked bars) |
|
||
|
|
| Status Mix | Quote distribution (progress bars) |
|
||
|
|
| Growth Trends | Customers vs Orders (12 weeks, area chart) |
|
||
|
|
| Approval Rate | Radial gauge (Approved / Active) |
|
||
|
|
| Activity Pulse | Multi-line (Customers / Quotes / Orders velocity) |
|
||
|
|
| Quote Pipeline | Donut chart (all non-template quotes) |
|
||
|
|
| Top Customers | Horizontal bars by quote volume |
|
||
|
|
| Follow-ups Due | Clickable cards → quote detail |
|
||
|
|
|
||
|
|
**Real-time**: PocketBase subscriptions on `customers`, `quotes`, `orders` → auto-refresh.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 12. **Settings** (`/settings`)
|
||
|
|
- Theme picker (light/dark/system) with CSS variables.
|
||
|
|
- Developer identity switcher (for local multi-user testing).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Automation & Integrations (n8n)
|
||
|
|
|
||
|
|
| Workflow | Trigger | Action |
|
||
|
|
|----------|---------|--------|
|
||
|
|
| **Email Intake** | Inbound email (IMAP/webhook) | Parse → AI extract items → Create quote + items + attachments |
|
||
|
|
| **Quote Outbound** | Quote status = `sending` | Render PDF → SMTP send → Callback `recordSendOutcome` |
|
||
|
|
| **Supplier RFQ** | Sprint bulk "Email suppliers" | Group by supplier → Send RFQ → Track `outbound_message_id` |
|
||
|
|
| **Supplier Reply Monitor** | Inbound email with `In-Reply-To` matching PO/quote | Update `supplier_replied_at`, `supplier_reply_count` |
|
||
|
|
| **Price List Ingest** | New `raw_price_list` row | AI extract → Write `price_list` rows → Update `log_summary` |
|
||
|
|
| **QuickBooks Invoice** | Invoicing modal submit | Build QB payload → QB API → Return PDF URL |
|
||
|
|
| **PO Stale Alert** | Scheduled | Find POs without reply > N days → Notify |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Technical Architecture
|
||
|
|
|
||
|
|
| Layer | Technology |
|
||
|
|
|-------|------------|
|
||
|
|
| **Frontend** | Next.js 15 (App Router), React 19, TypeScript, Tailwind CSS |
|
||
|
|
| **Backend** | PocketBase (Go) — auth, real-time, file storage, admin UI |
|
||
|
|
| **Automation** | n8n (self-hosted) — workflows, AI agents, webhooks |
|
||
|
|
| **Charts** | Custom SVG (no Chart.js/Recharts) — sparklines, bars, donuts, radial, area, concentric |
|
||
|
|
| **PDF** | `@react-pdf/renderer` (server-side in API routes) |
|
||
|
|
| **Maps** | Leaflet + OpenStreetMap |
|
||
|
|
| **Auth** | PocketBase auth (email/password), cookie-based session via middleware |
|
||
|
|
| **Real-time** | PocketBase `subscribe("*", callback)` on collections |
|
||
|
|
| **API** | Next.js Route Handlers (`/api/v1/...`) + PocketBase SDK (server) |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Key Design Patterns
|
||
|
|
|
||
|
|
1. **Item-Level Workflows** — Not quote/order level. Sprint, Collection Pool, Invoicing all operate on `order_items` / `quote_items` rows.
|
||
|
|
2. **Optimistic UI + Real-time** — Local state updates immediately; PB subscriptions reconcile.
|
||
|
|
3. **Server-Side Number Generation** — Atomic counters (`quote_counters`, `order_counters`) prevent collisions.
|
||
|
|
4. **Idempotency** — Email intake dedups on `Message-ID`; quote/PO sends record outbound `Message-ID` for bounce matching.
|
||
|
|
5. **Human-in-the-Loop AI** — AI writes `pricing_source="ai"` + zero prices; operator must claim, review, save.
|
||
|
|
6. **JSON Audit Fields** — `order_item_details`, `quote_item.pricing_notes`, `print` metadata stored as JSON on the row — no extra collections for append-only logs.
|
||
|
|
7. **Per-User Color Coding** — `claimed_by` / `rep` → deterministic hue (OKLCH) for row tinting + avatar chips.
|
||
|
|
8. **Working-Day SLA** — `isOlderThanWorkingDays()` excludes weekends; dashboard ages quotes in working days.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Data Model Highlights (PocketBase Collections)
|
||
|
|
|
||
|
|
| Collection | Purpose |
|
||
|
|
|------------|---------|
|
||
|
|
| `quotes` | Quote header (number, customer, company, status, email thread, PDF, follow-up) |
|
||
|
|
| `quote_items` | Line items (description, qty, cost/sell/markup, supplier, claim_status, pricing_source, category) |
|
||
|
|
| `quote_attachments` | Inbound RFQ files + generated PDFs + outbound attachments |
|
||
|
|
| `quote_replies` | Customer email replies (thread) |
|
||
|
|
| `orders` | Order header (number, company, customer, quote link, rep, invoiced flag) |
|
||
|
|
| `order_items` | Line items (status lifecycle, supplier, PO link, in_stock, surplus_qty, pricing audit) |
|
||
|
|
| `purchase_orders` | Supplier POs (versioned PDFs, email payload, supplier replies) |
|
||
|
|
| `collection_pool` | Driver assignment queue (links order_item + supplier + driver) |
|
||
|
|
| `stock_items` | Inventory master (SKU, qty, min, location, supplier) |
|
||
|
|
| `stock_ledger` | Immutable movements (signed qty, reason, reference) |
|
||
|
|
| `stock_purchases` | Stock replenishment orders |
|
||
|
|
| `price_list` | Structured catalogue (AI-extracted) |
|
||
|
|
| `raw_price_list` | Uploaded supplier files awaiting extraction |
|
||
|
|
| `categories` | Admin-managed buckets for sprint (match_brands, match_supplier_tags, match_keywords) |
|
||
|
|
| `delivery_notes` | Delivery note header |
|
||
|
|
| `delivery_note_items` | Line items on delivery note |
|
||
|
|
| `repairs` | Repair jobs |
|
||
|
|
| `companies` / `customers` / `suppliers` | Master data |
|
||
|
|
| `users` (auth) | Operators + drivers |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Pages / Routes Summary
|
||
|
|
|
||
|
|
| Route | Purpose |
|
||
|
|
|-------|---------|
|
||
|
|
| `/dashboard` | KPI dashboard |
|
||
|
|
| `/quotes` | Quote list (tabs: New, Claimed, Pending Approval, Completed, Email Failed, Problem, All, Templates) |
|
||
|
|
| `/quotes/[id]` | Quote detail (items, pricing, PDF, email, approvals) |
|
||
|
|
| `/quotes/[id]/process` | Sprint-style item processing for a single quote |
|
||
|
|
| `/quotes/approvals` | Approval queue |
|
||
|
|
| `/sprint` | Global item-level pricing queue (by category) |
|
||
|
|
| `/orders` | Order list (item-status tabs) |
|
||
|
|
| `/orders/new` | Create order (from quote or blank) |
|
||
|
|
| `/orders/[id]` | Order detail (items, status transitions, PO, delivery, invoice) |
|
||
|
|
| `/orders/assign` | Legacy driver assign (hidden from nav) |
|
||
|
|
| `/invoicing` | To Invoice / Invoiced (QB + delivery notes + print) |
|
||
|
|
| `/collection-pool` | Supplier collection queue + bulk assign |
|
||
|
|
| `/collection-map` | Map view of suppliers |
|
||
|
|
| `/collection-confirm` | Mobile driver confirmation |
|
||
|
|
| `/stock` | Stock list + low-stock filter |
|
||
|
|
| `/stock/[id]` | Stock detail + ledger + adjustments |
|
||
|
|
| `/stock-orders` | Stock replenishment orders |
|
||
|
|
| `/pricelist` | Raw uploads + Catalogue tabs |
|
||
|
|
| `/suppliers` | Supplier CRUD |
|
||
|
|
| `/companies` | Company CRUD |
|
||
|
|
| `/customers` | Customer CRUD |
|
||
|
|
| `/repairs` | Repair job tracker |
|
||
|
|
| `/delivery-notes` | Delivery note list |
|
||
|
|
| `/settings` | Theme, dev identity |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## API Endpoints (Key)
|
||
|
|
|
||
|
|
| Endpoint | Method | Purpose |
|
||
|
|
|----------|--------|---------|
|
||
|
|
| `/api/v1/quotes/[id]/pdf` | GET | Render quote PDF |
|
||
|
|
| `/api/v1/quotes/[id]/send-email` | POST | Validate + queue outbound send |
|
||
|
|
| `/api/v1/quotes/[id]/retry-email` | POST | Retry failed send |
|
||
|
|
| `/api/v1/quotes/[id]/create-from-template` | POST | Spawn quote from template |
|
||
|
|
| `/api/v1/orders/from-quote` | POST | Convert quote → order |
|
||
|
|
| `/api/v1/order-items/[id]/status` | PATCH | Transition item status |
|
||
|
|
| `/api/v1/invoicing/customers` | GET/POST | QB customer search/create |
|
||
|
|
| `/api/v1/stock/items` | GET/POST | Stock CRUD |
|
||
|
|
| `/api/v1/stock/items/[id]/adjust` | POST | Manual qty adjustment + ledger |
|
||
|
|
| `/api/v1/stock/fulfill` | POST | Book out stock for customer order |
|
||
|
|
| `/api/v1/stock/surplus-book` | POST | Book surplus from collection |
|
||
|
|
| `/api/v1/stock/purchases` | GET/POST | Stock orders CRUD |
|
||
|
|
| `/api/v1/stock/purchases/[id]/receive` | POST | Receive stock order lines |
|
||
|
|
| `/api/v1/sprint/items/[id]/claim` | POST | Race-safe 30-min claim |
|
||
|
|
| `/api/v1/sprint/items/[id]/attachments/upload` | POST | Upload pricing proof |
|
||
|
|
| `/api/v1/purchase-orders/[id]/send-email` | POST | Send PO to supplier |
|
||
|
|
| `/api/v1/purchase-orders/[id]/pdf` | GET | Render PO PDF |
|
||
|
|
| `/api/v1/files/[collection]/[recordId]/[filename]` | GET | Stream PocketBase file |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Deployment Notes
|
||
|
|
|
||
|
|
- **PocketBase** runs separately (Docker/binary). App connects via `NEXT_PUBLIC_POCKETBASE_URL`.
|
||
|
|
- **n8n** runs separately; webhooks configured in n8n point to `/api/v1/...` routes.
|
||
|
|
- **Files** served via PocketBase `/api/files/...` — proxied through Next.js route for auth if needed.
|
||
|
|
- **Environment**: `NEXT_PUBLIC_POCKETBASE_URL`, `POCKETBASE_ADMIN_EMAIL`, `POCKETBASE_ADMIN_PASSWORD` (for server SDK).
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Summary
|
||
|
|
|
||
|
|
WFDASH is a **complete operational ERP** for a fastener distributor, covering:
|
||
|
|
|
||
|
|
1. **Quote intake** (email → AI → structured quote)
|
||
|
|
2. **Item-level pricing** (Sprint claim/price/save)
|
||
|
|
3. **Order management** (quote → order → multi-status item lifecycle)
|
||
|
|
4. **Supplier procurement** (PO generation, email, reply tracking)
|
||
|
|
5. **Collection logistics** (pool → map → driver assign → confirm)
|
||
|
|
6. **Inventory** (stock items, ledger, surplus booking, stock orders)
|
||
|
|
7. **Invoicing** (delivery notes + QuickBooks + PDF print)
|
||
|
|
8. **Catalogue** (raw uploads → AI extraction → searchable price list)
|
||
|
|
9. **Repairs** (lightweight job tracker)
|
||
|
|
10. **Real-time dashboard** (KPIs, charts, follow-ups)
|
||
|
|
|
||
|
|
Built for **speed, auditability, and operator ergonomics** — every screen is a work queue, not a CRUD form.
|