|
|
|
@@ -313,6 +313,14 @@ type Urgency = "Standard" | "High" | "Emergency";
|
|
|
|
|
type PhoneRow = { number: string; type: string };
|
|
|
|
|
type EmailRow = { address: string };
|
|
|
|
|
|
|
|
|
|
const FULL_STEPS = [
|
|
|
|
|
{ value: "contact", label: "Contact", icon: "user" },
|
|
|
|
|
{ value: "property", label: "Property", icon: "owners" },
|
|
|
|
|
{ value: "job", label: "Job Details", icon: "projects" },
|
|
|
|
|
{ value: "insurance", label: "Insurance", icon: "shield" },
|
|
|
|
|
{ value: "assignment", label: "Assignment", icon: "team" },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const LEAD_TYPE_OPTS = ["Residential", "Commercial", "Multi-Family"];
|
|
|
|
|
const PROPERTY_TYPE_OPTS = ["Residential", "Commercial", "Multi-Family", "Industrial"];
|
|
|
|
|
|
|
|
|
@@ -322,7 +330,7 @@ const BLANK = {
|
|
|
|
|
emails: [] as EmailRow[],
|
|
|
|
|
address: "", city: "", state: "TX", zip: "", propertyType: "",
|
|
|
|
|
photos: [] as string[],
|
|
|
|
|
source: "", leadType: "", workType: "", tradeType: "", urgency: "Standard" as Urgency, notes: "",
|
|
|
|
|
source: "", referralNote: "", canvasser: "", leadType: "", workType: "", tradeType: "", urgency: "Standard" as Urgency, notes: "",
|
|
|
|
|
insCompany: "", claimNumber: "", claimStatus: "", adjusterName: "", adjusterPhone: "", policyNumber: "",
|
|
|
|
|
assignRep: "", priority: "Medium" as Priority, followUp: "",
|
|
|
|
|
};
|
|
|
|
@@ -357,6 +365,12 @@ function NewLead({ open, onClose }: { open: boolean; onClose: () => void }) {
|
|
|
|
|
|
|
|
|
|
const repOptions = [{ id: "", initials: "—", name: "Unassigned" }, ...REPS];
|
|
|
|
|
|
|
|
|
|
const stepIdx = FULL_STEPS.findIndex((s) => s.value === section);
|
|
|
|
|
const isFirstStep = stepIdx <= 0;
|
|
|
|
|
const isLastStep = stepIdx === FULL_STEPS.length - 1;
|
|
|
|
|
const goNext = () => { if (!isLastStep) setSection(FULL_STEPS[stepIdx + 1].value); };
|
|
|
|
|
const goBack = () => { if (!isFirstStep) setSection(FULL_STEPS[stepIdx - 1].value); };
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Modal
|
|
|
|
|
open={open}
|
|
|
|
@@ -366,10 +380,20 @@ function NewLead({ open, onClose }: { open: boolean; onClose: () => void }) {
|
|
|
|
|
subtitle="Full lead profile with insurance and assignment details."
|
|
|
|
|
icon="plus"
|
|
|
|
|
footer={
|
|
|
|
|
<>
|
|
|
|
|
<Btn variant="ghost" onClick={close}>Cancel</Btn>
|
|
|
|
|
<Btn icon="check" onClick={submit}>Create Lead</Btn>
|
|
|
|
|
</>
|
|
|
|
|
mode === "full" ? (
|
|
|
|
|
<>
|
|
|
|
|
<Btn variant="ghost" onClick={close}>Cancel</Btn>
|
|
|
|
|
{!isFirstStep && <Btn variant="ghost" onClick={goBack}>Back</Btn>}
|
|
|
|
|
{isLastStep
|
|
|
|
|
? <Btn icon="check" onClick={submit}>Create Lead</Btn>
|
|
|
|
|
: <Btn icon="arrow" onClick={goNext}>Next</Btn>}
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<Btn variant="ghost" onClick={close}>Cancel</Btn>
|
|
|
|
|
<Btn icon="check" onClick={submit}>Create Lead</Btn>
|
|
|
|
|
</>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<div className="nl-form">
|
|
|
|
@@ -384,12 +408,17 @@ function NewLead({ open, onClose }: { open: boolean; onClose: () => void }) {
|
|
|
|
|
<Field label="First name" required><input className="ds-input" value={f.firstName} onChange={set("firstName")} placeholder="John" /></Field>
|
|
|
|
|
<Field label="Last name"><input className="ds-input" value={f.lastName} onChange={set("lastName")} placeholder="Smith" /></Field>
|
|
|
|
|
<Field label="Phone"><input className="ds-input" value={f.phones[0]?.number ?? ""} onChange={(e) => setPhone(0, "number", e.target.value)} placeholder="(555) 000-0000" /></Field>
|
|
|
|
|
<Field label="Lead source"><Select value={f.source} onChange={set("source")} options={LEAD_SOURCES} placeholder="How did you find this lead?" /></Field>
|
|
|
|
|
<div className="nl-full"><Field label="Street address"><input className="ds-input" value={f.address} onChange={set("address")} placeholder="123 Main St" /></Field></div>
|
|
|
|
|
<Field label="City"><input className="ds-input" value={f.city} onChange={set("city")} placeholder="Plano" /></Field>
|
|
|
|
|
<Field label="State"><input className="ds-input" value={f.state} onChange={set("state")} /></Field>
|
|
|
|
|
<Field label="ZIP"><input className="ds-input" value={f.zip} onChange={set("zip")} placeholder="75023" /></Field>
|
|
|
|
|
<Field label="Assign rep"><RepSelect value={f.assignRep} onChange={set("assignRep")} options={repOptions} /></Field>
|
|
|
|
|
<Field label="Lead source"><Select value={f.source} onChange={set("source")} options={LEAD_SOURCES} placeholder="How did you find this lead?" /></Field>
|
|
|
|
|
{f.source === "Referral" && (
|
|
|
|
|
<div className="nl-full"><Field label="Referral note"><textarea className="ds-textarea" rows={3} value={f.referralNote} onChange={set("referralNote")} placeholder="Who referred this lead? Any details…" /></Field></div>
|
|
|
|
|
)}
|
|
|
|
|
{f.source === "Door Knock" && (
|
|
|
|
|
<div className="nl-full"><Field label="Canvasser"><CanvasserSearch value={f.canvasser} onChange={(v) => setF((s) => ({ ...s, canvasser: v }))} options={REPS} /></Field></div>
|
|
|
|
|
)}
|
|
|
|
|
<div className="nl-full"><PriorityPicker value={f.priority} onChange={(p) => setF((s) => ({ ...s, priority: p }))} /></div>
|
|
|
|
|
<Field label="Follow-up date"><input className="ds-input" type="date" value={f.followUp} onChange={set("followUp")} /></Field>
|
|
|
|
|
</div>
|
|
|
|
@@ -398,13 +427,7 @@ function NewLead({ open, onClose }: { open: boolean; onClose: () => void }) {
|
|
|
|
|
<SegTabs
|
|
|
|
|
value={section}
|
|
|
|
|
onChange={setSection}
|
|
|
|
|
tabs={[
|
|
|
|
|
{ value: "contact", label: "Contact", icon: "user" },
|
|
|
|
|
{ value: "property", label: "Property", icon: "owners" },
|
|
|
|
|
{ value: "job", label: "Job Details", icon: "projects" },
|
|
|
|
|
{ value: "insurance", label: "Insurance", icon: "shield" },
|
|
|
|
|
{ value: "assignment", label: "Assignment", icon: "team" },
|
|
|
|
|
]}
|
|
|
|
|
tabs={FULL_STEPS}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{section === "contact" && (
|
|
|
|
@@ -516,6 +539,55 @@ function RepSelect({ value, onChange, options }: { value: string; onChange: (e:
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function CanvasserSearch({ value, onChange, options }: { value: string; onChange: (v: string) => void; options: { id: string; initials: string; name: string; email: string }[] }) {
|
|
|
|
|
const [query, setQuery] = useState("");
|
|
|
|
|
const [open, setOpen] = useState(false);
|
|
|
|
|
const selected = options.find((o) => o.id === value);
|
|
|
|
|
const matches = useMemo(() => {
|
|
|
|
|
const q = query.trim().toLowerCase();
|
|
|
|
|
if (!q) return options;
|
|
|
|
|
return options.filter((o) => o.name.toLowerCase().includes(q) || o.email.toLowerCase().includes(q));
|
|
|
|
|
}, [query, options]);
|
|
|
|
|
|
|
|
|
|
if (selected) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="nl-canvasser-chip">
|
|
|
|
|
<Avatar initials={selected.initials} size={28} />
|
|
|
|
|
<span className="nl-canvasser-name">{selected.name}</span>
|
|
|
|
|
<span className="nl-canvasser-email">{selected.email}</span>
|
|
|
|
|
<button type="button" className="nl-canvasser-clear" onClick={() => { onChange(""); setQuery(""); }} aria-label="Clear canvasser"><Icon name="x" size={14} /></button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="nl-canvasser">
|
|
|
|
|
<input
|
|
|
|
|
className="ds-input"
|
|
|
|
|
value={query}
|
|
|
|
|
onChange={(e) => { setQuery(e.target.value); setOpen(true); }}
|
|
|
|
|
onFocus={() => setOpen(true)}
|
|
|
|
|
onBlur={() => setTimeout(() => setOpen(false), 150)}
|
|
|
|
|
placeholder="Search canvasser by name or email"
|
|
|
|
|
/>
|
|
|
|
|
{open && matches.length > 0 && (
|
|
|
|
|
<div className="nl-canvasser-menu">
|
|
|
|
|
{matches.map((o) => (
|
|
|
|
|
<button key={o.id} type="button" className="nl-canvasser-opt" onMouseDown={(e) => { e.preventDefault(); onChange(o.id); setQuery(""); setOpen(false); }}>
|
|
|
|
|
<Avatar initials={o.initials} size={26} />
|
|
|
|
|
<span className="nl-canvasser-name">{o.name}</span>
|
|
|
|
|
<span className="nl-canvasser-email">{o.email}</span>
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
{open && matches.length === 0 && (
|
|
|
|
|
<div className="nl-canvasser-menu"><div className="nl-canvasser-empty">No canvasser found</div></div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function PriorityPicker({ value, onChange }: { value: Priority; onChange: (p: Priority) => void }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="ds-field">
|
|
|
|
|