diff --git a/src/components/leads/CanvasserSelector.jsx b/src/components/leads/CanvasserSelector.jsx
new file mode 100644
index 0000000..ba794e6
--- /dev/null
+++ b/src/components/leads/CanvasserSelector.jsx
@@ -0,0 +1,215 @@
+import React, { useState, useRef, useEffect, useMemo } from 'react';
+import { createPortal } from 'react-dom';
+import { motion, AnimatePresence } from 'framer-motion';
+import { Check, ChevronDown, Search } from 'lucide-react';
+import { useMockStore } from '../../data/mockStore';
+
+function FieldLabel({ children }) {
+ return (
+
+ );
+}
+
+export default function CanvasserSelector({ value, onChange, onNameChange }) {
+ const { orgMembers } = useMockStore();
+ const [open, setOpen] = useState(false);
+ const [query, setQuery] = useState('');
+ const [pos, setPos] = useState(null);
+ const inputRef = useRef(null);
+ const dropdownRef = useRef(null);
+
+ const canvassers = useMemo(() =>
+ orgMembers.filter(m => m.roleKey === 'CANVASSER' && m.status === 'active'),
+ [orgMembers]
+ );
+
+ const filtered = useMemo(() => {
+ if (!query.trim()) return canvassers;
+ const q = query.toLowerCase();
+ return canvassers.filter(m =>
+ m.name.toLowerCase().includes(q) || m.email.toLowerCase().includes(q)
+ );
+ }, [query, canvassers]);
+
+ const selectedMember = useMemo(() =>
+ canvassers.find(m => m.userId === value),
+ [canvassers, value]
+ );
+
+ const computePos = () => {
+ if (inputRef.current) {
+ const r = inputRef.current.getBoundingClientRect();
+ setPos({
+ top: r.bottom + window.scrollY + 4,
+ left: r.left + window.scrollX,
+ width: Math.max(r.width, 280),
+ });
+ }
+ };
+
+ const handleFocus = () => {
+ computePos();
+ setQuery('');
+ setOpen(true);
+ };
+
+ const handleChange = (e) => {
+ setQuery(e.target.value);
+ if (!open) {
+ computePos();
+ setOpen(true);
+ }
+ };
+
+ const handleSelect = (member) => {
+ onChange(member.userId);
+ if (onNameChange) onNameChange(member.name);
+ setQuery('');
+ setOpen(false);
+ inputRef.current?.blur();
+ };
+
+ const handleBlur = (e) => {
+ if (dropdownRef.current?.contains(e.relatedTarget)) return;
+ setOpen(false);
+ setQuery('');
+ };
+
+ const handleKeyDown = (e) => {
+ if (e.key === 'Escape') { setOpen(false); setQuery(''); inputRef.current?.blur(); }
+ if (e.key === 'Enter' && filtered.length > 0) { handleSelect(filtered[0]); }
+ };
+
+ useEffect(() => {
+ if (!open) return;
+ const handler = (e) => {
+ const inInput = inputRef.current?.contains(e.target);
+ const inDropdown = dropdownRef.current?.contains(e.target);
+ if (!inInput && !inDropdown) { setOpen(false); setQuery(''); }
+ };
+ document.addEventListener('mousedown', handler);
+ return () => document.removeEventListener('mousedown', handler);
+ }, [open]);
+
+ useEffect(() => {
+ if (!open) return;
+ const close = (e) => {
+ const path = e.composedPath?.() ?? [];
+ if (dropdownRef.current && path.includes(dropdownRef.current)) return;
+ setOpen(false);
+ setQuery('');
+ };
+ window.addEventListener('scroll', close, true);
+ window.addEventListener('resize', close);
+ return () => {
+ window.removeEventListener('scroll', close, true);
+ window.removeEventListener('resize', close);
+ };
+ }, [open]);
+
+ const displayValue = open ? query : (selectedMember?.name || '');
+
+ return (
+
+
Canvasser *
+
+
+
+
+
+
+
+
+
+ {createPortal(
+
+ {open && pos && (
+
+
+ {filtered.length === 0 ? (
+
+ No canvassers match "{query}"
+
+ ) : (
+ filtered.map(member => {
+ const isSelected = member.userId === value;
+ return (
+
+ );
+ })
+ )}
+
+
+ )}
+ ,
+ document.body
+ )}
+
+ );
+}
diff --git a/src/components/leads/LeadJobSection.jsx b/src/components/leads/LeadJobSection.jsx
index 2b3e01c..7d6db1a 100644
--- a/src/components/leads/LeadJobSection.jsx
+++ b/src/components/leads/LeadJobSection.jsx
@@ -3,6 +3,7 @@ import { motion, AnimatePresence } from 'framer-motion';
import { LEAD_FORM_OPTIONS } from '../../data/mockStore';
import UrgencyPillSelector from './UrgencyPillSelector';
import LeadCustomSelect from './LeadCustomSelect';
+import CanvasserSelector from './CanvasserSelector';
// ---------------------------------------------------------------------------
// Shared field primitives (local to Job section)
@@ -61,6 +62,52 @@ export default function LeadJobSection({ formData, updateField, isQuickCapture }
accent="blue"
/>
+ {/* --- Door Knock → Canvasser selector --- */}
+
+ {formData.leadSource === 'Door Knock' && (
+
+
+ updateField('canvasserId', v)}
+ onNameChange={v => updateField('canvasserName', v)}
+ />
+
+
+ )}
+
+
+ {/* --- Referral → Referral Note --- */}
+
+ {formData.leadSource === 'Referral' && (
+
+
+ updateField('referralNote', v)}
+ placeholder="e.g. Referred by John Smith, existing customer on Oak Street…"
+ rows={2}
+ />
+
+
+ )}
+
+
{/* --- Lead Type — Full Form only --- */}
{!isQuickCapture && (
diff --git a/src/pages/CreateLeadPage.jsx b/src/pages/CreateLeadPage.jsx
index e9afb3a..2e05302 100644
--- a/src/pages/CreateLeadPage.jsx
+++ b/src/pages/CreateLeadPage.jsx
@@ -69,6 +69,9 @@ const SECTIONS = [
const INITIAL_FORM = {
// Job Details
leadSource: '',
+ canvasserId: '',
+ canvasserName: '',
+ referralNote: '',
leadType: '',
workType: '',
tradeType: '',
@@ -156,7 +159,14 @@ export default function CreateLeadPage() {
}, [progress]);
const updateField = (field, value) => {
- setFormData(prev => ({ ...prev, [field]: value }));
+ setFormData(prev => {
+ const next = { ...prev, [field]: value };
+ if (field === 'leadSource') {
+ if (value !== 'Door Knock') { next.canvasserId = ''; next.canvasserName = ''; }
+ if (value !== 'Referral') { next.referralNote = ''; }
+ }
+ return next;
+ });
};
const toggleSection = (id) => {
@@ -173,6 +183,7 @@ export default function CreateLeadPage() {
if (!formData.address) missingFields.push('Street Address');
if (!formData.city) missingFields.push('City');
if (!formData.leadSource) missingFields.push('Lead Source');
+ if (formData.leadSource === 'Door Knock' && !formData.canvasserId) missingFields.push('Canvasser');
if (missingFields.length > 0) {
setValidationError(`Required: ${missingFields.join(', ')}`);
diff --git a/src/pages/LeadsListPage.jsx b/src/pages/LeadsListPage.jsx
index 8f2ab7b..d8e2f72 100644
--- a/src/pages/LeadsListPage.jsx
+++ b/src/pages/LeadsListPage.jsx
@@ -8,6 +8,7 @@ import PermissionGate from '../components/PermissionGate';
import {
Plus, Search, MapPin, Phone, Zap, FileText,
Clock, User, ChevronRight, Filter, Lock,
+ DoorOpen, MessageSquare,
} from 'lucide-react';
// ---------------------------------------------------------------------------
@@ -117,6 +118,18 @@ function LeadCard({ lead, index }) {
{lead.leadSource}
)}
+ {lead.canvasserName && (
+
+
+ {lead.canvasserName}
+
+ )}
+ {lead.referralNote && (
+
+
+ {lead.referralNote}
+
+ )}