WFDASH-WEBAPP/docs/rep-performance-data-source.md
Dave 7555196166 feat(audit): implement audit trail for delivery notes and orders
- 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.
2026-09-30 01:48:44 +02:00

296 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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