diff --git a/.agents/skills/wfdash-rep-visit-batch-pin/SKILL.md b/.agents/skills/wfdash-rep-visit-batch-pin/SKILL.md new file mode 100644 index 0000000..a3a8a4a --- /dev/null +++ b/.agents/skills/wfdash-rep-visit-batch-pin/SKILL.md @@ -0,0 +1,234 @@ +--- +name: wfdash-rep-visit-batch-pin +description: 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`. + +```bash +# 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`: + +```json +{ "identity": "", "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 + +{ + "coordinates": { "lat": , "lon": }, + "site_location": "", + "lastvisiteddate": "Z" +} +``` + +**B. Create a `rep_visits` record** + +``` +POST ${PB_URL}/api/collections/rep_visits/records +Authorization: Bearer + +{ + "customer": "", + "rep": "", + "position_source": "manual", + "coordinates": { "lat": , "lon": }, + "notes": "Manual backfill from WhatsApp pin: (,). Driving-mode typos: → , ...", + "visited_at": "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 `t`s), 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.