/* global React, AppShell, Tag, Eyebrow, useFoyerData, leadBucket, fmtRelative, fmtClock, foyerGoToSession */ // /#/sessions — index page. The agent lands here when they click "Sessions" // in the sidebar; only after picking one do we route into the detail view. // Sortable by recency, filterable by recorded vs manual, with a per-card // preview so they can spot the right session without drilling in. const SessionsList = () => { const { summaries, sessionsById, loading, error } = useFoyerData(); const [kindFilter, setKindFilter] = React.useState('all'); // all | recorded | manual const [search, setSearch] = React.useState(''); const sortedSummaries = React.useMemo(() => { return [...summaries].sort((a, b) => (b.created_at || '').localeCompare(a.created_at || '')); }, [summaries]); const filtered = sortedSummaries.filter(s => { const kind = s.kind || 'recorded'; if (kindFilter !== 'all' && kind !== kindFilter) return false; const q = search.trim().toLowerCase(); if (!q) return true; if ((s.address || '').toLowerCase().includes(q)) return true; const visitors = sessionsById[s.id]?.result?.visitors || []; return visitors.some(v => (v.visitor?.name || '').toLowerCase().includes(q)); }); return (
Library

Sessions · {summaries.length}

Every recording and manual lead, newest first. Pick one to dive in.

Open kiosk
{/* search + kind chips */}
setSearch(e.target.value)} placeholder="Search address or guest…" style={{ width: '100%', background: 'transparent', border: 0, borderBottom: '1px solid var(--hairline)', padding: '10px 0', color: 'var(--cream)', fontSize: 13, fontFamily: 'var(--sans)', outline: 'none', }} />
{[ { id: 'all', label: 'All' }, { id: 'recorded', label: 'Recorded' }, { id: 'manual', label: 'Manual' }, ].map(opt => ( ))}
{loading && (
LOADING…
)} {error && (
Couldn't load: {error}
)} {!loading && filtered.length === 0 && (
{summaries.length === 0 ? 'No sessions yet.' : 'No sessions match.'}

{summaries.length === 0 ? 'Record an open house from the iOS app, or add a lead manually below.' : 'Try clearing the search or switching kind filter.'}

{summaries.length === 0 && ( Open kiosk )}
)} {/* card grid */}
{filtered.map(s => { const full = sessionsById[s.id]; const visitors = full?.result?.visitors || []; const needsCount = visitors.filter(v => leadBucket(v.lead_state) === 'needs').length; const kind = s.kind || 'recorded'; return (
foyerGoToSession(s.id)} style={{ background: 'var(--bg-card)', borderRadius: 14, padding: '22px 22px 18px', display: 'flex', flexDirection: 'column', gap: 14, cursor: 'pointer', }}>
{s.address || Untitled session}
{fmtRelative(s.created_at)} · {s.visitor_count || 0} {(s.visitor_count || 0) === 1 ? 'GUEST' : 'GUESTS'}
{visitors.length > 0 && (
{visitors.slice(0, 3).map(v => { const tagToken = (v.analysis?.tag || '').toLowerCase(); return (
{v.visitor?.name || '—'} {v.analysis?.score ?? '—'}
); })} {visitors.length > 3 && (
+ {visitors.length - 3} MORE
)}
)}
0 ? 'var(--gold)' : 'var(--text-muted)', letterSpacing: '0.12em' }}> {needsCount > 0 ? `${needsCount} NEED${needsCount === 1 ? 'S' : ''} ACTION` : 'INBOX CLEAR'} Open →
); })}
); }; function KindPill({ kind, status }) { const isProcessing = status === 'processing'; if (isProcessing) { return ( PROCESSING… ); } if (kind === 'manual') { return ( MANUAL ); } return ( RECORDED ); } Object.assign(window, { SessionsList });