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