7.8 KiB
Lead Verification — Design Spec
Status: Approved direction (adapted port from gitea/bugfix/project-refinement). Feeds an implementation plan.
Purpose
Add the pre-pipeline lead-verification stage the CRM is missing. Inbound leads (door-knock, storm canvass, web form, call center, referral) land in a verification queue where a verification/call-center team confirms they're real — identity, insurance, reachability — before they enter the sales pipeline. This is a B2B-first internal tool (the roofing company's verification desk), not a customer-facing surface.
It exists on the old lineage (gitea/bugfix/project-refinement, commits 567b29c "adds lead verification module" + 8a4d577) as a self-contained module. Our revamp branch has diverged heavily (Plans 1–7), so this is an adapted port — bring the UI + flow, re-author the data against our canonical model, wire it to our existing leads pipeline.
Why it fits (no duplication)
verifyLead(id) in the source module auto-converts a verified record into a New Lead pushed onto the leads array. That leads array is exactly the one we enriched (Plan 3) and made clickable with a detail page (Plan 6). So the chain becomes:
Lead Verification queue → (verify) → New Lead in `leads` → Kanban pipeline → Project (PRJ-2026-###)
It sits upstream of everything we've built and reuses our pipeline as its output. Nothing we built is duplicated or replaced.
The flow & states
Two status axes (both preserved from the source):
- Coarse
status(drives summary tiles + primary badge):Pending → Assigned → In Progress → Verified | Unverified. - Granular
verificationStatus(the work detail):Unassigned,Pending Outreach,Awaiting Confirmation,Verifying Identity,Reviewing Insurance,Emergency — Awaiting Verifier,Failed Contact,Verified.
Transitions:
- A new record is
Pending/Unassigned. - Assign to a verifier →
Assigned/Pending Outreach. - Verifier works it →
In Progresswith a granularverificationStatus(Verifying Identity / Reviewing Insurance / etc.). - Verify →
Verified(record leaves the active queue) and a New Lead is pushed toleads(linked back viaverificationId). - Unverify (e.g. Failed Contact) →
Unverifiedwith a reason note. - Notes can be added at any point (audit trail).
Data model (into src/data/mockStore.jsx)
MOCK_LEAD_VERIFICATIONS— ~15 records, re-keyed to our world. Per record (shape from source, adapted):{ id, leadId, customerName, phone, email, address, source, status, verificationStatus, assigneeId, assigneeName, assignedAt, verifiedAt, urgency, verificationNotes, history:[{ at, by, action }] }.- Assignees/verifiers = our canonical admins as the verification desk: a1 Wade Hollis, a2 Darlene Brooks, a3 Roy Schaefer (field agents may also appear). No fake names.
- Sources = our real lead sources: Door Knock, Storm Canvass, Web Form, Call Center, Referral.
- Customers = realistic Plano homeowners (varied; not the existing pipeline customers — these are new inbound).
- A spread across all states so the demo shows every status (a couple Verified, a couple Unverified/Failed Contact, several in-progress/assigned/pending, one "Emergency — Awaiting Verifier").
leadVerificationsstate + the 6 mutators on the context value:assignLeadVerification(id, assigneeId, assigneeName)→ Assigned / Pending Outreach.reassignLeadVerification(id, assigneeId, assigneeName).setLeadVerificationStatus(id, newStatus, note?).verifyLead(id)→ marks Verified + appends history + pushes a New Lead ontoleadsusing OUR enriched lead schema (firstName/lastName, address/city/state, phones:[{number,type,isPrimary}], emails, leadSource, status:'New', createdByName:'Lead Verification', verificationId).unverifyLead(id, note).addLeadVerificationNote(id, note).
UI (ported components, adapted imports)
src/components/ui/Select.jsx— the dark-mode-aware select (reusable; also lets us standardise dropdowns). New shared component.src/components/LeadVerification/—statusConfig.js,SummaryCards.jsx,LeadTableRow.jsx,LeadCard.jsx,LeadStatusBadge.jsx,LeadActionsMenu.jsx,AssignLeadModal.jsx,LeadDetailsModal.jsx.src/pages/LeadVerification/LeadVerificationPage.jsx— summary tiles (counts per status) + search + filters (status / source / assignee) + a responsive table (desktop) / cards (mobile) + the assign / reassign / verify / unverify / view modals. UsesusePermissions(we already have the hook) for action gating.
Routing & nav
- Add a single unified route
/lead-verification→LeadVerificationPage, gatedallowedRoles={['OWNER','ADMIN','FIELD_AGENT']}(interim coarse guard; Plan 8 ABAC replaces it). (Per our Plan-2 unified-route direction — not the source's three role-prefixed routes.) - Add a nav entry ("Lead Verification") for the relevant roles in the sidebar/layout.
Explicitly NOT ported
- The source's access-control-matrix / RBAC changes (commit
7b485a4) — our Plan 8 ABAC supersedes them. Gate with the existing coarseProtectedRoutefor now. - Their old-lineage mockStore wholesale — only the verification slice is ported, re-authored against our canonical roster.
Grounding requirements (must be real, coherent data — not placeholders)
The source module's MOCK_LEAD_VERIFICATIONS is not copied verbatim — it is re-authored to our standards:
- Real people, no fakes. Verifiers/assignees are the canonical admins (Wade Hollis / Darlene Brooks / Roy Schaefer); customer names are realistic Plano/DFW homeowners (no "John Smith", "Alice Customer", movie characters, or sequential placeholders). Phones use varied real DFW area codes (972/469/214/945) formatted en-US, never
(972) 555-0NNNsequences. - Real places & sources. Addresses are real Plano street/zip combos; sources are the company's actual channels (Door Knock, Storm Canvass, Web Form, Call Center, Referral). Storm-sourced records should reference the same storm-season framing used elsewhere.
- Computed, not hardcoded, summaries. The summary tiles (counts per status) and any totals are derived from the
leadVerificationsarray at render — never hardcoded numbers. (Same compute-don't-hardcode rule as the dashboards.) - Coherent dates.
assignedAt≤verifiedAt≤ DEMO_TODAY (2026-05-29); history timestamps strictly increasing; created dates realistic for an inbound queue. - Verify produces a real, coherent lead. The New Lead pushed onto
leadsmatches our enriched schema so it renders correctly in the Leads list AND opens in the lead detail page (Plan 6) with no empty/broken fields. - Internally consistent. A record's
statusandverificationStatusagree (e.g.Verifiedcoarse ↔Verifiedgranular;In Progress↔ a working granular state); anAssigned/In Progressrecord has a real assignee;Unassignedhas none.
Acceptance
- Build clean (0 duplicate-key warnings).
- The page lists 15 verification records with correct per-status summary tiles; filters (status/source/assignee) work; assignees are canonical admins (no fake names).
- Verify a record → it leaves the active queue as Verified AND a matching New Lead appears in the Leads list (
/…/leads) withcreatedByName: 'Lead Verification'; the new lead opens in the lead detail page (Plan 6). - Assign / reassign / set-status / unverify / add-note all persist in session and reflect in the table + badges.
/lead-verificationreachable from nav for OWNER/ADMIN/FIELD_AGENT.
Reuse / dependencies
usePermissionshook — already present.- Our enriched
leadsschema (Plan 3/6) — the verify output target. components/ui/Select— newly ported; reusable across the app.