# 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.