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.
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)
- Coordinates — either:
- Google Maps URL:
https://maps.google.com/?q=-26.217859,27.791794(extract theq=) - Or raw:
-26.217859, 27.791794(decimal degrees, lat first, lon second)
- Google Maps URL:
- Site name — free-text string, e.g.
Khuseleka,Cementation,Doornkop,Elandsrand / Kusasalethu - Person names — one per line, numbered, or pasted as a WhatsApp message block
- (optional) Per-message timestamps — use when each name was its own message; default to the single shared timestamp of the names message
- (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, NOTname. The PB filter operator is~(contains), notLIKE.- ✅
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 — confirmcoordinates,site_location,lastvisiteddatepersisted.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:
- What was done — table: customer, coords, site, timestamp, visit ID.
- Spelling corrections — e.g. "PB spelling:
Kobus Oberholtzer(twots), Gary's spelling was the typo." - Skipped names — tally grows by N.
- Site growth — distinct
site_locationcount. - Outstanding follow-ups:
site_locationis still free-text oncustomers(and not onrep_visits). Past ~10 sites, migrate to a structuredsitecollection +rep_visits.siteforeign 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_locationonrep_visitswrites — the field doesn't exist on that collection. - Don't use
name~as a filter — the field iscustomer. - 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:
- Parse: coords =
(-25.624908, 27.256472), site =Khuseleka, names = [Corrie Strydom,Hein haasbroek,Johan Schultz,Lucas barnard,Nthabising maloisi,Titus Negota], single shared timestamp10:38:59 SAST → 08:38:59Z. - Read
.env.local, auth to PB. - Fuzzy-match all 6 in parallel.
- Present table (exact, exact, typo, exact, 0 hits, typo).
- Ask: "Pin Schoultz?" / "Pin Neqota?" / "Skip or create Maloisi?".
- After answers, run 2N writes (PATCH + POST each).
- Verify, report.
That's the whole loop.