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} + + )}