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

14 KiB
Raw Blame History

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