+ {/* ---- head -------------------------------------------- */}
+
+
+
+
+
Pipeline
+
{leads.length} leads · {stageCount} stages
+
+
+
+
+ setQuery(e.target.value)}
+ placeholder="Search leads…"
+ aria-label="Search leads"
+ />
+ {query && }
+
+
+
+ {/* ---- board ------------------------------------------- */}
+
+ {STAGES.map((stage) => {
+ const items = byStage(stage.key);
+ const step = PROGRESSION.indexOf(stage.key);
+ return (
+
{ e.preventDefault(); setOverStage(stage.key); }}
+ onDragLeave={() => setOverStage((s) => (s === stage.key ? null : s))}
+ onDrop={() => moveTo(stage.key)}
+ >
+
+
+ {stage.label}
+ {items.length}
+ {step >= 0 && {step + 1}/{stageCount}}
+
+ = 0 ? `${((step + 1) / stageCount) * 100}%` : "100%" }} />
+
+
+ {items.map((l) => (
+
setSelected(l)}
+ onDragStart={() => setDragId(l.id)}
+ onDragEnd={() => { setDragId(null); setOverStage(null); }}
+ dragging={dragId === l.id}
+ />
+ ))}
+ {items.length === 0 && Drop leads here
}
+
+
+ );
+ })}
+
+
+
setSelected(null)}
+ onMore={() => { setDetailId(selected?.id ?? null); setSelected(null); }}
+ />
+
+ );
+}
+
+/* ---------------------------------------------------------- */
+/* Card */
+/* ---------------------------------------------------------- */
+
+function PipelineCard({ lead, onOpen, onDragStart, onDragEnd, dragging }: {
+ lead: PLead; onOpen: () => void; onDragStart: () => void; onDragEnd: () => void; dragging: boolean;
+}) {
+ return (
+
+ {/* ---- breadcrumb -------------------------------------- */}
+
+
+ {/* ---- hero -------------------------------------------- */}
+
+
+
+
{lead.name}
+
+ {lead.address}, {lead.city} {lead.state} {lead.zip}
+
+
+
{stage?.label}
+
{lead.leadType}
+
{lead.workType}
+ {lead.storm &&
Storm}
+
{lead.ageDays}d in pipeline
+
+
+
+
+ toast.push({ tone: "info", title: "Call", desc: `${lead.name} · ${primaryPhone?.number}` })}>Call
+ toast.push({ tone: "info", title: "Email", desc: lead.email })}>Email
+
+
+
+ {/* ---- stage progress ---------------------------------- */}
+
+
+ {stage?.label}
+ {pct}% complete
+
+
+
+ {progStages.map((s, i) => (
+ -
+ {i < reachedIdx ? : i + 1}
+ {s.label}
+
+ ))}
+
+
+
+ {/* ---- overview ---------------------------------------- */}
+
+
+ Phone Numbers
+ {lead.phones.map((p, i) => (
+
+
+
{p.number}
+
{p.type}
+ {p.primary &&
Primary}
+
+ ))}
+ Email Address
+
+
+
{lead.email}
+
Primary
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Field Notes
+ {lead.notes}
+
+
+
+ {lead.insurance ? (
+ <>
+
+
+
+
+
+
+ >
+ ) : (
+
+
+
Retail job — no insurance claim on file.
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+/** "Move Stage" dropdown — the detail-page equivalent of dragging a card. */
+function StagePicker({ current, open, onToggle, onPick }: {
+ current: StageKey; open: boolean; onToggle: (open: boolean) => void; onPick: (stage: StageKey) => void;
+}) {
+ const groups: { title: string; stages: Stage[] }[] = [
+ { title: "Pipeline stages", stages: STAGES.filter((s) => !s.flag) },
+ { title: "Flags", stages: STAGES.filter((s) => s.flag) },
+ ];
+
+ return (
+ { if (e.key === "Escape") onToggle(false); }}>
+
onToggle(!open)}>Move Stage
+ {open && (
+ <>
+
onToggle(false)} />
+
+ {groups.map((g) => (
+
+
{g.title}
+ {g.stages.map((s) => (
+
+ ))}
+
+ ))}
+
+ >
+ )}
+
+ );
+}
+
+function PSection({ title, icon, children, wide }: { title: string; icon: string; children: ReactNode; wide?: boolean }) {
+ return (
+
+ );
+}
+
+function Kv({ label, value }: { label: string; value: string }) {
+ return (
+
+ {label}
+ {value}
+
+ );
+}