// People β€” contact resolution + management screen. // Two sections: Pending clusters (to resolve) + Confirmed people (to edit/merge). const RELATIONSHIP_OPTIONS = [ { value: 'family', label: 'Family', emoji: 'πŸ‘¨β€πŸ‘©β€πŸ‘§' }, { value: 'partner', label: 'Partner', emoji: 'πŸ’‘' }, { value: 'friend', label: 'Friend', emoji: 'πŸ‘₯' }, { value: 'childhood', label: 'Childhood friend', emoji: '🏫' }, { value: 'colleague', label: 'Colleague', emoji: 'πŸ’Ό' }, { value: 'army', label: 'Army / Reserves', emoji: 'πŸͺ–' }, { value: 'acquaintance', label: 'Acquaintance', emoji: '🀝' }, { value: 'pet', label: 'Pet', emoji: '🐾' }, { value: 'unknown', label: 'Unknown', emoji: '❓' }, ]; const REL_MAP = Object.fromEntries(RELATIONSHIP_OPTIONS.map(r => [r.value, r])); const REL_COLORS = { family: '#FF2D55', partner: '#AF52DE', friend: '#007AFF', childhood: '#5856D6', colleague: '#FF9500', army: '#34C759', acquaintance:'#5AC8FA', pet: '#FF9500', unknown: 'rgba(60,60,67,0.45)', }; const USER = (new URLSearchParams(window.location.search)).get('user') || 'shay'; const API = '/api'; function api(path, opts = {}) { return fetch(API + path, { headers: { 'Content-Type': 'application/json' }, ...opts, }).then(r => r.json()); } // ── Relationship picker sheet ───────────────────────────────────────────────── function RelPicker({ current, onPick, onClose }) { return (
e.stopPropagation()} style={{ background: T.bg2, borderRadius: '18px 18px 0 0', padding: '16px 16px 32px', }}>
Relationship
{RELATIONSHIP_OPTIONS.map(r => (
onPick(r.value)} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '11px 0', borderBottom: `1px solid ${T.sep}`, cursor: 'pointer', }}> {r.emoji} {r.label} {current === r.value && ( βœ“ )}
))}
); } // ── Confirmed person card ───────────────────────────────────────────────────── function PersonCard({ person, mergeSelected, onMergeSelect, onUpdate }) { const [editing, setEditing] = React.useState(false); const [name, setName] = React.useState(person.canonical_name); const [showRelPicker, setShowRelPicker] = React.useState(false); const [notes, setNotes] = React.useState(person.notes || ''); const [showNotes, setShowNotes] = React.useState(!!(person.notes)); const [busy, setBusy] = React.useState(false); const rel = REL_MAP[person.relationship_type] || REL_MAP.unknown; const relColor = REL_COLORS[person.relationship_type] || REL_COLORS.unknown; const isSelected = mergeSelected && mergeSelected.some(p => p.id === person.id); const mergeMode = mergeSelected !== null; const saveName = async () => { setEditing(false); if (name.trim() === person.canonical_name) return; setBusy(true); await api(`/people/${person.id}`, { method: 'PATCH', body: JSON.stringify({ user: USER, canonical_name: name.trim() }), }); onUpdate(); setBusy(false); }; const saveRel = async (val) => { setShowRelPicker(false); setBusy(true); await api(`/people/${person.id}`, { method: 'PATCH', body: JSON.stringify({ user: USER, relationship_type: val }), }); onUpdate(); setBusy(false); }; const saveNotes = async () => { await api(`/people/${person.id}`, { method: 'PATCH', body: JSON.stringify({ user: USER, notes: notes }), }); }; const doSplitAlias = async (alias) => { if (!confirm(`Split "${alias}" into a separate person?`)) return; setBusy(true); await api(`/people/${person.id}/split`, { method: 'POST', body: JSON.stringify({ user: USER, alias }), }); onUpdate(); setBusy(false); }; return ( <> {showRelPicker && ( setShowRelPicker(false)} /> )}
onMergeSelect(person) : undefined} style={{ background: T.bg2, borderRadius: 14, padding: '12px 14px', marginBottom: 8, opacity: busy ? 0.6 : 1, border: isSelected ? `2px solid ${T.blue}` : mergeMode ? `2px solid ${T.sep}` : '2px solid transparent', cursor: mergeMode ? 'pointer' : 'default', transition: 'border 0.15s', }} > {/* Header row */}
{mergeMode && (
{isSelected && βœ“}
)} {editing ? ( setName(e.target.value)} onBlur={saveName} onKeyDown={e => e.key === 'Enter' && saveName()} style={{ flex: 1, fontSize: 16, fontWeight: 600, border: 'none', outline: 'none', background: 'transparent', color: T.label, }} /> ) : (
{ e.stopPropagation(); if (!mergeMode) setEditing(true); }} style={{ flex: 1, fontSize: 16, fontWeight: 600, color: T.label, cursor: mergeMode ? 'default' : 'text' }} > {person.canonical_name}
)} {/* Relationship badge */}
{ e.stopPropagation(); if (!mergeMode) setShowRelPicker(true); }} style={{ display: 'flex', alignItems: 'center', gap: 4, background: relColor + '18', borderRadius: 999, padding: '3px 8px', cursor: mergeMode ? 'default' : 'pointer', }} > {rel.emoji} {rel.label}
{/* Aliases */} {person.aliases && person.aliases.length > 0 && (
{person.aliases.slice(0, 12).map(a => (
{a} {!mergeMode && ( { e.stopPropagation(); doSplitAlias(a); }} style={{ cursor: 'pointer', color: T.label3, marginLeft: 2, fontSize: 11 }} >βœ• )}
))} {person.aliases.length > 12 && (
+{person.aliases.length - 12}
)}
)} {/* Notes area */} {!mergeMode && showNotes && (