- Added an audit trail feature that logs every create, update, and delete action on delivery notes and orders, capturing detailed changes per field. - Introduced the `AuditTimeline` component to display the history of changes for delivery notes and orders, enhancing traceability. - Updated the `page.tsx` files for delivery notes and orders to fetch and display audit logs, ensuring users can view the history of modifications. - Enhanced the `package.json` with a new migration script for setting up the audit log functionality. This feature improves accountability and transparency in the management of delivery notes and orders.
14 KiB
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 >= "<from> 00:00:00.000Z" && created < "<to+1 day> 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 = "<user id>" |
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
- A manager downloads a "Travel History" CSV from the tracker's web portal.
- They upload it on the report page (Import time log).
- 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/demoare 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:
- "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. - Order cancellation is coarse. An order with any cancelled line is excluded entirely, not just the cancelled portion.
- The time log's place names depend on manual mapping (section 4.2).
- Work hours are a single fleet-wide default, not per-rep shifts.
- 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."