WFDASH-WEBAPP/.agents/skills/wfdash-rep-visit-batch-pin/SKILL.md
Dave 482c1adf25 chore(agents): add wfdash-rep-visit-batch-pin skill
Bulk-pin a list of customers to a PocketBase site from a rep's visit log
(typically Gary's driving-mode WhatsApp pins). Encodes the PB quirks we
hit during the Khuseleka batch:

- PB filter field on customers is 'customer' (not 'name'); fuzzy match
  with customer~"X"
- coordinates is a geoPoint on both customers and rep_visits
  ({"lat": X, "lon": Y} GeoJSON, not a flat string)
- site_location is free-text on customers and does NOT exist on
  rep_visits (PB silently drops unknown keys on writes)
- position_source is a select with ["tracker", "manual"]; manual is
  the backfill marker
- PB admin auth email lives in .env.local (PB_ADMIN_EMAIL), never
  hardcode
- local pb_schema-2.json snapshot is stale (missing site_location) —
  live PB is source of truth
- SAST = UTC+2; convert WhatsApp timestamps before stamping
- single-letter driving-mode typos → pin the canonical PB record,
  ask before assuming
- zero-hit names → skip by default; ask before creating new
  customers

Same folder as wfdash-giellie; available to any agent that loads the
project's skills folder.
2026-09-20 23:49:58 +02:00

11 KiB

name description
wfdash-rep-visit-batch-pin Bulk-pin a list of customers to a PocketBase site from a rep's visit log (typically Gary's driving-mode WhatsApp pins). Use whenever the user provides a coordinate (lat/lon or Google Maps URL), a site name, and a list of person names, and wants each person pinned to that location with a `rep_visits` record. Handles PB admin auth via `.env.local`, fuzzy match against the `customers` collection, presents ambiguity for unclear/duplicate/zero-hit names, then writes the pins in one batch. Covers the `customer~"X"` PB filter gotcha, the SAST→UTC timestamp conversion, the live-vs-snapshot schema drift warning, and the known skipped-name patterns. This is NOT the agent/MCP layer — for that, load `wfdash-giellie` instead.

WFDASH rep-visit batch pin

You are a small data-entry agent. Given a coordinate, site name, and a list of person names from a rep's visit log (typically Gary's WhatsApp driving-mode pins), pin each person to that location in PocketBase and create a rep_visits record per person.

This is not the same scope as wfdash-giellie. Giellie is the agent/MCP layer (Bearer token, /api/mcp). This skill talks directly to PocketBase admin because Giellie doesn't expose a bulk-pin-customers tool and won't gain one soon.


When to load this skill

Load when the user does any of:

  • "pin these names to [site]" / "backfill this WhatsApp batch"
  • Hands you a coordinate + place name + list of names
  • Pastes a raw WhatsApp export with a Google Maps link followed by names (most common case — parse it yourself)
  • Says "do the Cementation / Khuseleka / Doornkop thing" referring to a previous batch in this conversation

If the user wants to interact via the agent/MCP layer instead (POST /api/mcp or /api/v1/*), load wfdash-giellie — not this skill.


Required inputs (ask for what's missing)

  1. Coordinates — either:
    • Google Maps URL: https://maps.google.com/?q=-26.217859,27.791794 (extract the q=)
    • Or raw: -26.217859, 27.791794 (decimal degrees, lat first, lon second)
  2. Site name — free-text string, e.g. Khuseleka, Cementation, Doornkop, Elandsrand / Kusasalethu
  3. Person names — one per line, numbered, or pasted as a WhatsApp message block
  4. (optional) Per-message timestamps — use when each name was its own message; default to the single shared timestamp of the names message
  5. (optional) Rep ID — defaults to Gary krpyec9lblra0qz

If the user pastes a raw WhatsApp export (timestamps + Location URL + names), parse out coordinates, site, and names yourself. That's the dominant pattern.


Workflow

1. Auth to PocketBase

Never hardcode the PB admin email. Always read .env.local in the WFDASH repo. Past bug: assistant assumed david@wfsales.co.za when the env file said david.els1@outlook.com.

# From WFDASH repo root
PB_EMAIL=$(grep -E '^PB_ADMIN_EMAIL=' .env.local | cut -d= -f2-)
PB_PASSWORD=$(grep -E '^PB_ADMIN_PASSWORD=' .env.local | cut -d= -f2-)
PB_URL=$(grep -E '^POCKETBASE_URL=' .env.local | cut -d= -f2-)

POST to ${PB_URL}/api/collections/_superusers/auth-with-password:

{ "identity": "<email>", "password": "<password>" }

Cache the returned token. It's valid for the session. If you get a 401 mid-batch, re-auth.

2. Verify live schema (don't trust the local snapshot)

The local snapshot at pb-exports/pb_schema-2.json may be stale. As of last sync it was missing site_location (which exists in live PB). Before writing, GET /api/collections/customers/records?perPage=1 and confirm the fields you intend to write are actually present in the live record. If site_location is gone from live PB too, drop it and ask the user how they want site tracked now.

3. Site check

site_location is a free-text select on customers. PB silently accepts new string values. Before writing, GET /api/collections/customers/records?perPage=200&fields=site_location&filter=site_location!='' to enumerate existing values and confirm whether the new site is genuinely new. Report site growth at the end of the batch.

4. Fuzzy-match each name against customers

Critical gotchas:

  • Field is customer, NOT name. The PB filter operator is ~ (contains), not LIKE.
    • ✅ GET /api/collections/customers/records?filter=customer~"Corrie"&perPage=10
    • ❌ ?filter=name~"Corrie" (returns 0 hits — the bug from the Khuseleka batch)
  • For each name, try exact match first, then substring if no exact.
  • If multiple hits, list all candidates with their IDs.
  • If zero hits, report zero and present skip/create options.
  • If one hit, mark ✓ clear.
  • Single-letter differences usually mean the same person (driving-mode typo) — flag with ⚠️ but don't auto-decide.

Run all searches in parallel (Promise.all / xargs -P / etc.). Don't burn a sequential round-trip per name.

5. Present match table + ask disambiguation

ALWAYS present a table before writing anything. Format:

| # | WhatsApp | PB matches | Notes |
|---|---------|-----------|-------|
| 1 | Corrie Strydom | `Corrie Strydom` (`zfasu6tsfcj87if`) — exact | ✓ clear |
| 2 | Johan Schultz | `Johan Schoultz Shaft Foreman` (`quvzzrufkn7xkqu`) | ⚠️ single-letter typo |
| 3 | Mark Wilkie | 2 records: `4lgz...` and `8rfe...` | ❓ duplicate |
| 4 | Andre Grobler | 0 hits | ❓ zero hit |

Then ask the user for each ambiguous/zero-hit name. Group into one set of questions with a/b/c options per item. Do not write anything until the user answers.

Standard question templates:

Situation Options
Single-letter typo (only candidate exists) (a) Pin as the typo-corrected record / (b) Skip / (c) Create new
2+ duplicates (a) Pin both (I'll merge later) / (b) Pin one (tell me which) / (c) Skip — let's dedupe first
0 hits (a) Skip (probably wrong name) / (b) Create new customer
Ambiguous (multiple distinct candidates) (a) Skip / (b) Pin [specific candidate] / (c) Create new

6. Apply writes

For each confirmed person, run TWO operations:

A. Update the customer record

PATCH ${PB_URL}/api/collections/customers/records/{id}
Authorization: Bearer <token>

{
  "coordinates": { "lat": <lat>, "lon": <lon> },
  "site_location": "<site>",
  "lastvisiteddate": "<SAST-2h>Z"
}

B. Create a rep_visits record

POST ${PB_URL}/api/collections/rep_visits/records
Authorization: Bearer <token>

{
  "customer": "<customer_id>",
  "rep": "<rep_id>",
  "position_source": "manual",
  "coordinates": { "lat": <lat>, "lon": <lon> },
  "notes": "Manual backfill from WhatsApp pin: <site> (<lat>,<lon>). Driving-mode typos: <typo1> → <canonical1>, ...",
  "visited_at": "<SAST-2h>Z",
  "pinned_customer": true
}

Run all writes in parallel. Verify after.

7. Verify

After all writes:

  • GET /api/collections/customers/records/{id} for one of the updated customers — confirm coordinates, site_location, lastvisiteddate persisted.
  • GET /api/collections/rep_visits/records/{id} for one of the created visits — confirm the fields persisted.
  • Report new site count across all customers.

8. Report

Final report should include:

  1. What was done — table: customer, coords, site, timestamp, visit ID.
  2. Spelling corrections — e.g. "PB spelling: Kobus Oberholtzer (two ts), Gary's spelling was the typo."
  3. Skipped names — tally grows by N.
  4. Site growth — distinct site_location count.
  5. Outstanding follow-ups:
    • site_location is still free-text on customers (and not on rep_visits). Past ~10 sites, migrate to a structured site collection + rep_visits.site foreign key.
    • The driving-mode importer (you, but automated from a WhatsApp webhook) is a half-day project.

Quirk table

Quirk Detail
Auth email .env.local → PB_ADMIN_EMAIL, never hardcoded
PB filter field customer~"X", NOT name~"X" or LIKE
customers.coordinates GeoJSON {lat, lon}
rep_visits.coordinates GeoJSON {lat, lon} (same as customers — both are geoPoint)
rep_visits.site_location Does not exist on the collection; do NOT include in visit writes. Site info lives on the customer record and in the visit's notes field.
Timestamp SAST = UTC+2; convert all WhatsApp times before writing
Local PB snapshot pb-exports/pb_schema-2.json may be stale — verify live schema before writing
Driving typos Single-letter differences are usually the same person (Kobus Oberholtzer, Enzo Msiza, Tinus van de Venter all pinned); always confirm with user
Duplicates If a name matches 2+ records, ask — usually "pin both, I'll merge later"
Zero hits Same shape as Gorden / Andre / Richard Dube — usually skip
Per-message timestamps Use when each name was its own WhatsApp message; default to shared timestamp
Pin-vs-name gap If the location pin and the names message have a >5 min gap, flag it — could mean a second un-pinned visit

Known skipped names (as of last sync)

If you encounter any of these again, ask before creating — they may be the same driving-typo situation:

  • Gorden van Staden
  • Andre Grobler
  • Richard Dube
  • Lucky Mabena
  • Janu
  • Nthabising Maloisi

If the user later confirms any of these are real people (not typos), backfill is straightforward: same workflow as a normal batch, treat as zero-hit option (b).


Anti-patterns

  • Don't write before presenting. The user always wants a match table + disambiguation questions first.
  • Don't guess on zero-hits. Skip by default; ask before creating new customers.
  • Don't auto-resolve duplicates. Ask whether to pin both, one, or skip for dedup-first.
  • Don't auto-resolve single-letter typos. Most of them are right (Kobus, Enzo, Tinus), but phrase as a question, not a fact.
  • Don't hardcode the PB email. Always read .env.local.
  • Don't include site_location on rep_visits writes — the field doesn't exist on that collection.
  • Don't use name~ as a filter — the field is customer.
  • Don't write without verifying — after each batch, re-fetch one customer and one visit to confirm persistence.
  • Don't trust the local PB snapshot — it's known to be stale on at least site_location.

Worked example

User input:

[2026/09/10, 10:07:15] Gary CNL: Location: https://maps.google.com/?q=-25.624908,27.256472 - Khuseleka
[2026/09/10, 10:38:59] Gary CNL: Corrie Strydom / Hein haasbroek / Johan Schultz / Lucas barnard / Nthabising maloisi / Titus Negota

What you do:

  1. Parse: coords = (-25.624908, 27.256472), site = Khuseleka, names = [Corrie Strydom, Hein haasbroek, Johan Schultz, Lucas barnard, Nthabising maloisi, Titus Negota], single shared timestamp 10:38:59 SAST → 08:38:59Z.
  2. Read .env.local, auth to PB.
  3. Fuzzy-match all 6 in parallel.
  4. Present table (exact, exact, typo, exact, 0 hits, typo).
  5. Ask: "Pin Schoultz?" / "Pin Neqota?" / "Skip or create Maloisi?".
  6. After answers, run 2N writes (PATCH + POST each).
  7. Verify, report.

That's the whole loop.