From 064145e5b77e5007070e4906532a22043c9da4d7 Mon Sep 17 00:00:00 2001 From: Dave Date: Thu, 3 Sep 2026 17:57:49 +0200 Subject: [PATCH] refactor(sops): streamline SOP documents for roles, consolidating content into quickstart guides for easier reference --- docs/sops/README.md | 94 +- docs/sops/approver-supervisor.md | 507 ++--------- docs/sops/collection-driver.md | 447 ++-------- docs/sops/order-operator.md | 594 +++---------- docs/sops/quote-operator.md | 639 +++----------- .../plans/2026-09-03-pos-network-printer.md | 831 ++++++++++++++++++ 6 files changed, 1269 insertions(+), 1843 deletions(-) create mode 100644 docs/superpowers/plans/2026-09-03-pos-network-printer.md diff --git a/docs/sops/README.md b/docs/sops/README.md index bbffb37..30cd2bb 100644 --- a/docs/sops/README.md +++ b/docs/sops/README.md @@ -1,80 +1,32 @@ -# WFDASH Standard Operating Procedures (SOPs) +# WFDASH SOPs -These documents define the **what, how, and how-measured** for every operator -role in the Westcott Fasteners business. Each role has its own SOP file -covering: +Quick cheat sheets for each role in the business. Read your role's SOP +on your first day. Keep it handy. -1. **Daily / Weekly Checklist** — concrete actions with cadence -2. **Step-by-Step Procedures** — how to do each recurring task -3. **Decision Tree** — what to do when X happens -4. **KPIs** — the scorecard, with how each metric is measured +## The four roles -The source of truth is the markdown in this folder. The WFDASH app -renders them at `/sops` for in-app reference. +| File | What they do | +|---|---| +| [quote-operator.md](./quote-operator.md) | Answer the phone, price items, send quotes, follow up. | +| [order-operator.md](./order-operator.md) | Turn accepted quotes into orders. Buy from suppliers. Get goods to the customer. | +| [collection-driver.md](./collection-driver.md) | Drive to suppliers, collect the goods, deliver to customers. (Mobile app.) | +| [approver-supervisor.md](./approver-supervisor.md) | Sign off quotes, run the weekly review, handle escalations. | ---- +## How to read your SOP -## Roles +Each one has four short sections: -| Role | File | What they own | -|---|---|---| -| **Quote Operator** | [`quote-operator.md`](./quote-operator.md) | Sprint queue, line-item pricing, sending quotes, follow-ups | -| **Order Operator** | [`order-operator.md`](./order-operator.md) | Order lifecycle, purchase orders, delivery, invoicing handoff | -| **Collection Driver** | [`collection-driver.md`](./collection-driver.md) | Mobile app, supplier pickups, deliveries, surplus/shorts | -| **Approver / Supervisor** | [`approver-supervisor.md`](./approver-supervisor.md) | Approvals queue, pricing policy, escalations, dashboards | +1. **What you do** — your job, in plain English. +2. **Your day** — the actions, in order, with times. +3. **How to do the common stuff** — step-by-step for the 3-4 tasks + you'll do most often. +4. **Don'ts & when to ask** — the 3-4 things you must never do, and + the 3-4 situations where you stop and ask the supervisor. ---- +That's it. There are no bonus points for reading more. -## How to read these +## When something changes -- Each SOP is **role-scoped**. Don't read another role's SOP unless you're - cross-training. They overlap on shared handoffs — those overlaps are - intentional and called out in the relevant procedures. -- The **Daily Checklist** is the minimum bar. If you only have 10 minutes, - do the checklist. -- The **Decision Tree** is a fallback. When you encounter the situation - and you don't know what to do, find the branch. The tree is not - exhaustive — when in doubt, escalate (see each role's escalation path). -- **KPIs are not aspirational.** They are how your performance is measured - in the weekly review. If a KPI is unclear, raise it — don't ignore it. - ---- - -## How to suggest changes - -1. Open a PR against the relevant file. -2. PR title: `sop(): ` (e.g. `sop(quote-operator): add - partial-approval procedure`). -3. Tag the approver / supervisor in the review. -4. After merge, the `/sops` page in the app updates on the next deploy. - -If you can't write the change yourself, drop the suggestion in -`#sop-feedback` and someone will turn it into a PR. - ---- - -## Conventions used in these documents - -- **Routes** are written as `/path/to/page` (e.g. `/sprint`, - `/orders/123`). -- **Buttons / actions** are in **bold** (e.g. **Claim**). -- **Status values** are in `code` (e.g. `approval_pending`, - `collection_pool`). -- **Collection / field names** are in `code` (e.g. - `order_items.item_status`). -- **SLA times** are in working days (Mon–Fri, SAST). Weekends don't - count. The `isOlderThanWorkingDays()` helper in the dashboard uses - the same convention. -- **Currency** is ZAR (excl. VAT) unless otherwise noted. - ---- - -## When a section is missing or wrong - -These SOPs are **derived from `APP_CAPABILITIES.md`, the -`item_status` lifecycle, the quote/order state machines, and the -order-workflow-rules**. If the system has changed and the SOP hasn't: - -- It's a doc bug. **File a PR** or ping the supervisor. -- The SOP is **not** authoritative over the system. The system is - authoritative. If they disagree, follow the system and fix the SOP. +If your job changes and the SOP doesn't match, tell the supervisor. +They'll fix the SOP. Don't keep doing the old thing because the SOP +said so. diff --git a/docs/sops/approver-supervisor.md b/docs/sops/approver-supervisor.md index b47f50a..44c1267 100644 --- a/docs/sops/approver-supervisor.md +++ b/docs/sops/approver-supervisor.md @@ -1,439 +1,122 @@ -# Approver / Supervisor — SOP +# Approver / Supervisor — Quickstart -**Owns:** the approvals queue, pricing/markup policy, escalations -from Quote and Order Operators, weekly KPI reviews, and the -health of the system overall. +**One-line job:** keep the team unblocked, the system healthy, and +the customers happy. -**Tools:** `/quotes/approvals`, all operator routes (read), -`/dashboard`, `/customers`, `/suppliers`, `/settings`, -`/api/v1/...` admin endpoints, the daily summary call. +You don't do the pricing or the driving. You make sure the people +who do have what they need, the system gives them the right signals, +and the customers don't fall through the cracks. -**Handoffs:** -- **From Quote Operator** → quote in `approval_pending`. -- **To Quote Operator** → approved / rejected / change-requested. -- **From Order Operator** → escalations (cancellation, refund, - discount > 15%, supplier issue). -- **To Order Operator** → decision + instruction. -- **From Collection Driver** → escalations (refusal, damage - dispute, customer relationship issue). -- **To everyone** → policy updates, training, weekly review. +## What you do ---- +- Sign off quotes that landed in `/quotes/approvals`. +- Handle escalations the operators bring to you (discount, cancellation, + customer complaint, supplier dispute). +- Pull the weekly KPI one-pager, run the Monday standup. +- Write the Friday summary. +- Set pricing policy when costs / markets shift. -## 1. Daily / Weekly Checklist +## Your day -### Every working day (start of shift, 08:00 SAST) +| Time | What to do | +|---|---| +| **08:00** | Open `/quotes/approvals`. Clear the queue. Anything > 4 hours old = bug. | +| **08:30** | Glance at `/dashboard`. Approval rate, pipeline depth, follow-ups due. Anything red? | +| **13:00** | Re-check `/quotes/approvals`. Anything still pending from this morning is now a bug. | +| **17:00** | Triage escalations. Every escalation gets a decision or a clear next step. | -- [ ] **Open `/quotes/approvals`.** Clear the queue. Anything - older than 4 working hours needs a decision today. -- [ ] **Open `/dashboard`.** Quick scan: - - **Approval Rate** gauge — should be > 70% on a healthy - pipeline. - - **Quote Pipeline** donut — too much in `new` for >2 days - is a Quote Operator capacity problem. - - **Follow-ups Due** — count should be trending down. - - **Concentric rings** (quote pipeline depth) — if the - "Approved" ring is growing while the "Sent" ring is - shrinking, the email send flow has a problem. -- [ ] **Check `#quote-ops` and `#order-ops`** channels. Triage - any escalations posted overnight. -- [ ] **Skim `/orders` — Cancelled tab.** A spike in - cancellations = supplier problem or pricing problem. - Investigate. +**Monday** (90 min block): pull the weekly KPI one-pager, run the +standup (15 min), update pricing policy if needed. -### Every working day (mid-day, 13:00 SAST) +**Friday** (afternoon): write the 1-page weekly summary, forecast +next month's cash collection. -- [ ] **Re-check `/quotes/approvals`.** Anything still in - `approval_pending` from this morning is now a process bug. -- [ ] **Triage any pending escalations.** Quote Operators and - Order Operators should be unblocked by lunch. -- [ ] **Review one random operator action** (audit). Pick an - order or quote at random, walk through the action log. - Calibrates your understanding of the team's actual work. +## How to do the common stuff -### Every working day (end of shift, 17:00 SAST) +### 1. Approve a quote -- [ ] **Clear the day's escalations.** Every escalation should - have either a decision or a clear next step. -- [ ] **Review today's KPI snapshot.** Any operator trending - red? Send them a 1-line note (not a formal message — keep - it human). - -### Every Monday morning (90 min block) - -- [ ] **Pull the weekly KPI one-pager** for each Quote and - Order Operator (and the Collection Driver's app-export). -- [ ] **Hold the weekly standup** (15 min): - - Each operator: 1 number (their worst KPI) + 1 win. - - You: 1 policy reminder + 1 system change coming this week. -- [ ] **Update pricing policy** if any of these moved: - - Supplier price list changes (cost-side). - - Competitor pricing (market-side). - - Currency moves (ZAR volatility). - - A new strategic account needs a standing discount. - -### Every Friday afternoon - -- [ ] **Write the 1-page weekly summary.** Distribution: owner - (you), the 4 operators, the invoicing clerk (if separate), - and the owner / MD if you have one. Sections: - - **Pipeline health** (counts by status, vs last week). - - **Top 3 wins** (one per operator, named). - - **Top 3 issues** (with an owner + deadline). - - **1 thing we changed** (a process, a policy, a system tweak). - - **Next week's risks.** -- [ ] **Forecast cash collection** for the next 30 days. Pull - `delivered` orders not yet `invoiced`, sum their - `selling_price`. This is the cash-in number the owner wants. - -### Every month (1st, 60 min) - -- [ ] **Monthly 1-on-1 with each operator.** 30 min each. - Agenda (use the 1-page review you wrote): - - Their KPIs vs target. - - Their 30-day improvement action. - - What they need from you (training, tools, headcount). - - What they want to keep doing. -- [ ] **Catalogue coverage review.** What % of quote items - this month were matched in the catalogue? If < 50%, the - Pricelist Curator needs to upload more supplier lists. -- [ ] **Supplier performance review.** Late deliveries, - cost-mismatch rate, damage rate. Note any supplier that - should be replaced or have terms renegotiated. - -### Every quarter - -- [ ] **Strategic review.** 2-hour session with the owner. - Topics: revenue trend, margin trend, customer concentration - risk, supplier concentration risk, system gaps. -- [ ] **Operator tier reviews.** Who's eligible for markup- - authority / trusted-buyer / senior-driver tier? -- [ ] **System roadmap input.** What 1-2 features would - unlock the most operator capacity? - ---- - -## 2. Step-by-Step Procedures - -### 2.1 Approve a quote - -**When:** a quote lands in `approval_pending`. - -1. Open `/quotes/approvals`. The queue is sorted by age, - oldest first. -2. Click a quote. Review: +1. Open `/quotes/approvals`. Oldest first. +2. For each quote, check: - **Customer + rep** — known account? Standing discount? - - **All lines priced** (every line has `selling_price > 0`). - - **Markup** — does the markup meet the policy for this - category / customer? - - **Validity** — is the validity date realistic? - - **Supplier** — do we have a healthy relationship with - this supplier? Any open POs with them? -3. **Decide:** - - **Approve** → click **Approve**. Status flips to - `approved`. The Quote Operator can now send. - - **Request changes** → add a comment in the change- - request field. Status stays in `approval_pending` (or - reverts to `processed` if it was a pricing issue). The - Quote Operator re-submits. - - **Reject** → add a reason. Status flips to `rejected`. - The Quote Operator may revise or close the quote. -4. **Log the decision in `quote.notes`** (auto-stamped by - the system, but add context if it's non-obvious — e.g. - "Approved at 18% markup, customer's standing discount - from June 2026 contract"). + - **Every line priced** — any zeros? Refuse and send back. + - **Markup** — does it match the policy for this category / customer? + - **Validity date** — realistic? +3. **Approve** (status → `approved`, operator can send) or + **Request changes** (operator re-prices) or **Reject** (with + a reason — not a silent "no"). +4. **Log the decision in the quote's notes** if it's non-obvious + (e.g. "approved at 18% markup per Aug 2026 customer contract"). -**Don't:** -- Don't approve without reading the full quote. A 30-second - review misses the wrong SKU, the wrong qty, the wrong - customer. -- Don't reject without a reason. "No" is demoralising; - "No, because the JEI prices are below cost" is a teachable - moment. +### 2. Handle a "should I give this discount?" escalation -### 2.2 Set / change a pricing policy +- **< 5%** — the operator can do it. Tell them to apply and log. +- **5-15%** — if strategic account OR one-off (large order, end-of-line), + approve. Otherwise tell the operator to push back. +- **> 15%** — you (or the owner) call the customer. Don't delegate. -**When:** cost or market conditions change. +### 3. Handle a "customer wants to cancel" escalation -1. **Identify the affected scope.** Is it a category (e.g. - "all FASTENERS"), a customer (e.g. "Account XYZ"), or a - supplier (e.g. "all JEI lines")? -2. **Pull the data.** What is the current avg markup for - that scope? What are competitors charging? What's the - supplier cost trend? -3. **Propose the new policy.** Document: - - Scope (which customers / categories / SKUs). - - New markup range. - - Effective date. - - Reason (market move, supplier cost, strategic account). -4. **Communicate.** Post in `#quote-ops`, mention in the - Monday standup, and pin in `/settings` (if there's a - pricing-policy field there). -5. **Audit after 2 weeks.** Pull the avg markup for the - scope — is the team applying the new policy? If not, - retrain. +- **No PO sent yet** — easy, cancel the line. +- **PO sent, supplier hasn't confirmed** — phone the supplier NOW + (most will cancel without charge if pre-confirm). +- **PO confirmed, goods in transit / collected** — three options: + accept and book to stock, refuse delivery (restocking fee?), or + return-to-vendor. You decide based on the customer + the cost. -### 2.3 Handle an escalation from a Quote Operator +### 4. Set / change pricing policy -**When:** a Quote Operator pings you with a question -(markup override, customer relationship, stuck item). +1. Identify the scope (category? customer? supplier?). +2. Pull the data: current avg markup for that scope, competitor + pricing, supplier cost trend. +3. Document the change: scope, new range, effective date, why. +4. Communicate: post in the team chat, mention Monday standup, pin + in `/settings` if there's a policy field. +5. Audit after 2 weeks — is the team actually applying it? -1. **Read the full context** before responding. Don't - respond to a 1-line ping with a 1-line answer. -2. **Categorise:** - - **Pricing** → see §2.2. Likely a one-off override. - - **Customer relationship** → call the customer yourself - if strategic; delegate back if not. - - **System / data** → the system has a gap. File a - feature request in `src/app/api/mcp/FEATURE-REQUESTS.md` - (or whatever the current backlog file is). - - **Process** → the SOP is unclear. Update the relevant - SOP (and tell the team). -3. **Respond with a decision + a why.** The operator needs - to learn from the decision, not just execute it. -4. **Follow up next day.** Verify the decision was applied. +## Don'ts — the 3 things you must never do -### 2.4 Handle an escalation from an Order Operator +1. **Never approve a quote without reading it.** 30-second reviews + miss the wrong SKU, wrong qty, wrong customer. If you don't have + time, do it later. Don't rubber-stamp. +2. **Never reject an operator's escalation without a reason.** "No" + is demoralising. "No, because the markup is below the policy + for this category" is a teachable moment. +3. **Never push a system-level decision to the operators.** If + the data is wrong, the SOP is unclear, or a workflow is + broken — that's yours to fix, not theirs to work around. -**When:** an Order Operator pings you with cancellation, -refund, discount > 15%, supplier dispute, customer -complaint. +## When to escalate to the owner (the 4 most common situations) -1. **Same triage as §2.3**, but the stakes are usually - higher (real money, real relationships). -2. **For customer complaints** — get the customer's side of - the story first. Don't take the operator's word at face - value; don't take the customer's at face value either. -3. **For supplier disputes** — pull the PO + delivery - evidence (photos, signed notes). Make the call based on - the contract, not the relationship. -4. **Document the decision** in the order's `notes` and - your private escalation log. +- **Strategic account (top 10% by spend) wants bespoke terms.** Call + the owner. Strategic accounts can have bespoke anything; it's + a relationship call, not an SOP. +- **A supplier wants to renegotiate terms that change > 20% of our + spend.** Call the owner. This is a business call, not an + operator call. +- **A customer escalates a quality issue and asks for the owner / + MD by name.** Call the owner. Don't try to be the buffer. +- **Anything involving a refund > R50,000, a credit note > R100,000, + or a write-off.** Call the owner. Documented, in writing. -### 2.5 Run a weekly KPI review +## How you're measured (the 3 numbers that matter) -**When:** every Monday. +1. **Approval turnaround** — target: **< 4 working hours median.** + (Quotes don't sit in your queue.) +2. **Escalation response time** — target: **< 2 working hours.** + (Operators aren't blocked waiting on you.) +3. **Team KPI attainment** — target: **> 80% of operator KPIs at + target** each week. (Your team is doing their job.) -1. **Pull the one-pager.** Each operator has 4-6 KPIs. The - report highlights who's red, who's yellow, who's green. -2. **For each red operator:** - - Look at the underlying data. Is it a process issue, - a capacity issue, or a skill issue? - - **Process** → fix the SOP, retrain. - - **Capacity** → redistribute the load or accept the - miss. - - **Skill** → 1-on-1 coaching, shadow a senior operator. -3. **For each green operator:** - - Find the win. Mention it in the standup. - - If they're consistently green, consider the tier - bump. -4. **Write the 1-page weekly summary.** (See §1 Friday.) +Bonus number: **catalogue coverage** — target > 60% of quote items +matched. If low, the Pricelist Curator needs to upload more supplier +lists. That's your nudge, not theirs. -### 2.6 Audit a random operator action +## Who to ask -**When:** mid-day, when you have 10 minutes. - -1. Pick a random order or quote from the last 24 hours. -2. Walk the action log: who touched it, when, what - changed. -3. **Look for:** - - Status transitions that don't match the SOP (e.g. - `pending` → `delivered` skipping the intermediate - states). - - Pricing changes without a `pricing_notes` breadcrumb. - - Items stuck in a state longer than the SLA. -4. **If you find a real issue:** message the operator - privately, not in public. Coaching, not blame. -5. **If you find a SOP gap:** update the SOP. - ---- - -## 3. Decision Tree - -### 3.1 Should I approve this discount? - -``` -What's the customer's standing discount (per the customer -record / contract)? -├── Matches → approve at face value -└── Doesn't match - ├── Is the difference small (< 3%)? → approve, log the - │ reason (e.g. - │ "volume uplift - │ from Q3 contract"). - ├── Is the difference medium (3-10%)? → approve if - │ the customer - │ is strategic - │ OR the deal - │ is a one-off - │ (large order, - │ end-of-line - │ stock, etc). - │ Otherwise, - │ ask the - │ operator to - │ split the - │ margin with - │ the customer - │ themselves. - └── Is the difference large (> 10%)? → talk to the - customer - yourself. - Strategic - pricing is a - relationship - call, not a - quote. -``` - -### 3.2 Supplier wants to renegotiate terms - -``` -Is the supplier a strategic dependency (> 20% of our spend)? -├── Yes → call the supplier yourself. Don't delegate. -│ Renegotiation is a relationship call. -└── No - ├── Will the new terms save us money or improve - │ service? → accept, log in the supplier record. - └── Will they cost more or worsen service? → decline, - keep the - relationship - professional - (don't burn - the bridge). -``` - -### 3.3 Customer escalates a quality issue - -``` -Is the issue real (defect, wrong item, damaged in transit)? -├── Yes - ├── Was the issue caused by us (wrong pick, late - │ delivery, damaged in our care)? - │ ├── Yes → own it. Replace or refund, no argument. - │ │ Don't ask the customer to prove the - │ │ damage. - │ └── No (supplier defect, customer mishandling) - │ ├── Defect is our supplier's → call them, get - │ │ a credit or - │ │ replacement, pass - │ │ it on. - │ └── Customer mishandling → politely decline. - │ Offer a goodwill - │ discount at your - │ discretion. - └── What's the cost of fixing vs losing the customer? - ├── Customer is strategic → fix, no matter the - │ cost. Relationships - │ > margins. - └── Customer is transactional → fix only if it's - cheaper than - losing them. Most - one-off customers - aren't worth a - free replacement. -``` - -### 3.4 An operator is consistently missing a KPI - -``` -How long have they been missing it? -├── < 2 weeks → noise. Mention in the 1-on-1, move on. -├── 2-4 weeks → pattern. Diagnose: -│ ├── Process gap (unclear SOP) → update the SOP, -│ │ retrain. -│ ├── Capacity gap (too much load) → redistribute. -│ └── Skill gap (doesn't know how) → pair with a -│ senior operator for -│ a week. -└── > 4 weeks → performance issue. Formal conversation. - Set a 30-day improvement target with consequences - if missed. -``` - -### 3.5 A quote is in `approval_pending` for > 1 working day - -``` -Who submitted it? -├── Quote Operator on shift → ping them directly. "Why is -│ this still in my queue?" -└── Quote Operator off shift / on leave → reassign to - another operator - OR approve it - yourself if - straightforward. -``` - -### 3.6 The system is misbehaving (data loss, wrong status, etc.) - -``` -Is the issue isolated to one record? -├── Yes → fix the record directly via the admin tools or -│ `pb-helpers.ts` / service layer. Document the -│ fix. -└── No (multiple records, or a pattern) - ├── Stop the workflow that's causing the damage. - │ (e.g. pause the n8n email intake if it's creating - │ bad quotes.) - ├── Page the dev. Don't try to fix the code yourself - │ unless you ARE the dev. - ├── Communicate to the team: "stop doing X until - │ further notice." - └── After the fix: write up the post-mortem. What - happened, what we did, what we'll do to prevent - it. Share with the team. -``` - -### 3.7 Strategic account wants terms we don't usually offer - -``` -What's the customer's annual spend with us? -├── Top 10% → call the owner / MD. Strategic accounts -│ can have bespoke terms; it's a relationship -│ call. -├── 10-25% → decide yourself. Document the terms in the -│ customer record, in a contract if material. -└── Bottom 75% → decline. Standardise. Bespoke terms - for non-strategic accounts erode - margin and create admin overhead. -``` - ---- - -## 4. KPIs - -The supervisor's KPIs are different from the operators' — -they're about the team's health, not the supervisor's own -output. - -| KPI | Target | How it's measured | -|---|---|---| -| **Approval turnaround** | < 4 working hours median | `approved_at - submitted_at` on `quotes/approvals` | -| **Escalation response time** | < 2 working hours | `decision_at - escalation_at` on the escalation log | -| **Team KPI attainment** | > 80% of operator KPIs at target | weekly one-pager summary | -| **Quote → order conversion** | > 35% trailing 90 days | orders / emailed quotes | -| **On-time delivery rate** | > 90% trailing 30 days | orders delivered by `delivery_date` / total delivered | -| **Catalogue coverage** | > 60% of quote items matched | sprint items with `matched_price_list_id` / total sprint items | -| **Supplier cost-mismatch rate** | < 3% trailing 30 days | `cost_mismatch` verdicts / total receives | -| **Customer concentration risk** | top customer < 30% of revenue | revenue by customer over trailing 90 days | -| **System uptime** | > 99% trailing 30 days | uptime of WFDASH + PB + n8n (monitor externally) | -| **Team tenure** | trending up | months since each operator's start date | - -### How the supervisor's KPIs feed into YOUR review - -- **Weekly**: the owner / MD sees the weekly summary. Your - team KPIs are the headline. -- **Monthly**: 1-on-1 with the owner / MD. Focus on - strategic accounts, concentration risk, and system gaps. -- **Quarterly**: strategic review. This is where you push - for the headcount / tools / process changes the team - needs. - -### What the system does NOT measure (yet) - -- Operator morale (no internal NPS yet) -- Customer satisfaction (no external NPS yet) -- Operator skill growth (no per-operator skill matrix yet) - -If a KPI is unfair, raise it. The owner can adjust -targets, but the metric stays. +- **The owner / MD** — for any of the 4 escalation situations above, + anything strategic, anything you can't decide alone. +- **Other supervisors / industry peers** — for pricing policy sanity + checks ("does this markup make sense for JEI?"). +- **When in doubt, write it down.** If you made a decision, log it + in the relevant record. Future you (and the auditors) will + thank you. diff --git a/docs/sops/collection-driver.md b/docs/sops/collection-driver.md index 0bd536c..6e38956 100644 --- a/docs/sops/collection-driver.md +++ b/docs/sops/collection-driver.md @@ -1,383 +1,118 @@ -# Collection Driver — SOP +# Collection Driver — Quickstart -**Owns:** picking up goods from suppliers, delivering to customers, -capturing proof of collection and delivery. Runs on the -**WFDASHDRIVER-APP** (mobile) and `/collection-confirm` (browser -fallback). +**One-line job:** pick up goods from suppliers, drop them at customers. -**Tools (mobile):** WFDASHDRIVER-APP, `/collection-confirm`. -**Tools (office view):** `/collection-pool`, `/collection-map`. +You do this on your **phone** (WFDASHDRIVER-APP) or the office +`/collection-confirm` page. The app is the source of truth — what's +on the app is what counts. -**Handoffs:** -- **From Order Operator** → PO is `po_generated`, item is in - `collection_pool` or `assigned` to you. -- **To Order Operator** → you mark `collected` on the way back - to the office, or on delivery. -- **To Stock Controller** → surplus or damaged goods need to be - booked. +## What you do -**Note:** `drv_status` and `drv_id` are the driver-side fields on -`order_items`. The status used here is the item-level -`item_status` (see Decision Tree for the exact mapping). +Every day, the app has a list of pickups and deliveries. You: ---- +- Drive to the supplier, collect the goods. +- Take a photo + sign at the supplier. +- Drop the goods at the customer. +- Take a photo + get the customer's signature. +- Flag anything weird (short, wrong, damaged) before leaving. -## 1. Daily / Weekly Checklist +## Your day -### Every working day (start of shift, 07:00 SAST) +| Time | What to do | +|---|---| +| **07:00** | Open the app. Look at today's run — suppliers, in order. | +| **07:15** | Phone the first supplier if you haven't worked with them in a while. Confirm they're open. | +| **08:00 - 17:00** | Do the pickups + deliveries. Use the app, not memory. | +| **12:00** | Lunch + call the office if anything is blocking. | +| **17:00** | Sync the app. Mark all today's items with their final status. Nothing in flight overnight. | -- [ ] **Open WFDASHDRIVER-APP — Today's Run.** Review the - supplier list, the order of pickups, and any urgent adds from - the Order Operator. -- [ ] **Check the truck:** fuel, waybills, blank GRN books, - packing tape, camera (phone), and a power bank. -- [ ] **Confirm any special instructions** on items (oversize, - fragile, supplier with restricted hours). -- [ ] **Phone the first supplier** if you haven't worked with - them in a while. Confirm they're open and the order is ready. +**Before you leave the truck**: every item from today is either +`collected`, `delivered`, or marked with a problem and a note. If +you don't, the office won't know what happened. -### Every working day (mid-day, 12:00 SAST — or at lunch) +## How to do the common stuff -- [ ] **Triage anything blocking you.** Phone the Order Operator - if a supplier can't fulfil today, if a customer can't receive, - or if you have a route problem. -- [ ] **Update the app** with any deviations (supplier short, - customer closed, etc). +### 1. At the supplier — happy path -### Every working day (end of shift, 17:00 SAST) +1. Tap the supplier row in the app. Tap **Navigate** (opens Maps). +2. At the supplier, tap **Confirm Collection**. +3. Scan or tap each line. Enter the qty received. +4. **Photograph the supplier's delivery note / invoice.** The app + attaches it automatically. +5. Tap **Submit**. Items flip to `collected`. -- [ ] **Mark all today's items with their final status.** No - item should be left "in flight" overnight. If a customer can't - receive, mark `assigned` (not `delivered`) and add a note. -- [ ] **Park any surplus / damage** with the Stock Controller. - Don't leave it on the truck. -- [ ] **Sync the app** — verify all photos uploaded, all - confirmations saved. Mobile data can be patchy; the sync - indicator in the top-right is the truth. +### 2. At the supplier — short / wrong / damaged -### Every Monday morning - -- [ ] **Review your KPIs** (see §4). Look at last week's - collection rate, on-time rate, and error rate. -- [ ] **Walk the truck.** Inventory the packing materials, - reusable crates, etc. Report any missing. - -### Every Friday afternoon - -- [ ] **Triage any open collections** — items still - `collection_pool` over the weekend are at risk of supplier - closure or customer cancellation. Flag to the Order Operator. - ---- - -## 2. Step-by-Step Procedures - -### 2.1 View your assigned run - -**When:** you arrive for your shift. - -1. Open WFDASHDRIVER-APP. -2. Tap **Today's Run**. You see a list grouped by supplier, in - the order the Order Operator suggested (usually optimised by - proximity — see `/collection-map`). -3. For each supplier, you see: - - Supplier name, address, contact. - - Items to collect, with qty + description. - - The PO number (so the supplier can match). - - Any special instructions. - -**If you need to re-order the run:** tap the supplier row, -**Re-order**. The Order Operator gets a notification. - -### 2.2 Navigate to a supplier - -**When:** you're at the first pickup. - -1. Tap the supplier row in the app. -2. Tap **Navigate**. The app opens your default maps app with - the supplier's address. -3. **If the address is wrong:** tap **Report Wrong Address** on - the supplier row. Don't try to find the supplier yourself — - the Order Operator needs to fix the master data. - -### 2.3 Confirm collection (the happy path) - -**When:** you arrive at the supplier and the order is ready. - -1. Tap **Confirm Collection** on the supplier row. -2. The app shows each line item. For each: - - **Scan the barcode** if the supplier labels have them - (faster, more accurate). The app matches against the - expected SKU. - - **Or tap to mark received** if no barcode. -3. If the supplier gave you a delivery note / invoice (paper - or PDF), tap **Attach**. Photograph it. The app stores it - with the standard `supplier_invoice_` filename prefix. -4. **Review the totals** at the bottom. The qty received should - match the qty ordered. -5. Tap **Submit**. The order items flip to `collected` and - the supplier invoice is attached to the PO. - -**Don't:** -- Don't submit without attaching the supplier's invoice. The - Order Operator needs it to do the diff/receive. -- Don't submit with the wrong qty. If the supplier shorted - you, see §3.1. - -### 2.4 Confirm a partial collection - -**When:** the supplier delivered less than ordered. - -1. Follow §2.3 up to step 3. -2. For each line, enter the **actual qty received** (not the - full qty). The app shows the difference. -3. In the **Note** field, write the reason ("supplier short by - 2, ETA TBD" or "supplier out of stock on this SKU"). -4. Continue. Tap **Submit**. -5. The items flip to `collected` with `qty_collected` set to - the partial amount. The Order Operator creates a - continuation order for the missing qty (see Order Operator - §2.6). - -### 2.5 Report a wrong / damaged item - -**When:** the supplier gave you the wrong item, or it's damaged. - -1. **Don't accept the goods** (refuse at the supplier). +1. **Don't accept** the bad goods. Talk to the supplier's rep. 2. In the app, on the line, tap **Report Problem**. -3. Choose: `wrong_item`, `damaged`, `expired`, or `other`. -4. **Take photos** — at least 2 (whole item + close-up of the - problem). The app attaches them with a `damage_` or - `wrong_item_` filename prefix. -5. Add a note explaining the situation. -6. **Get the supplier's rep to sign** the delivery note with - the discrepancy. Photograph the signed note. -7. Tap **Submit**. The item stays in `collection_pool` (not - flipped to `collected`). The Order Operator handles the - dispute. +3. Pick: `short` / `wrong_item` / `damaged` / `other`. +4. **Take 2+ photos** (whole item + close-up of the issue). +5. Get the supplier to **sign the delivery note** with the + discrepancy. Photograph the signed note. +6. **Add a note** explaining what happened. +7. Tap **Submit**. The item stays in `collection_pool` — the office + handles the dispute. -### 2.6 Deliver to a customer +### 3. At the customer — happy path -**When:** you're at the customer's site with their order. +1. Tap the order row. Tap **Confirm Delivery**. +2. Each line: tap **Delivered** (qty matches). +3. **Photograph the goods at the customer's site.** +4. **Get the customer's signature** on the app (or on the waybill + + photograph it). +5. Tap **Submit**. Items flip to `delivered`. -1. Tap the order row in the app. -2. Tap **Confirm Delivery**. -3. **For each line:** - - **Mark delivered** (qty matches). - - Or **partial delivery** (customer accepts less — see §3.4). - - Or **refused** (customer won't accept — see §3.5). -4. **Take a photo of the delivered goods** at the customer's - site (best practice for disputes). -5. **Get the customer's signature.** The app captures the - signature on screen. Alternatively, photograph the signed - waybill. -6. Tap **Submit**. Items flip to `delivered`. +### 4. At the customer — partial / refused / not home -**Don't:** -- Don't deliver without a signature / photo. A "delivered" - item without proof is a dispute magnet. +- **Customer wants less than you brought:** mark the line as + **Partial**, enter the qty they took, return the rest to the + office. Note why. +- **Customer refuses (damaged, wrong, "changed my mind"):** + photograph everything, get them to sign the refusal note, bring + it back. The office decides next. +- **Customer not on site:** wait 15 min, try to call, leave a card. + Mark `assigned` with a note "not on site" + the time you tried. + Bring the goods back. -### 2.7 Report a surplus +## Don'ts — the 3 things you must never do -**When:** the supplier gave you MORE than ordered (e.g. 12 -instead of 10). +1. **Never deliver without a photo + signature.** A "delivered" + item with no proof is a dispute magnet. The app needs both. +2. **Never submit a wrong / damaged pickup silently.** The + supplier will say "we never gave you damaged goods" and you'll + be on the hook. Photo + signed note at the supplier, every time. +3. **Never leave the app showing items "in flight" overnight.** + Mark everything as final before you clock out. If something's + stuck, the office can't help you at 7am Monday if the app says + you're still on the road. -1. Follow §2.3, but for the over-delivered qty, tap **Surplus**. -2. Enter the surplus qty (e.g. 2). -3. The system splits the line: customer qty (10) stays on - the order, surplus qty (2) is booked to stock. A - `stock_ledger` entry is written with - `reason: "purchase_surplus"`. -4. Continue with §2.3 for the rest. +## When to ask the office (the 4 most common situations) -### 2.8 Hand off a damaged delivery to stock +- **Supplier closed / order not ready.** Don't wait. Mark + `assigned`, note the time, re-order your run. Call the office + so they can reschedule the customer. +- **Wrong address.** Don't drive around. Tap **Report Wrong + Address** on the row. The office fixes the master data and + re-routes you. +- **Customer wants a discount to accept.** Don't promise anything. + Call the office. They'll decide. +- **Truck breakdown / accident.** Safety first. Then call the + office. The goods are secondary. -**When:** a customer refuses damaged goods on arrival. +## How you're measured (the 2 numbers that matter) -1. Take photos at the customer's site (see §2.5). -2. Get the customer's signature on the refusal note. -3. Bring the goods back to the office. **Don't** book them to - stock yourself — hand to the Stock Controller. They'll - inspect, decide return-to-vendor vs write-off, and book the - ledger entry. +1. **On-time delivery rate** — target: **> 95%** of items delivered + by the customer-promised time. (You hit the date.) +2. **Photo + signature proof rate** — target: **100%** of collections + and deliveries. (You have the evidence if it goes wrong.) ---- +Bonus number: **error rate** — target < 2% of line items have +damage / wrong-item reports. (You get it right first time.) -## 3. Decision Tree +## Who to ask -### 3.1 Supplier short / partial - -``` -Did the supplier know they were short (pre-called you)? -├── Yes → log partial qty, attach their note, submit. Order -│ Operator creates the continuation. -└── No - ├── Did they offer a back-order ETA? - │ ├── Yes (within 1 week) → take the partial, log the - │ │ ETA, Order Operator schedules the back-order. - │ └── No / vague → take what they have. Order Operator - │ phones the supplier for an ETA, may - │ source from a backup. - └── Is the partial critical (customer waiting, no - substitute)? - ├── Yes → phone the Order Operator NOW. They may - │ source from a backup supplier while you're - │ still at the supplier. - └── No → take the partial, log it, continue the run. -``` - -### 3.2 Supplier closed / not ready - -``` -Can you reach the supplier by phone? -├── Yes - ├── "Order not ready, will be in 2 hours" → wait if your - │ schedule allows. If not, re-order your run. - ├── "Order not ready, will be tomorrow" → re-order your - │ run, do the other suppliers first. Phone the Order - │ Operator if it means the customer won't get it today. - └── "Closed for the day" → re-order, mark `assigned` not - `collected`, phone the Order - Operator. -└── No → re-order your run. Phone the Order Operator. Don't - wait — every minute parked is a minute not delivering. -``` - -### 3.3 Wrong address (supplier or customer) - -``` -Is the address in the app matching what you see? -├── No (typo in the app, building really exists) -│ └── Tap **Report Wrong Address**. Don't try to find it -│ yourself — the master data needs fixing. Wait for -│ the Order Operator. -└── The address is right but you can't find the place - ├── Phone the contact on the row. - ├── If no answer, ask locals / security. - └── If still can't find → mark `assigned`, note "couldn't - locate", return to the office. Order Operator - resolves. -``` - -### 3.4 Customer wants partial delivery (not what we brought) - -``` -Is the customer a strategic account? -├── Yes → call the Order Operator before doing anything. Don't -│ make a delivery decision on the customer's porch. -└── No - ├── Will they accept what they need and leave the rest? - │ ├── Yes → deliver the partial, note the refused qty - │ │ and reason, return the rest to the office. - │ └── No (must be all or nothing) → mark `assigned`, - │ return the whole - │ delivery to the - │ office. Phone the - │ Order Operator. - └── Is the partial a "while you're here" ask (can they - take an extra qty)? - └── Phone the Order Operator. Don't unilaterally - add to the delivery. -``` - -### 3.5 Customer refuses (damaged, wrong, "changed my mind") - -``` -Why are they refusing? -├── Damaged → §2.5 / §2.8. Take photos, get signature, return -│ to office. -├── Wrong item → §2.5. Same process. -├── Customer changed mind (no defect) -│ ├── Customer pays restocking fee per policy? -│ │ ├── Yes → take the refusal, return to office, note -│ │ │ the fee on the order. -│ │ └── No / unclear → phone the Order Operator. Don't -│ │ bring the goods back without -│ │ confirming the restocking fee. -│ └── Customer wants to keep it but at a discount? -│ └── Phone the Order Operator. Don't unilaterally - discount. -└── Other - └── Phone the Order Operator. Get instruction. -``` - -### 3.6 Customer not on site when you arrive - -``` -Have you called ahead? -├── Yes (no answer) → wait 15 min. Try again. If still -│ nothing, leave a card / note, mark -│ `assigned` with "not on site", return -│ to office. Order Operator reschedules. -└── No → call now. If no answer, same as above. -``` - -### 3.7 Truck breakdown / accident - -``` -Is anyone hurt? -├── Yes → emergency services first. Then phone the Order -│ Operator. The goods are secondary. -└── No - ├── Is the truck driveable? - │ ├── Yes → drive to a safe spot. Phone the Order - │ │ Operator. They'll re-assign the rest of - │ │ your run to another driver. - │ └── No → road service. Phone the Order Operator. - │ They'll re-assign. - └── Can the goods be salvaged? → assess, take photos, - continue if possible. -``` - -### 3.8 Bad weather - -``` -Is the road to the customer / supplier safe? -├── No → don't go. Phone the Order Operator. They'll -│ re-schedule. Customer/supplier gets a weather -│ delay note on the order. -└── Yes → continue. Take extra care. -``` - ---- - -## 4. KPIs - -All KPIs measured per working day unless noted. - -| KPI | Target | How it's measured | -|---|---|---| -| **On-time collection rate** | > 90% of `assigned` items `collected` same day | `count(items where item_status=collected AND collected_at date = assigned_at date) / count(assigned items)` | -| **On-time delivery rate** | > 95% of `ready_delivery` items `delivered` within the customer-promised window | per-order `delivery_date` vs `delivered_at` | -| **Error rate (wrong item / damaged)** | < 2% of line items | `count(items with damage/wrong_item photo attached) / total items handled` | -| **Photo proof rate** | 100% of collections + deliveries have at least one photo | `count(photos) / count(items handled)` — should be 1:1 minimum | -| **Signature capture rate** | 100% of deliveries have a signature | same as above, for the signature field | -| **Surplus booking rate** | 100% of surpluses booked same day | `stock_ledger` entries with `reason="purchase_surplus"` per day vs supplier delivery notes flagged with surplus | -| **Refusal rate** | < 3% of attempted deliveries | `count(customer refusals) / count(delivery attempts)` | -| **Re-run rate** | < 5% of items needing a second visit | `count(items with 2+ collection attempts)` | -| **App sync success** | 100% (all confirmations uploaded by EOD) | `count(items confirmed offline more than 4h) / total items confirmed` — should be zero | -| **Cost per collection** | tracked, target stable | total driver cost (incl truck, fuel) / number of collections per month | - -### How KPIs feed into your review - -- **Daily** (end of shift): the app's home screen shows your - day's KPI snapshot. If any are red, the supervisor gets an - alert. -- **Weekly** (Monday standup): one-pager with the week's KPIs. - On-time rate and error rate are the headline numbers. -- **Monthly** (1st of month): 1-page review with the - supervisor. **Photo proof rate and signature capture rate - must be 100%** — these are the dispute-defence evidence. - Anything below is a process failure. -- **Quarterly**: top performers on on-time + zero-error are - eligible for the senior-driver tier (they can take the - high-value / strategic-account runs). - -### What the system does NOT measure (yet) - -- Customer satisfaction (no NPS yet) -- Fuel efficiency per route -- Truck wear-and-tear - -If a KPI is unfair, raise it. The supervisor can adjust -targets, but the metric stays. +- **The office phone number** — for anything that needs a decision + (refusal, discount, wrong address, breakdown). +- **Your supervisor** — for any pattern (same supplier always short, + same customer always refusing). +- **When in doubt, call.** Don't guess from the cab. diff --git a/docs/sops/order-operator.md b/docs/sops/order-operator.md index 0ce28e9..fe56c8a 100644 --- a/docs/sops/order-operator.md +++ b/docs/sops/order-operator.md @@ -1,479 +1,119 @@ -# Order Operator — SOP +# Order Operator — Quickstart -**Owns:** the order lifecycle, from quote → order → delivered → -invoiced. Creates purchase orders, drives the `order_items` state -machine, hands off to invoicing. +**One-line job:** turn accepted quotes into delivered goods. + +## What you do + +The customer said yes. You: + +- Create the order from the quote. +- Order the goods from the supplier (a Purchase Order). +- Track them through collection, packing, and delivery. +- Hand off to invoicing once delivered. + +You're the bridge between "they said yes" and "they have it". -**Tools:** `/orders`, `/orders/[id]`, `/orders/new`, `/collection-pool`, -`/collection-map`, `/stock`, `/stock-orders`, dashboard widgets. - -**Handoffs:** -- **From Quote Operator** → customer accepted a quote. -- **To Collection Driver** → PO sent, item moves to - `collection_pool`. -- **To Invoicing** → item delivered, ready to invoice. -- **To Approver** → escalated pricing / margin / customer issues. - -**Note:** the order lifecycle is **item-level**, not order-level. -`orders.status` does not exist as a field. The "Complete" badge on -the order header is derived from `every item is delivered`. This is -in `src/lib/server/order-completion.ts`. Get familiar with it. - ---- - -## 1. Daily / Weekly Checklist - -### Every working day (start of shift, 08:00 SAST) - -- [ ] **Open `/orders` — New tab.** Every `pending` item should have - a clear next action: either waiting for pricing review, waiting - for a PO, or waiting for a supplier reply. -- [ ] **Open `/orders` — Processing tab.** Verify all `processed` - items have a PO generated. Items stuck in `processed` for >1 - working day = process bug. Fix or escalate. -- [ ] **Open `/orders` — Collection Pool tab.** If you see - `ready_for_collection: true` items with no driver assigned, the - collection driver hasn't picked them up — phone them. -- [ ] **Open dashboard → "Follow-ups Due".** Action anything from - you (POs waiting on supplier reply, etc). -- [ ] **Check `/api/v1/purchase-orders/stale`** (or the dashboard - widget). POs with no supplier reply for >3 working days must - be phoned. - -### Every working day (mid-day, 13:00 SAST) - -- [ ] **Open `/orders` — Collected tab.** Verify `stock_verified` - and `picking` items are progressing. Anything stuck in - `stock_verified` for >2 hours = stock discrepancy. Resolve or - escalate to the Stock Controller. -- [ ] **Check `/stock?filter=low`** — if any of your orders used a - stock item that's now critical, the customer needs a heads-up. - -### Every working day (end of shift, 17:00 SAST) - -- [ ] **Review every order that changed today.** Make sure all - status transitions are correct. A `delivered` item that - shouldn't be `delivered` is a customer-facing disaster. -- [ ] **Triage `/orders` — Delivered tab.** Any `delivered` items - older than 1 working day that aren't yet `ready_invoice`? Push - them through. - -### Every Monday morning - -- [ ] **Review your KPIs** (see §4). Pay attention to the items - that drifted last week. -- [ ] **Triage stuck orders** — anything in `pending` or - `processed` for >5 working days is a zombie. Either ship, - cancel, or escalate. - -### Every Friday afternoon - -- [ ] **Close the week.** Make sure no orders have items in - limbo states over the weekend. `assigned` to a driver who - doesn't work weekends = uncollected Monday morning. -- [ ] **Invoicing handoff**: confirm the invoice clerk has - received everything they need for the week's `ready_invoice` - items. They'll print Monday morning. - ---- - -## 2. Step-by-Step Procedures - -### 2.1 Create an order from a quote - -**When:** a customer accepts a quote (you got the call, or the -customer replied accepting). - -1. Open the quote detail. -2. Click **Create Order**. The system creates: - - One new `orders` record with number `ORD-YYYY-MM-NNN`. - - One `order_items` row per `quote_items` row, linked back - via `order_items.quote_item`. -3. The quote status does NOT change automatically. The quote - stays in `approved` (or `emailed` + your acceptance note). -4. The order starts in **New** (`pending`) tab. - -**Don't:** -- Don't try to create an order from the same quote twice. The - system returns the existing order, not a duplicate. -- Don't edit the quote after creating the order — the items - are now in `order_items`, edits to `quote_items` don't - propagate. - -### 2.2 Create a walk-in order (no quote) - -**When:** a customer walks in / phones with an order that wasn't -quoted. - -1. Open `/orders/new`. -2. Pick or create the customer (and company). -3. Add items manually. Each item needs at minimum `description` - + `quantity` + `unit_price`. Cost is optional at creation - (defaults to 0 — fix later from the PO). -4. **Save.** Order lands in `pending`. -5. Pricing: see §2.3. - -### 2.3 Price and process a `pending` item - -**When:** an order item lands in `pending` and you have a quote -to source from. - -1. Open the order detail. -2. Click the line item. Set `cost_price` (from the quote or - catalogue) and `selling_price`. -3. Click **Mark Processed** (status flips to `processed`). -4. **Decide next path:** - - **In stock?** → click **Book from Stock**. The item flips - to `stock_verified` and the stock qty decrements via the - ledger. (See §2.7 for stock flow.) - - **Not in stock, need to buy?** → generate a PO. See §2.4. - -### 2.4 Generate a purchase order (PO) - -**When:** an item needs to be ordered from a supplier. - -1. On the order detail, click **Generate PO**. The system groups - items by supplier and creates one PO per supplier. -2. **Open the PO.** Verify: - - Items + quantities match the order. - - Cost prices are correct (from the quote or a fresh - supplier call). - - Delivery date is realistic. -3. Click **Send PO**. The email modal opens. Same shape as - sending a quote (§2.4 in the Quote Operator SOP). -4. n8n sends the email. The supplier reply monitor updates - `supplier_replied_at` when the supplier replies. -5. The order item flips to `po_generated`. - -**Don't:** -- Don't generate a PO without verifying the cost. Once sent, the - PO is the contract — wrong cost = wrong margin. - -### 2.5 Receive a supplier invoice and book the goods - -**When:** a supplier invoice / delivery note arrives (PDF in -email, or paper). - -1. Identify the PO. Look for the "Your Ref" / "Order No" on - the document. It'll be `PO-YYYY-MM-NNN` (customer PO) or - `PO{yy}{6d-seq}STK` (stock PO). -2. **Diff first** — call the system to compare the invoice - against the PO. Read the verdict: - - `ready_to_receive` → continue to step 3. - - Anything else (`cost_mismatch`, `qty_mismatch`, - `supplier_mismatch`, `wrong_status`, `po_not_found`, - `unmatched_*`) → **stop and report** to the supervisor. - Don't force it through. -3. **Commit the receive** — for stock POs this books into - `stock_items.current_qty` and writes a `stock_ledger` row. - For customer POs, the order items flip to `collected` and - the supplier invoice number is stamped. -4. The system attaches the invoice PDF to the PO. - -**Mode:** -- `mode: "invoice"` — full invoice received (price + qty). -- `mode: "grn"` — only a delivery note (qty only, no price - check). Use this when the supplier sends the packing slip - before the invoice. - -**Surplus / Short:** -- If the supplier delivered MORE than ordered (surplus), the - PO diff flags it. Confirm with the Stock Controller; they - may book the surplus to stock (§2.8). -- If LESS than ordered (short), the system records the - received qty. Create a continuation order for the missing - qty (§2.6). - -### 2.6 Split an order into a continuation - -**When:** some lines are stuck (waiting on a back-ordered part), -the rest can ship. Or: a walk-in order has lines added later -and the original is past `pending`. - -1. Open the order detail. -2. Verify which lines are `pending`. **Only `pending` lines can - be moved** (others refuse silently to avoid orphaning a PO - or driver assignment). -3. Click **Split Order**. Pick the `pending` lines to move. -4. A new `ORD-YYYY-MM-NNN` order is created with the moved - lines. The parent keeps its number; the moved lines get a - breadcrumb in `notes`. -5. Process the new continuation order normally. - -### 2.7 Book from stock - -**When:** a `processed` line item has a `stock_item_id` set and -qty available. - -1. On the order detail, click **Book from Stock**. -2. The system calls `/api/v1/stock/fulfill` — decrements - `stock_items.current_qty` and writes a `stock_ledger` row - with `reason: "customer_fulfillment"`. -3. The order item flips to `stock_verified` → `picking` - (or directly to `collected` if the picking step is - bypassed for direct-from-stock orders). - -**If the stock qty is short** (someone else booked it first), -the system refuses. Either: -- Cancel the line (status → `cancelled`). -- Wait for restock and re-try later. -- Source from the supplier instead (revert to `processed` → - generate PO). - -### 2.8 Book a surplus - -**When:** the supplier delivered more than ordered (e.g. 12 -instead of 10), and the customer only needs 10. - -1. On the order item, click **Book Surplus**. -2. Enter the surplus qty (e.g. 2). -3. The system: - - Splits the order line: customer qty (10) stays on the - order, surplus qty (2) goes to stock. - - Writes a `stock_ledger` row with `reason: "purchase_surplus"`. - - Sets `order_items.surplus_qty` and `stock_booked_from` on - the line. -4. The stock item's `current_qty` increments. - -### 2.9 Confirm collection (driver flow) - -**When:** the driver has picked up the goods and the collection -is being confirmed back at the office (or via the mobile app). - -1. On the order detail, find the item. It should be in - `collection_pool` or `assigned`. -2. Click **Confirm Collected**. Enter the qty received (the - `quantity_collected` field is the amount to ADD, not the - new total). -3. For partial deliveries: clamp to `received_qty - qty_collected` - (≥0). Add a `note` explaining the partial. -4. The item flips to `collected` (full) or stays in - `collection_pool` (partial) with the `qty_collected` - updated. - -**Actor field is required.** The system stamps the actor on -every audit entry. If you don't pass it, the audit trail -loses you. - -### 2.10 Pick, pack, deliver - -**When:** items are `collected` and ready to be packed for -delivery. - -1. `collected` → `stock_verified` (verify qty against the - shelf count). -2. `stock_verified` → `picking` (operator starts pulling). -3. `picking` → `ready_delivery` (packed, on the truck). -4. `ready_delivery` → `delivered` (signed for at the customer). - -**Each step needs an operator action.** Don't bulk-flip — -the audit trail per step is the customer's dispute evidence. - -### 2.11 Mark an order delivered (header-level) - -**When:** every item on the order is `delivered` (or -short-circuits via the eligible set — see tool doc). - -1. Open the order detail. -2. Click **Mark Delivered**. The system flips every eligible - item to `delivered` and stamps `delivered_at` + - `delivered_date`. -3. **Does NOT** flip `order.invoiced`. The Invoicing Clerk does - that. - -If the tool reports `data.itemsFailed` (some items refused), -inspect them. The most common reason is an item in -`pending`/`processed`/`cancelled` that wasn't eligible. - -### 2.12 Hand off to invoicing - -**When:** items are `delivered` and the customer has been -notified. - -1. On the order detail, click **Mark Ready to Invoice** (the - button shows when items are eligible). The items flip to - `ready_invoice`. -2. **Don't** call `mark_invoiced` here — that's the - Invoicing Clerk's job, after they generate the QB invoice - or delivery note. - ---- - -## 3. Decision Tree - -### 3.1 The customer wants to cancel after the order is placed - -``` -Has the supplier been PO'd yet (item is in `processed` or later)? -├── No → cancel the line(s), no cost incurred. Easy. -└── Yes - ├── Has the supplier confirmed the order (replied to PO)? - │ ├── No → phone the supplier NOW. Most will cancel - │ │ without charge if you catch it pre-confirm. - │ └── Yes - │ ├── Is the goods in transit / collected? - │ │ ├── No → call the supplier, negotiate a cancel. - │ │ │ Some will charge a handling fee. - │ │ └── Yes → goods will arrive. Three options: - │ │ a) Accept the goods, book to stock, - │ │ re-sell later. - │ │ b) Refuse delivery (record the refusal - │ │ on the order notes, supervisor - │ │ decides whether to charge the customer - │ │ a restocking fee). - │ │ c) Ask the supplier to return-to-vendor. - │ │ Their policy, not yours. - │ └── Surplus / different qty → see §3.3 - └── Customer wants to change items, not cancel → see §3.2 -``` - -### 3.2 Customer wants to change items after the order is placed - -``` -Is the order still all-`pending`? -├── Yes → use the order editor to swap the items. -└── No (some lines already `processed` or later) - ├── Only the `pending` lines need to change → split off the - │ pending lines, edit on the parent, leave the rest. - └── Some `processed` lines need to change - ├── Has the supplier PO'd been sent? - │ ├── No → revert the line to `pending`, edit. - │ └── Yes → phone the supplier first, then decide: - │ either cancel the PO + re-issue, or ship - │ the original and create a separate order - │ for the replacement. - └── The change affects pricing → update the order's - selling price, document the customer agreement in - `order.notes`. -``` - -### 3.3 Supplier delivered wrong / damaged / short - -``` -What's the discrepancy? -├── Wrong qty (short) -│ ├── Receive what arrived (use the `quantity_collected` -│ │ to-add semantics). -│ ├── Mark the missing qty as back-ordered. Create a -│ │ continuation order for it (§2.6) when you can re-supply. -│ └── Phone the supplier, log the short in `order.notes`. -├── Wrong qty (over — surplus) -│ └── See §2.8. Book the surplus to stock, keep the order qty -│ at the customer's need. -├── Wrong item -│ ├── Refuse delivery. Phone the supplier for a return-to- -│ │ vendor or a credit note. -│ └── If customer will accept the wrong item at a discount → -│ call the supervisor, don't unilaterally discount. -├── Damaged -│ ├── Refuse delivery. Take photos, attach to the PO via the -│ │ supplier_invoice_ prefix on `order_item_attachements`. -│ └── If customer accepts damaged at a discount → supervisor -│ call, photos on file, credit note from supplier. -└── All correct - └── Confirm the receive (§2.5). -``` - -### 3.4 Stock item is short when I try to book - -``` -Is the short a rounding / measurement error? -├── Yes → recheck the stock count. The Stock Controller may -│ have missed an entry. -└── No - ├── Is the item critical (low stock) → flagged in - │ `/stock?filter=critical`? - │ ├── Yes → the order is more important than the stock - │ │ refill. Book it. File a stock-order for - │ │ restock. - │ └── No → cancel the stock booking, source from the - │ supplier via PO. - └── Has the stock item been double-booked? → check - `stock_ledger` for recent bookings. The Stock - Controller can release the duplicate. -``` - -### 3.5 Supplier hasn't replied to a PO in 3+ days - -``` -Did the PO send successfully (check `email_message_id`)? -├── No (mail_failed) → retry the send, verify recipient address -└── Yes - ├── Phone the supplier. Most POs are lost in spam. - ├── If no answer, email the sales rep directly (check - │ `/suppliers` for the contact). - └── After 7 working days with no reply → escalate to - supervisor. We may need to find a backup supplier. -``` - -### 3.6 Customer wants a delivery date that's not realistic - -``` -Can the supplier meet it (have they confirmed)? -├── Yes → set the order's `delivery_date` accordingly. -└── No - ├── Is the customer willing to wait? - │ ├── Yes → set a realistic date, log the agreement in - │ │ `order.notes`. Don't over-promise. - │ └── No → escalate to supervisor. We may need to split - │ sources, expedite (at a cost), or partial- - │ ship. - └── Is there stock for at least part of the order? → split - the order (§2.6). Ship what's in stock, back-order - the rest. -``` - -### 3.7 An order has items in mixed states (some delivered, some pending) - -``` -Is the customer OK with partial delivery? -├── Yes → ship what's ready. The delivered items go to -│ invoicing. The pending items stay on the order -│ until they're ready. -└── No (customer needs everything together) → leave the order - open. Don't flip anything to delivered until everything - is on the truck. The customer is OK with the wait. -``` - ---- - -## 4. KPIs - -All KPIs measured in working days. - -| KPI | Target | How it's measured | -|---|---|---| -| **Order creation turnaround (quote accepted → order exists)** | < 30 min median | `orders.created - quotes.approved_at` | -| **`pending` → `processed` turnaround** | < 4 working hours | median `processed_at - created` on `order_items` | -| **PO send turnaround (processed → po_generated)** | < 1 working day | median `po_generated_at - processed_at` | -| **Supplier reply time** | < 2 working days median | `supplier_replied_at - email_message_id_sent` | -| **PO stale rate** | < 5% of open POs | POs without reply > 3 working days / open POs | -| **Collection turnaround (po_generated → collected)** | < 3 working days | median `collected_at - po_generated_at` | -| **`collected` → `delivered` turnaround** | < 1 working day | median `delivered_at - collected_at` | -| **Order completion rate (delivered within 5 working days of `pending`)** | > 80% | orders with all items `delivered` within 5 wd / orders created | -| **PO cost-mismatch rate** | < 3% of receives | count of `cost_mismatch` verdicts on diff / total receives | -| **Surplus booking rate** | 100% of surpluses booked same day | `stock_ledger` entries with `reason="purchase_surplus"` per day vs supplier delivery notes | -| **Split order rate** | < 10% of orders | `orders` with `notes` containing a split breadcrumb / total orders | -| **Cancellation rate** | < 5% of `pending` lines | `order_items.item_status="cancelled"` / total `order_items` created | - -### How KPIs feed into your review - -- **Weekly** (Monday standup): one-pager with the operator's - KPIs. Stale POs, cost mismatches, and stuck pending items are - the supervisor's first questions. -- **Monthly** (1st of month): 1-page review with the operator. - Surplus booking rate and PO cost-mismatch rate are weighted - double — getting them wrong is a money leak. -- **Quarterly**: top performers on completion rate + on-time - delivery are eligible for the "trusted buyer" tier — they can - raise POs up to a higher value without supervisor sign-off. - -### What the system does NOT measure (yet) - -- Customer satisfaction post-delivery (no feedback loop yet) -- Damage rate per supplier (only the PO-level diff verdict is - captured, not the cause) -- Driver utilisation (that's the collection driver's KPI) - -If a KPI is unfair, raise it. The supervisor can adjust -targets, but the metric stays. +## Your day + +| Time | What to do | +|---|---| +| **08:00** | Open `/orders?tab=New`. Anything new from yesterday? Move it forward. | +| **08:30** | Open `/orders?tab=Processing`. Every line should have a PO. Flag any stuck > 1 day. | +| **12:00** | Glance at `/orders?tab=Collected`. Anything stuck in `stock_verified`? Investigate. | +| **17:00** | Anything you changed today — sanity check. Don't leave surprises for Monday. | + +**Three times a day**, scan the dashboard. The order-trend chart +should show lines moving right (New → Processing → Collection → +Collected → Delivered). If anything's piling up in one tab, that's +where to focus. + +## How to do the common stuff + +### 1. Create an order from a quote + +1. Open the quote (status `approved` or `emailed`). +2. Click **Create Order**. One new order, one row per quote line. +3. Order lands in **New** tab with everything `pending`. +4. The quote status does **not** change. Leave it alone. + +### 2. Process a `pending` line + +1. Open the order detail, click the line. +2. Set `cost_price` (from the quote) and `selling_price` (auto-derived + from cost × markup). +3. Click **Mark Processed**. Status → `processed`. +4. Decide next: in stock? → **Book from Stock**. Not in stock? → + **Generate PO** (next step). + +### 3. Generate a Purchase Order (PO) + +1. Click **Generate PO**. One PO per supplier. +2. Open the PO. Verify items, quantities, costs, delivery date. +3. Click **Send PO**. Same email modal as quotes — check recipient, + attach any terms docs, hit send. +4. Status → `po_generated`. The order item waits for the supplier to + reply / ship. + +### 4. Receive a supplier invoice (when the email or paper arrives) + +1. Find the PO number on the invoice (top right usually — "Your Ref"). +2. The system will diff the invoice against the PO and tell you if + it matches. **If it says "ready to receive"** → click **Confirm + Receive**. Done. +3. **If it says anything else** (cost mismatch, qty mismatch, etc.) → + **stop and ask the supervisor.** Don't force it. + +### 5. Mark delivered (once the goods are with the customer) + +1. Confirm the order has all its items in `delivered`. The header + will show a green "Complete" badge. +2. Click **Mark Delivered** on the header. (Idempotent — safe to + click twice.) +3. Then click **Mark Ready to Invoice**. Items flip to `ready_invoice`. + The Invoicing Clerk takes it from here. + +## Don'ts — the 3 things you must never do + +1. **Never generate a PO without verifying the cost.** Once sent, it's + the contract. Wrong cost = wrong margin. +2. **Never "force through" a receive diff that doesn't say + `ready_to_receive`.** If the diff says cost mismatch, qty mismatch, + supplier mismatch — call the supervisor. Don't bypass. +3. **Never mark an order delivered before every line is `delivered`.** + Use the per-line `Mark Delivered` if needed. Don't bulk-flip the + header to "make it look done". + +## When to ask the supervisor (the 4 most common situations) + +- **Supplier hasn't replied to a PO in 3+ working days.** Phone them + first. If no answer after 2 attempts, escalate. +- **Customer wants to cancel after the PO is sent.** Depends on + whether the supplier has confirmed / shipped. Call the supervisor — + there are 3 options and they pick. +- **Supplier delivered the wrong / damaged goods.** Refuse at the + supplier (if still there) or take photos at receipt. **Don't + accept damaged goods silently.** The supervisor decides credit / + return / write-off. +- **Stock item is short when you try to book.** Either the Stock + Controller can re-check the count, or we source from the supplier + instead. Phone, don't poke. + +## How you're measured (the 2 numbers that matter) + +1. **Order completion time** — target: **> 80% of orders delivered + within 5 working days** of the order being created. (From "yes" to + "they have it".) +2. **PO stale rate** — target: **< 5% of open POs** without a supplier + reply for > 3 working days. (Catch the silent ones early.) + +Bonus number the supervisor watches: **PO cost-mismatch rate** — +target < 3% of receives. If yours is higher, your cost entries are +probably sloppy. + +## Who to ask + +- **Anything about a specific order, a PO, a customer, a supplier** + → supervisor. +- **Anything broken in the system** → supervisor. +- **Anything you're not sure about** → supervisor. Better to ask + than guess. diff --git a/docs/sops/quote-operator.md b/docs/sops/quote-operator.md index ce89c6c..79cbcc7 100644 --- a/docs/sops/quote-operator.md +++ b/docs/sops/quote-operator.md @@ -1,527 +1,112 @@ -# Quote Operator — SOP - -**Owns:** the full quote pipeline — inbound RFQ intake, item-level -pricing (sprint or manual), approval submission, sending, and -follow-up. The sprint queue is one part of the pricing piece; the -operator owns the whole pipeline from "new quote" to "customer -replied". - -**Tools:** `/quotes`, `/quotes/[id]`, `/sprint`, `/quotes/approvals`, -`/quotes/email-monitor`, dashboard widgets. - -**Entry points** (where a quote can come from): -- **Inbound email** (n8n ingests RFQ → quote lands in `new`) -- **Walk-in** (operator types "make me a quote for John" — no email) -- **Template** (re-using a previous quote as a scaffold) -- **Sprint** (item-level pricing queue — items from the above that - need a price from you) - -**Handoffs:** -- **From** Email Intake (n8n) → quote lands in `new` status. -- **To Approver** → when status flips to `approval_pending`. -- **To Order Operator** → when customer accepts (quote becomes an order). -- **To Customer** → outbound email + inbound reply tracking. - ---- - -## 1. Daily / Weekly Checklist - -### Every working day (start of shift, 08:30 SAST) - -- [ ] **Open `/quotes?tab=new`** — every quote that came in via - email overnight must have an owner. Either claim it (you'll - work it), or pass to a colleague. Quotes older than 1 working - day in `new` are a process bug. -- [ ] **Open `/sprint`** — clear the "UNCLAIMED" filter. Look for - items you can claim in your strongest categories first. (If - the inbound-email intake has already pre-priced most items, - the sprint queue may be small today — that's the goal.) -- [ ] **Open `/quotes/email-monitor`** — scan for any failed - sends from the last 24h. If any, **retry or escalate** (see - Decision Tree §3.5). -- [ ] **Open `/quotes?tab=claimed`** — pick up any of your items - whose 30-min claim is about to expire. -- [ ] **Open dashboard → "Follow-ups Due"** — every quote you own - with a `follow_up_due` date of today or earlier must get a - follow-up action today (call, email, or set a new - `follow_up_due`). - -### Every working day (end of shift, 17:00 SAST) - -- [ ] **Release any sprint items you can't finish** — the 30-min claim - expires automatically, but it's polite to leave a `pricing_notes` - breadcrumb on what's been tried. -- [ ] **Confirm every quote you sent today** has moved out of `sending` - — if any are stuck in `sending` for >5 min, see §3.5. -- [ ] **Set `follow_up_due`** for every quote you sent without a reply - expected inside 5 working days. Default: today + 5 working days. - -### Every Monday morning - -- [ ] **Review your KPIs** (see §4). Look for any that are trending - down vs last week. Note them in `#quote-ops` with a one-line - reason. -- [ ] **Clear the `/quotes/approvals` queue** if you have items there - from Friday. Approvals older than 1 working day are a process bug — - surface to the approver. - -### Every Friday afternoon - -- [ ] **Close out the week** — every quote in `claim` by you with no - activity for 3+ days must either be priced, sent, or `problem`-ed. - Don't leave zombies for Monday. - ---- - -## 2. Step-by-Step Procedures - -The procedures are ordered by the quote lifecycle: - -| # | Procedure | Entry point | -|---|---|---| -| 2.1 | Process an inbound email quote | RFQ email arrives via n8n | -| 2.2 | Create a walk-in quote | Phone / face-to-face / WhatsApp | -| 2.3 | Create a quote from template | Repeat customer, similar items | -| 2.4 | Claim a sprint item | Unpriced item from §2.1, §2.2, or §2.3 | -| 2.5 | Price a line item | Inside the line editor | -| 2.6 | Submit a quote for approval | Every line priced | -| 2.7 | Send the quote to the customer | Approver has approved | -| 2.8 | Follow up on a sent quote | `follow_up_due` reached | -| 2.9 | Partial approval / split a quote | Customer accepts some, rejects others | - -### 2.1 Process an inbound email quote - -**When:** a customer sends an RFQ email. n8n ingests it, the AI -extracts items, and a new quote appears in `/quotes?tab=new` with -status `new`. You'll see a `📨` icon on the dashboard's "New -Quotes" widget. - -1. **Open `/quotes?tab=new`.** Find the new quote. -2. **Sanity-check the AI extraction** (open the quote detail): - - **Customer** — did the AI match it to a known customer? If - it landed on `Unknown Sender`, see Decision Tree §3.7. - - **Company** — same check. Wrong company = wrong letterhead - on the PDF. - - **Items** — are the descriptions readable? Are the - quantities right? If a line is missing or wrong, edit it. - - **Attachments** — is the customer's original RFQ attached? - The buyer needs to be able to compare our quote against - their ask. -3. **Check `pricing_source` on each line:** - - `ai` or `catalogue` → already priced. Verify the price - against the AI's reasoning (if shown). If sane, move on. - - `manual` or empty → unpriced. These lines will land in the - sprint queue (see §2.4). -4. **Claim the quote** by clicking **Claim** on the header (or - it'll auto-claim on first edit). Status flips to `claimed`. -5. **Triage unpriced lines:** if the sprint queue has unpriced - lines from this quote, claim them in §2.4. -6. **If everything is priced**, jump to §2.6 (submit for - approval). - -**Don't:** -- Don't submit a quote with `Unknown Sender` — the customer - needs to exist first (see §3.7). -- Don't edit the AI's prices without a `pricing_notes` - breadcrumb explaining why. - -### 2.2 Create a walk-in quote - -**When:** a customer calls / walks in / WhatsApps with an RFQ -that didn't come through email. No inbound message to extract -from — you build the quote from scratch. - -1. **Open `/quotes/new` (or click "+ New Quote" on the quotes - list).** -2. **Pick or create the customer** in the customer picker. - - If they exist, pick them. The letterhead, rep, and email - template default from the customer record. - - If they don't exist, type the name. The system creates a - draft customer row with that name — the supervisor - completes the rest of the fields later. -3. **Pick the company** (which letterhead). CNL vs ISHERWOOD - etc — match what the customer is used to. -4. **Set the validity** (default 30 days). -5. **Add the description** — the free-text subject of the RFQ - (e.g. "M16 hex bolts, grade 8.8, zinc, 2000 units"). Goes - on the PDF header. -6. **Add items** in the line editor: - - One row per SKU / spec combo the customer asked for. - - Each row needs at minimum `description`. Add - `quantity` and `unit`. - - If you know the price (e.g. the customer named a SKU that - is in the catalogue), set `cost_price` and `markup` — - `selling_price` is auto-derived. - - If you don't know the price, leave it blank and the lines - will land in the sprint queue (§2.4). -7. **Save.** The quote lands in `new` status. -8. **If items are unpriced**, go to §2.4. If everything is - priced, go to §2.6. - -**Don't:** -- Don't pick a company / letterhead you don't recognise — - ask the supervisor. -- Don't save with `quantity = 0` — that's a placeholder, not - an order. If the customer is uncertain about qty, write - "TBD" in the description and price the line anyway, or - mark the line as "Re-quote later" (split it out — §2.9). - -### 2.3 Create a quote from a template - -**When:** a customer asks for something we've quoted before with -the same line items / similar scope. Re-using a template saves -the line-item re-entry. - -1. **Open `/quotes?tab=templates`.** Pick the template that - matches. -2. Click **Create from Template**. A new quote is spawned with: - - Same customer, company, letterhead. - - Same line items (descriptions + quantities). - - Status: `new`. - - Prices reset (templates don't carry live pricing — you - re-price from the current catalogue). -3. **Update `description`** to reflect the new RFQ (date, - customer reference, anything that's changed since the - template was made). -4. **Update quantities** if the customer wants different - amounts. -5. **Re-price** the lines from the current catalogue (or leave - them for sprint if the SKU / supplier has changed). -6. **Continue from §2.6** (or §2.4 if items are unpriced). - -**Don't:** -- Don't send a template quote without verifying the prices - against today's catalogue. Supplier cost changes, and - template-stored prices go stale fast. -- Don't re-use a template for a new customer without - confirming the markup policy applies. Strategic accounts - may have different markups. - -### 2.4 Claim a sprint item - -**When:** an item lands in the sprint queue and you have capacity. - -1. Open `/sprint`. The "UNCLAIMED" tab shows available items. -2. **Prefer items you can resolve in <30 min** — known SKUs, items - with attachments, or items in your strongest category. -3. Click **Claim**. The 30-min TTL starts. The item moves to your - `Claimed` filter. -4. **Within 30 min:** price the item (see §2.5), or **release** by - simply not saving (the claim auto-expires). - -**Don't:** -- Don't claim more than 3 items at once. The 30-min TTL is per item; - the bottleneck is your attention. -- Don't claim items you'll need a supplier call for. Sprint is for - pricing from known data, not for sourcing. - -### 2.5 Price a line item - -**When:** you've claimed an item and you're at the line editor. - -1. **Read the description.** If the AI provided a match (look for - `matched_price_list_id` filled in), use it. -2. **Decide pricing source:** - - **Catalogue** (`pricing_source: "catalogue"`) — the matched - catalogue row has a valid `cost_price` and `markup`. Use it. - - **AI** (`pricing_source: "ai"`) — the AI agent pre-filled a - suggested price. **Verify against the catalogue or supplier - list** before saving. Never save an AI price blind. - - **Manual** (`pricing_source: "manual"`) — no match. You need - to find a price yourself (see §3.1). -3. **Set `cost_price` and `selling_price`.** Selling price is - auto-derived from `cost_price × (1 + markup/100)` if you set the - markup — pass `selling_price` explicitly only when the catalogue - has a stored selling price you want to preserve. -4. **Set `pricing_source`** to the source you actually used. -5. **Add a `pricing_notes` breadcrumb** (e.g. "matched JEI catalogue - row, ZAR excl VAT, 30% markup per August 2026 policy"). -6. **Click Save.** The item drops off the sprint queue. The linked - `quote_item` flips to `user_edited: true`. - -**Don't:** -- Don't save with `cost_price = 0` and `selling_price = 0`. That's - a sign you didn't find a price — see §3.1. -- Don't override a catalogue price without a `pricing_notes` - reason. - -### 2.6 Send a quote for approval - -**When:** every line on a quote has `selling_price > 0`. - -1. Open the quote detail (`/quotes/[id]`). -2. Click **Submit for Approval**. The status flips to - `approval_pending`. Idempotent — if it's already in - `approval_pending`, nothing changes. -3. If the submit returns `unpriced_items` with `offendingItemIds`: - - Open each offending line. - - Either price it (see §2.5) or move it to a "Re-quote" group - (see §2.9). -4. **Wait for the approver.** The quote is now in their queue at - `/quotes/approvals`. - -**Don't:** -- Don't bypass submit by calling **Send Email** directly. The - pre-send validator will reject with `invalid_status` for `new` - quotes. - -### 2.7 Send the quote to the customer - -**When:** the approver has flipped the quote to `approved`. - -1. Open the quote detail. -2. Click **Send Email**. The email modal opens. -3. **Verify the recipient(s):** - - `To` — the customer's primary email. - - `CC` — usually the customer rep (`rep` on the customer row). - - `BCC` — internal archive address (already pre-filled). -4. **Edit subject/body if needed.** Default subject is `Quote - {number} — {description}`. Don't remove the quote number — the - inbound reply tracker uses it. -5. **Attachment picker:** the quote PDF is auto-included. Add any - supporting docs (datasheet, authorisation letter) the customer - asked for. -6. Click **Send**. Status flips to `sending` → n8n takes over the - SMTP send → callback records `emailed` or `mail_failed`. -7. **Set `follow_up_due`** to today + 5 working days. - -**Don't:** -- Don't send without verifying the recipient. Wrong customer = lost - quote. -- Don't include internal-only files. The customer sees everything - in the attachment picker. - -### 2.8 Follow up on a sent quote - -**When:** a sent quote has a `follow_up_due` date of today or earlier. - -1. Open the quote detail. Check the **Reply** chip — green if a - reply is logged, red if not. -2. **If no reply:** - - **Phone first** (faster than email). Log the call in - `quote.notes`. - - If no answer, send a follow-up email: keep it short, reference - the original quote number, ask one direct question - (e.g. "Are the JEI prices workable?"). - - Update `follow_up_due` to today + 5 working days. -3. **If replied:** open the reply in `/quotes/email-monitor`, read - it, then **either:** - - **Customer accepted** → hand off to the Order Operator - (create an order from quote). Mark the quote `approved` if not - already. - - **Customer wants changes** → see §3.3. - - **Customer rejected** → flip to `rejected`. Move on. - -### 2.9 Partial approval / split a quote - -**When:** a customer says "yes to lines 1, 2, 5; no to 3, 4". - -1. Open the quote. -2. Click **Partial Approve**. For each line, mark `approved` or - `rejected`. Add a `note` for each rejection. -3. Click **Split Quote** (in the action bar). A new quote is - created with the rejected lines. -4. **Re-price the new quote** if needed (the prices inherit but - sometimes a re-quote justifies a different markup). -5. **Send the new quote** following §2.7. - ---- - -## 3. Decision Tree - -### 3.1 I can't find a price for an item - -``` -Is the item in the catalogue (search /pricelist)? -├── Yes → use the catalogue row, set pricing_source="catalogue" -└── No - ├── Is there a recent price_list upload from the supplier? - │ ├── Yes → open /pricelist, find the upload, check if the - │ │ item is in the extracted list (sometimes AI misses - │ │ lines) - │ └── No → call the supplier - └── Supplier not reachable / no price available - → Set the line to "Re-quote" group (separate it from the - rest of the quote). Don't hold up the rest of the quote. - → Or, if the customer is waiting urgently, price the item - at your best guess with a high margin (40%+) and add a - pricing_notes disclaimer. -``` - -### 3.2 The AI price looks wrong (too low / too high) - -``` -Is the catalogue price within 10% of the AI price? -├── Yes → accept the AI price, set pricing_source="ai" -└── No - ├── Low (< catalogue - 10%) → reject, use catalogue price, - │ add pricing_notes "AI underpriced - │ by X%, used catalogue per policy" - └── High (> catalogue + 10%) → reject, use catalogue price, - escalate to approver if the - customer has a standing - discount you don't know about -``` - -### 3.3 Customer wants changes to a sent quote - -``` -Are the changes only pricing/markup? -├── Yes → re-price the affected lines, save, regenerate the PDF, -│ send a "Revised quote" email referencing the original -│ number. -└── No (changes to scope, qty, items) - ├── Customer wants fewer lines → Partial Approve (§2.9) + Split - ├── Customer wants more lines → Add the lines, re-price, send - │ as "Revised quote" - └── Customer wants different items entirely → Clone the quote - as a new RFQ, - mark old as - `rejected` -``` - -### 3.4 The 30-min sprint claim is about to expire - -``` -Have you made progress (found a price, just not saved)? -├── Yes → save your work, even if partial. The claim can be re-claimed. -└── No - ├── Item is too hard → release (just don't save). It returns - │ to the pool. - └── Item needs supplier input → add to your "needs supplier" - personal list, release the claim. -``` - -### 3.5 Email send failed - -``` -What's the failure reason (check n8n log)? -├── SMTP / network error -│ └── Click Retry on the quote. If it fails again, escalate to -│ IT. The quote stays in `mail_failed` until retried. -├── "Recipient rejected" -│ └── Check the email address. If typo, fix and resend. If -│ genuine reject (mailbox full, domain dead), phone the -│ customer for a new address. -├── "PDF render failed" -│ └── Escalate to the Order Operator / dev. Don't retry — the -│ PDF will fail again until the bug is fixed. -└── Unknown - └── Capture the full n8n log. Escalate with the log attached. -``` - -### 3.6 Customer escalates (calls you directly to push for a discount) - -``` -Is the customer a strategic account (rep = senior staff)? -├── Yes → call the approver before quoting anything. Don't -│ promise a discount over the phone. -└── No - ├── Discount < 5% → apply it. Log in pricing_notes with the - │ reason. - ├── Discount 5-15% → apply it, but flag the approver in the - │ quote notes. - └── Discount > 15% → don't apply. Tell the customer it's - above your authority, escalate to the - approver. Don't bluff. -``` - -### 3.7 An inbound email looks like a new RFQ but lands in `Unknown Sender` - -``` -Is the sender a known customer (search /customers)? -├── Yes → reassign the quote to the right customer via the -│ customer picker on the quote detail. -└── No - ├── Is the sender domain one we recognise (e.g. a known - │ customer at a new branch)? - │ └── Yes → ask the supervisor to create the customer, then - │ reassign. - └── No → it's a real new customer. Hand to the supervisor to - create the customer record. Don't price the quote - until the customer exists — the foreign key chain - breaks otherwise. -``` - -### 3.8 The AI extracted the quote wrong - -``` -What's wrong? -├── Wrong company / letterhead -│ └── Re-pick the company on the quote header. The PDF will -│ re-render with the right letterhead on the next send. -├── Wrong items (missing lines, extra lines, wrong description) -│ ├── Missing line → add it manually, price it. -│ ├── Extra line (AI hallucinated) → delete it. Log in -│ │ quote.notes: "removed -│ │ hallucinated line X". -│ └── Wrong description → edit the description. The SKU -│ and matched_price_list_id may -│ need re-linking. -├── Wrong quantities -│ └── Cross-check the original RFQ email attachment -│ (quote.attachments). If the customer typed qty=200 -│ and the AI read qty=20, fix it. If the AI hallucinated -│ the qty, delete and re-add. -└── Wrong unit (each / pack / metre) - └── Edit per line. Note: until PB adds the `unit` column - (see `quote-items` AGENTS.md), this is stored in the - description, not a structured field. -``` - -### 3.9 A walk-in customer has no account yet - -``` -Do you have a contact name and a way to follow up? -├── Yes (phone, email, name) → create the quote. The system -│ creates a draft customer with the name you typed; the -│ supervisor completes the address / VAT / contact fields -│ later. Don't block the quote on missing customer data. -└── No (anonymous, cash-on-delivery) → ask the supervisor - before creating. We may need a different flow for - cash customers (e.g. a "Cash" pseudo-customer). -``` - ---- - -## 4. KPIs - -All KPIs are measured in **working days** (Mon–Fri, SAST). The -dashboard's "Quote Pipeline" widget is the source of truth for most -of these. - -| KPI | Target | How it's measured | -|---|---|---| -| **Sprint claim → priced turnaround** | < 30 min median | `now - claim_expires_at` for resolved items, where resolution = `pricing_source` set + `user_edited: true` | -| **Quote first-touch turnaround** | < 2 working days | `now - created` for quotes whose status has left `new` | -| **Quote send turnaround (priced → emailed)** | < 4 working hours after approval | `emailed_at - approved_at` | -| **Follow-up rate** | 100% of overdue quotes actioned same day | count of quotes with `follow_up_due <= today AND last_action < today` (should be zero) | -| **Approval rate** | > 70% of `approval_pending` quotes approved within 1 working day | `approved_at - submitted_at` | -| **Sprint abandonment rate** | < 10% of claims released without save | `released_claims / total_claims` | -| **Mail-failure rate** | < 2% of sent emails | `mail_failed / (mail_failed + emailed)` over rolling 30 days | -| **AI pricing override rate** | < 15% of `pricing_source="ai"` lines changed before save | operator save of a different price on an AI-pre-priced line | -| **Win rate** | > 35% of `emailed` quotes become orders | `orders.quote -> quotes.id` join, over rolling 90 days | -| **Quote value captured** | tracked per rep, monthly | sum of `selling_price` on quotes in `emailed`/`approved`/`complete` status, attributed to `claimed_by` | - -### How KPIs feed into your review - -- **Weekly** (Monday standup): the supervisor pulls a one-pager - showing each quote operator's KPIs. Red flags = items below - target for 2+ weeks. -- **Monthly** (1st of month): the supervisor writes a 1-page review - per operator covering the four KPIs they're weakest on and - agreeing on a 30-day improvement action. -- **Quarterly**: above-target on all KPIs for 2 consecutive months - = eligible for the markup-authority bump (you can approve your - own quotes up to 10% discount without supervisor sign-off). - -### What the system does NOT measure (yet) - -- Customer satisfaction (no NPS in WFDASH yet) -- Cross-sell effectiveness (no line-item upsell tracking) -- Catalogue coverage of your claims (you might be claiming a lot - of items that the catalogue doesn't cover — that's a catalogue - problem, not a you problem, but it's worth flagging) - -If you think a KPI is unfair or misleading, raise it in `#quote-ops`. -The supervisor can adjust targets, but the metric stays. +# Quote Operator — Quickstart + +**One-line job:** turn customer requests into priced quotes, fast. + +## What you do + +A customer wants something. We need to tell them "yes, we can supply +it, and here's the price". You: + +- Pick up new quotes that come in by email or phone. +- Find the right price (from the catalogue, an AI suggestion, or a + supplier list). +- Get the quote approved. +- Send it to the customer. +- Follow up if they don't reply. + +## Your day + +| Time | What to do | +|---|---| +| **08:30** | Open `/quotes?tab=new`. Anything new from overnight? Pick it up. | +| **09:00** | Open `/sprint`. Claim 2-3 items you can price fast. (30-min timer per item.) | +| **10:00, 13:00, 16:00** | Same as 09:00. Keep the sprint queue small. | +| **12:00** | Lunch. | +| **17:00** | Set `follow_up_due` on every quote you sent today (5 working days from now). | + +**Three times a day**, glance at `/dashboard` — is the "Quote Pipeline" +doughnut getting stuck in `new` or `claim`? If yes, the queue is +backing up. Pick up more. + +## How to do the common stuff + +### 1. Price a line item (the most common task — you'll do this 50× a day) + +1. Open the line. +2. Is there a **matched catalogue row** (look for the catalogue panel + on the line)? Use its price. Set `pricing_source: "catalogue"`. +3. Not matched? Search `/pricelist` by description or SKU. If you + find it, copy the price across. Set `pricing_source: "catalogue"`. +4. Still nothing? Look at recent supplier lists in `/pricelist` + (the "Raw Uploads" tab). If the supplier is one we work with, + use their price. Set `pricing_source: "manual"`. +5. **Cannot find a price anywhere** → flag the line for the + supervisor. Don't guess. + +### 2. Send a quote (once everything is priced) + +1. Click **Submit for Approval**. Status → `approval_pending`. +2. Wait for the supervisor. (You'll see it move off the queue.) +3. Approved? Click **Send Email**. +4. Check the recipient. Add the customer rep to CC if you know them. +5. The PDF is already attached. Hit **Send**. +6. **Set `follow_up_due`** to today + 5 working days. Don't forget. + +### 3. Follow up on a sent quote + +1. Open the quote. Is there a reply logged? (Green chip on the row.) +2. No reply? **Phone the customer first** — faster than email. +3. Log the call in the quote's notes. +4. Set a new `follow_up_due` date. Try again in 5 working days. + +### 4. Inbound email quote (the common one) + +1. n8n already created the quote in `/quotes?tab=new`. Open it. +2. Check: right **customer**? right **company/letterhead**? items + look right? +3. Items with `pricing_source: "ai"` are pre-priced — verify, save. +4. Items with no price landed in `/sprint` — go price them. + +## Don'ts — the 3 things you must never do + +1. **Never send a quote with a line that has `selling_price = 0`.** + The system will refuse the send. If you can't price a line, ask + the supervisor. +2. **Never override an AI price without checking the catalogue.** + The AI is right ~85% of the time. If you change it, write a + `pricing_notes` line saying why (e.g. "catalogue price used, + AI was 12% low"). +3. **Never leave a sprint claim to expire with no work.** Save + what you have (even partial), or release the claim by walking + away. Don't ghost your own claims. + +## When to ask the supervisor (the 4 most common situations) + +- **AI price is way off (more than 10% from catalogue).** Use the + catalogue price and log the why. Don't agonise. +- **Customer wants a discount.** < 5% — apply it, log the reason. + 5-15% — apply it, flag the approver in the quote notes. + > 15% — don't apply; tell the customer it's above your authority. +- **Email send failed.** Click **Retry** once. If it fails again, + check the address (typo? bounced?). If still failing, escalate. +- **Customer is unknown / wrong.** The quote landed on "Unknown + Sender". Don't price it. Hand to the supervisor to create the + customer first. + +## How you're measured (the 2 numbers that matter) + +1. **Time to first touch on a new quote** — target: **< 2 hours** + during business hours. (How fast did you pick it up?) +2. **Win rate** — target: **> 30%** of quotes you send become + orders. (Out of every 10 quotes you sent, how many got accepted?) + +The supervisor pulls these for you every Monday. If either is red +for 2+ weeks, you'll get a 1-on-1. + +## Who to ask + +- **Anything about a specific quote, a customer, a price** → supervisor. +- **Anything broken in the system (button missing, error message, weird + data)** → supervisor, who'll loop in the dev if needed. +- **Anything you're not sure about** → supervisor. Better to ask + than guess. diff --git a/docs/superpowers/plans/2026-09-03-pos-network-printer.md b/docs/superpowers/plans/2026-09-03-pos-network-printer.md new file mode 100644 index 0000000..e4f110b --- /dev/null +++ b/docs/superpowers/plans/2026-09-03-pos-network-printer.md @@ -0,0 +1,831 @@ +# Network (Ethernet) Thermal Printer Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add support for printing sale receipts and delivery notes to a network (Ethernet) Epson TM-T20II printer from the `public/pos` PWA, alongside the existing Web Bluetooth path, working on both Android and iOS. + +**Architecture:** The printer exposes Epson's ePOS-Print XML API over **HTTPS** (confirmed live against the shop's TM-T20II at `192.168.1.33` — see verification notes below), with an open CORS header (`Access-Control-Allow-Origin: *`). Because both the PWA and the printer are HTTPS, calling the printer directly from the browser hits neither mixed-content blocking nor Safari's lack of Web Bluetooth support — no native wrapper is needed. A new `public/pos/epos-print.js` module builds the ePOS-Print XML (mirroring the existing `buildEscPos()` byte-builder in `receipt.html`) and POSTs it with `fetch()`. `receipt.html` gains a printer-type setting (Bluetooth vs. Network) persisted in `localStorage`, and `printThermal()`/`printDelivery()` branch on it. + +**Tech Stack:** Vanilla JS (no build step — this is a static PWA), `fetch`, `` for logo rasterization, Epson ePOS-Print XML. + +**Verified findings this plan relies on (from live testing against the shop printer):** +- `http://192.168.1.33/` 301-redirects to `https://192.168.1.33/` (self-signed cert). +- `POST https://192.168.1.33/cgi-bin/epos/service.cgi?devid=local_printer&timeout=10000` with an ePOS-Print SOAP XML body returns `HTTP 200` and ``, and a real slip printed with text, item list, subtotal/VAT/total, and the WFDASH logo image. +- Raw ESC/POS bytes also print successfully over plain TCP to port 9100 (confirms the byte-building logic already in `buildEscPos()` is sound, though this plan uses the HTTPS path since browsers can't open raw sockets). +- The response's `success="true"` was observed once without a physical printout (cause not fully isolated — possibly a transient state on the printer). Task 10 below re-verifies each XML feature individually against the real printer before calling this done. +- Each till device (iOS or Android) must visit `https://192.168.1.33` once in its browser and accept the self-signed certificate warning before `fetch()` calls from the PWA will succeed silently. + +--- + +### Task 1: Fix the pre-existing `const d` redeclaration bug + +**Files:** +- Modify: `public/pos/receipt.html:535-591` (the `buildEscPos` function) + +This is a blocking bug unrelated to network printing, but it sits inside the exact function this plan extends, and it currently breaks the entire inline ` + +``` + +Change to: + +```html + + + +``` + +- [ ] **Step 4: Add the settings toggle/save functions and init call** + +Inside the main inline `` block now starts and ends after your edits.) + +- [ ] **Step 6: Manual browser check** + +Open `receipt.html` in a browser (with a `pos.lastSale` value in `sessionStorage`, or just check that the page doesn't error in the console). Click the new "Printer: Bluetooth" row — the settings form should expand. Switch the select to "Network (Ethernet)", type `192.168.1.33`, click "Save printer settings" — the row should update to read "Printer: Network (192.168.1.33)" and the value should persist across a page reload (check via `localStorage.getItem('pos.printerSettings')` in the console). + +- [ ] **Step 7: Commit** + +```bash +git add public/pos/receipt.html +git commit -m "feat(pos): add printer type/IP settings UI to receipt page" +``` + +--- + +### Task 7: Wire `printThermal()` to the network path + +**Files:** +- Modify: `public/pos/receipt.html` + +- [ ] **Step 1: Rename the existing Bluetooth flow** + +Find (around line 487): + +```js +async function printThermal(){ + const btn = document.getElementById('printBtn'); +``` + +Change the function name only: + +```js +async function printThermalBluetooth(){ + const btn = document.getElementById('printBtn'); +``` + +(Leave the rest of that function's body untouched.) + +- [ ] **Step 2: Add the network variant and the dispatcher** + +Immediately before the (now renamed) `printThermalBluetooth` function, add: + +```js +async function printThermal(){ + const settings = window.EposPrint.getSettings(); + if(settings.type === 'network') return printThermalNetwork(settings); + return printThermalBluetooth(); +} + +async function printThermalNetwork(settings){ + const btn = document.getElementById('printBtn'); + const status = document.getElementById('statusText'); + const errBox = document.getElementById('status'); + errBox.classList.remove('err'); + btn.disabled = true; + if(!settings.ip){ + status.textContent = 'Set the printer IP address in Printer settings first.'; + errBox.classList.add('err'); + btn.disabled = false; + return; + } + status.textContent = 'Sending to ' + settings.ip + '…'; + try{ + const logo = await window.EposPrint.imageToRaster('/wfdash-logo.png', 384).catch(()=>null); + const xml = window.EposPrint.buildEposXml(sale, logo); + await window.EposPrint.printViaNetwork(settings.ip, xml); + status.textContent = 'Printed · ' + settings.ip; + }catch(e){ + status.textContent = 'Print failed: ' + e.message; + errBox.classList.add('err'); + }finally{ + btn.disabled = false; + } +} +``` + +- [ ] **Step 3: Verify the script still parses** + +Run: `node --check <(sed -n '319,760p' public/pos/receipt.html | sed '1s/.*