// people.jsx — Manifest CRM workspace const { useState: useStateM, useEffect: useEffectM, useMemo: useMemoM } = React; const CRM_TOUCH_KINDS = ["note", "email", "call", "meeting", "message", "intro", "follow-up"]; const CRM_ORG_KINDS = ["client", "partner", "vendor", "funder", "agency", "company", "nonprofit", "other"]; const CRM_STATUSES = ["active", "paused", "archived"]; function crmGmailCompose(email) { return `https://mail.google.com/mail/?view=cm&fs=1&to=${encodeURIComponent(email || "")}`; } function crmGmailSearch(email) { const q = `from:${email || ""} OR to:${email || ""}`; return `https://mail.google.com/mail/u/0/#search/${encodeURIComponent(q)}`; } function crmMapSearch(location) { return `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(location || "")}`; } function crmTodayPlus(days) { const d = new Date(); d.setDate(d.getDate() + days); return d.toISOString().slice(0, 10); } function crmFmtDate(value) { if (!value) return ""; const d = new Date(value); if (Number.isNaN(d.getTime())) return String(value).slice(0, 10); return d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }); } function crmOpen(url) { if (url) window.open(url, "_blank", "noopener,noreferrer"); } function ContactActions({ person, compact }) { const email = person?.email || ""; const phone = person?.phone || ""; const location = person?.location || ""; return (
Call
); } function PeopleView({ projects }) { const [people, setPeople] = useStateM([]); const [orgs, setOrgs] = useStateM([]); const [q, setQ] = useStateM(""); const [view, setView] = useStateM("all"); const [projectFilter, setProjectFilter] = useStateM(""); const [orgFilter, setOrgFilter] = useStateM(""); const [selectedKind, setSelectedKind] = useStateM("person"); const [selectedId, setSelectedId] = useStateM(null); const [detail, setDetail] = useStateM(null); const [orgDetail, setOrgDetail] = useStateM(null); const [showCreatePerson, setShowCreatePerson] = useStateM(false); const [showCreateOrg, setShowCreateOrg] = useStateM(false); const status = view === "archived" ? "archived" : view === "paused" ? "paused" : "active"; const loadPeople = () => { const params = new URLSearchParams(); if (q) params.set("q", q); if (view === "due") params.set("due", "1"); if (projectFilter) params.set("project_id", projectFilter); if (orgFilter) params.set("org_id", orgFilter); if (status) params.set("status", status); fetch(`/api/v1/people?${params}`) .then(r => r.ok ? r.json() : []) .then(setPeople) .catch(() => setPeople([])); }; const loadOrgs = () => { fetch("/api/v1/organizations") .then(r => r.ok ? r.json() : []) .then(setOrgs) .catch(() => setOrgs([])); }; useEffectM(loadPeople, [q, view, projectFilter, orgFilter]); useEffectM(loadOrgs, []); useEffectM(() => { if (!selectedId) { setDetail(null); setOrgDetail(null); return; } if (selectedKind === "organization") { fetch(`/api/v1/organizations/${selectedId}`) .then(r => r.ok ? r.json() : null) .then(setOrgDetail) .catch(() => setOrgDetail(null)); return; } fetch(`/api/v1/people/${selectedId}`) .then(r => r.ok ? r.json() : null) .then(setDetail) .catch(() => setDetail(null)); }, [selectedId, selectedKind]); const dueCount = people.filter(p => p.next_touch).length; const selectedProject = projects.find(p => p.id === projectFilter); const refreshSelected = () => { loadPeople(); loadOrgs(); if (!selectedId) return; if (selectedKind === "organization") { fetch(`/api/v1/organizations/${selectedId}`).then(r => r.ok ? r.json() : null).then(setOrgDetail); } else { fetch(`/api/v1/people/${selectedId}`).then(r => r.ok ? r.json() : null).then(setDetail); } }; return (
{selectedProject ? selectedProject.name : "All camps"}

People

setQ(e.target.value)} />
{showCreatePerson && ( setShowCreatePerson(false)} onSaved={(p) => { setShowCreatePerson(false); setSelectedKind("person"); setSelectedId(p.id); refreshSelected(); }} /> )} {showCreateOrg && ( setShowCreateOrg(false)} onSaved={(o) => { setShowCreateOrg(false); setSelectedKind("organization"); setSelectedId(o.id); refreshSelected(); }} /> )}
NameOrganizationLast touchNext
{people.map(p => ( ))} {people.length === 0 &&
No matching people.
}
Organizations
{orgs.map(o => ( ))}
{selectedKind === "person" && detail && ( { setSelectedId(null); setDetail(null); refreshSelected(); }} /> )} {selectedKind === "organization" && orgDetail && ( { setSelectedId(null); setOrgDetail(null); refreshSelected(); }} /> )} {!selectedId && (
Select a person or organization.
Open a dossier to edit contact details, log touchpoints, and manage follow-ups.
)}
); } function PersonForm({ person, orgs, onSaved, onCancel }) { const isEdit = !!person; const [draft, setDraft] = useStateM({ name: person?.name || "", title: person?.title || "", org_id: person?.org_id || "", email: person?.email || "", phone: person?.phone || "", location: person?.location || "", preferred_contact: person?.preferred_contact || "", next_touch: person?.next_touch || "", status: person?.status || "active", context: person?.context || "", notes: person?.notes || "", tags: (person?.tags || []).join(", "), linkedin: person?.links?.linkedin || "", website: person?.links?.website || "", }); const [error, setError] = useStateM(""); const set = (field, value) => setDraft(prev => ({ ...prev, [field]: value })); async function submit(e) { e.preventDefault(); setError(""); const body = { name: draft.name.trim(), title: draft.title.trim(), org_id: draft.org_id || null, email: draft.email.trim(), phone: draft.phone.trim(), location: draft.location.trim(), preferred_contact: draft.preferred_contact, next_touch: draft.next_touch, status: draft.status, context: draft.context.trim(), notes: draft.notes.trim(), tags: draft.tags.split(",").map(t => t.trim()).filter(Boolean), links: { linkedin: draft.linkedin.trim(), website: draft.website.trim(), }, }; try { const r = await fetch(isEdit ? `/api/v1/people/${person.id}` : "/api/v1/people", { method: isEdit ? "PATCH" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); const data = await r.json().catch(() => ({})); if (!r.ok) throw new Error(data.detail || "Could not save person"); onSaved(data); } catch (err) { setError(err.message); } } return (