# Rep Performance Report — Data Source & Provenance > **Audience:** anyone who asks where the numbers come from — a manager, an > auditor, a customer-facing colleague. > **Scope:** `/reports/rep-performance` (summary, per-rep detail, team > comparison) and the PDF download. > **Author context:** WFDash is the operator app (Next.js + PocketBase). This doc > describes where every figure on the report originates, so the report can be > defended without reading the code. --- ## TL;DR The report is **two separate things stitched together**, and it matters that you can tell them apart: | | **A — Activity** | **B — Time log** | |---|---|---| | What it is | Quotes, orders, delivery notes, customers, companies | "Where the time went", visit map, hours per place | | Source | **WFDASH itself** (PocketBase) | **Hardwired vehicle tracker** (GPS unit in the truck) | | Entered by | Reps working in the app; some quotes arrive by email automation | A CSV export from the tracker portal, imported by a manager | | On a phone? | No — server-side database | No — a fixed vehicle unit, not a personal phone | | Pages in the PDF | 1, 3, 4, 5, 6 | 2 (and the map behind it) | **The charts anyone is likely to challenge — quotes loaded, orders placed, customers visited, companies — are all Section A, and every one of them is a read-only count of rows that already exist in WFDASH.** Nothing on those pages is typed in by a person for reporting purposes; the report only counts work the reps already did in the normal course of business. The only non-WFDASH input is Section B, and it is confined to two clearly labelled pages. --- ## 1. Topology — where the data physically comes from ``` WHO ENTERS THE DATA │ ┌───────────┴───────────┐ ▼ ▼ Reps working in the app n8n email intake quotes · orders quotes from RFQ mail │ │ │ order_items │ │ (cancellation check) │ │ │ └───────────┬───────────┘ ▼ ┌───────────────────┐ │ delivery_notes │ raised & completed └─────────┬─────────┘ ▼ ══════════════════════════════════════════════════════ WFDASH → POCKETBASE (system of record) quotes · quotes_items · orders · order_items delivery_notes · customers · companies · users ══════════════════════════════════════════════════════ │ │ read-only counts — nothing is created ▼ ┌───────────────────┐ │ REP PERFORMANCE │ │ REPORT │ (this document) └─────────┬─────────┘ ▼ screen · PDF download ``` --- ## 2. Section A — Activity metrics (system of record) Every row below is a **count of existing WFDASH records** inside the selected date window. The report creates nothing, edits nothing, and nobody enters a "report number" anywhere. | Report column | Collection | Field counted | Rule | |---|---|---|---| | **Quotes loaded (QL)** | `quotes` | `created` | Any quote created in the window. | | **Quotes done (QD)** | `quotes` | `created` + `status` | Quote **and** `claimed_by` set **and** status is `emailed`, `approved` or `sent`. | | **Orders placed (OP)** | `orders` | `created` | Order in the window, **minus** any order with at least one `order_items` row marked `cancelled`. | | **Delivery notes (DN)** | `delivery_notes` | `created` | DN in the window, excluding `cancelled`. | | **Orders received (OR)** | `delivery_notes` | `created` + `status` | DN in the window with status `completed`. | | **Customers / Companies** | `customers`, `companies` | — | Derived from the rows above; customers roll up to the company on the customer record. | | **Total** | — | — | Plain sum of the five metrics above. Not weighted, not scored. | **Code of record:** `src/lib/server/rep-performance-service.ts` (the counting loop is `loadActivity`). The `QD` status list and the order-rep resolver are imported from `src/lib/server/boss-dashboard-service.ts`, so this report and the boss dashboard cannot drift apart. ### Date filtering Every collection is filtered on the same generated window: ``` created >= " 00:00:00.000Z" && created < " 00:00:00.000Z" ``` The upper bound is pushed out one day so the last day is **inclusive** — a common off-by-one that would otherwise silently drop activity on the final day of the month. --- ## 3. Who gets the credit (attribution) This is the part most worth explaining, because "whose number is it?" is the usual challenge. | Metric | Credited to | |---|---| | Quotes loaded, Delivery notes, Orders received | The rep who **owns the customer** (`customers.rep`) | | Quotes done | The rep who **claimed the quote** (`quotes.claimed_by`) | | Orders placed | The rep on the **order itself** (`orders.rep`) — *not* the customer's owner | Orders deliberately follow the order's own rep field, matching the boss dashboard. A rep who is covering someone else's customers shows their orders under their own name, which is the behaviour the business expects. ### ⚠️ Known quirk worth knowing about `orders.rep` is a **free-text field** that over the years holds *either* a PocketBase user id *or* a person's name. The report resolves both: exact user id first, then a case-insensitive name match. This is not a cosmetic detail — for Tiaan in September 2026 it is the difference between 1 order and 27: | How you look it up | Result | |---|---| | `orders.rep = ""` | 1 | | `orders.rep = "Tiaan"` | 26 | | **Report total** | **27** | If someone queries the database themselves and gets a small number, this is why. --- ## 4. Section B — The time log (vehicle tracker, not WFDASH) Stated plainly: **the "where the time went" page is not sourced from WFDASH.** It is sourced from a GPS tracking unit fitted to the vehicle. It is kept in a separate section of the report for exactly this reason, and it should be presented differently from Section A if it is challenged. ### 4.1 How it gets in 1. A manager downloads a **"Travel History" CSV** from the tracker's web portal. 2. They upload it on the report page (*Import time log*). 3. The file is parsed and written into PocketBase as `rep_trip_legs` — **one row per tracker interval, with no interpretation applied at import time.** ``` ══════ SEPARATE PROVENANCE — NOT WFDASH ══════ ┌────────────────────────────────┐ hardwired unit in the truck, │ Vehicle GPS tracker │ logs position every few minutes │ (vehicle registered in │ │ users.tracker_vehicle_id) │ └────────────────────────────────┘ │ manager downloads the CSV from the tracker │ portal, then uploads it on the report page ▼ ┌────────────────────────────────┐ one row per tracker interval, │ rep_trip_legs │ stored in PocketBase as a copy │ (raw, un-interpreted) │ └────────────────────────────────┘ │ read-time derivation: │ work-hours clip · 250 m clustering · │ company matching · operator labels ▼ ┌────────────────────────────────┐ PDF page 2 + the visit map │ "Where the time went" │ └────────────────────────────────┘ ``` Storing raw and deriving at read time is deliberate: if company coordinates or weekend rules are corrected tomorrow, historical reports improve immediately with **no re-import**. Which vehicle belongs to which rep is recorded on the user (`users.tracker_vehicle_id`, e.g. `MW13TCGP`), so a CSV cannot be silently attached to the wrong person. Re-importing a file **replaces only the dates that file covers**, so importing October does not destroy September. ### 4.2 What is derived (and therefore arguable) | Derived value | Rule | Why it exists | |---|---|---| | Work hours | 07:30–17:00, local (SAST, UTC+2, no DST) | One global default across the fleet. | | Weekends | **Excluded** | A truck is often out or parked at weekends for reasons unrelated to selling. One flag flips this back on. | | Noise filter | Stops under **5 minutes** dropped | Traffic lights, gate checks, GPS drift. Applied at *period* level, so a place survives or drops whole rather than losing a 4-minute sliver. | | Clustering | Points within **250 m** of each other become one "place" | Keeps neighbouring mine gates apart while absorbing GPS jitter when parked. | | Place name | Matched against **`companies.coordinates` within 750 m** | `customers.coordinates` is deliberately **ignored** — those are residential addresses, and reporting "visited: a customer's house" would be worse than no name. | | Unnamed places | Operator-assigned labels, stored in `place_overrides`, matched by proximity (300 m) | Most company records have no coordinates yet, so an unnamed place is a placeholder the manager can name on the map. | **Consequence to be honest about:** a place only carries a company name if the company has usable coordinates **or** someone has mapped it manually. Unmapped places show as a raw location, not as a company. The time log is a strong signal on *how the time was spent*; it is only as good as the mapping for *naming* it. --- ## 5. How to verify any number yourself The report is read-only, so anyone with admin access can reproduce its figures directly from the database. Worked example — **Tiaan, September 2026**, the exact period in the sample PDF: | Report says | Raw PocketBase query | Result | |---|---|---| | Orders placed = 27 | `orders` where `rep` = id **or** `"Tiaan"`, created in Sept | 1 + 26 = **27** ✓ | | Delivery notes = 3 | `delivery_notes` where `customer.rep` = Tiaan, `status != "cancelled"` | **3** ✓ | | Orders received = 2 | same, `status == "completed"` | **2** ✓ | | Time log rows = 692 | `rep_trip_legs` where `rep` = Tiaan | **692** ✓ | The same check works in the WFDASH admin UI, or via the PocketBase admin API with a filter such as: ``` customer.rep = "54uj39i4juqkh9x" && created >= "2026-09-01 00:00:00.000Z" && created < "2026-10-01 00:00:00.000Z" && status != "cancelled" ``` Every metric is a plain count of rows returned by a filter like this one. There is no spreadsheet step, no manual tally, and no intermediate cache to explain. --- ## 6. Which reps appear The rep list is **not hardcoded**. Someone appears on the report if they own a customer, claimed a quote in the period, or carry the `REP` user type. This means: - a new rep shows up without a code deploy; - a rep with no activity in the period drops off naturally; - accounts matching `test` / `qa` / `demo` are filtered out automatically. The summary page can additionally hide low-activity reps via the **Min total** control; this is a display filter only and does not change any underlying total. --- ## 7. Honest limitations Stated up front so nobody is caught out: 1. **"Orders received" is approximated by completed delivery notes.** The natural source would be `order_items.delivered_at`, but that field is empty on every current record, so a completed DN is used instead. If delivery-note completion is not always entered promptly, this figure runs behind reality. 2. **Order cancellation is coarse.** An order with *any* cancelled line is excluded entirely, not just the cancelled portion. 3. **The time log's place names depend on manual mapping** (section 4.2). 4. **Work hours are a single fleet-wide default**, not per-rep shifts. 5. **The report reflects the selected period only.** A quote loaded in August and sent in September counts its *completed* event in September. --- ## 8. Where the code lives | Concern | File | |---|---| | Counting, attribution, company rollups | `src/lib/server/rep-performance-service.ts` | | Shared status rules (kept in step with boss dashboard) | `src/lib/server/boss-dashboard-service.ts` | | Tracker CSV parsing | `src/lib/server/travel-history-parser.ts` | | Time log import + read-time derivation | `src/lib/server/rep-time-log-service.ts` | | PDF layout (A4 portrait, 6 pages) | `src/lib/pdf/rep-performance-detail-document.tsx` | | API surface | `src/app/api/v1/reports/rep-performance/**` | | Schema for tracker data | `scripts/migrations/2026-09-28-create-rep-trip-legs.ts` | | Schema for operator place labels | `scripts/migrations/2026-09-28-create-place-overrides.ts` | Both the counting service and the CSV parser are covered by unit tests (`rep-performance-service.test.ts`, `travel-history-parser.test.ts`), so the rules in this document are enforced by the build rather than by memory. --- ## 9. One-paragraph answer > "The report counts records in our own WFDASH database. Quotes loaded, orders > placed and delivery notes are rows the reps created in the app while doing > their jobs — we just count them. Customers and companies are read straight > off those rows. The one exception is the 'where the time went' page, which > comes from the GPS unit fitted to the truck, exported as a CSV and imported > manually; it's a separate page for that reason. You can reproduce any number > in section A yourself by filtering the database on the rep and the date > range — the queries are in section 5."