import { useCallback, useEffect, useRef, useState } from 'react'; import { api, type Inbox, type InboxRow, type InboxStatus, type SyncResult } from '../api.js'; import { formatDayTimeParts, formatRelative } from '../components.js'; const INPUT = 'rounded border border-gray-300 px-2 py-1.5 text-sm'; const STATUS_TONE: Record = { pending: 'border-amber-300 bg-amber-50 text-amber-800', signed: 'border-green-300 bg-green-50 text-green-800', declined: 'border-red-300 bg-red-50 text-red-800', }; const isoDaysAgo = (days: number) => new Date(Date.now() - days * 86_400_000).toISOString().slice(0, 10); /** * Date and time as two nowrap spans: they sit on one line when the column * allows it, otherwise the time wraps under the date. The time matters — * Dropbox orders to the second, and same-day rows are common. */ function DateTime({ iso }: { iso: string }) { const { day, time } = formatDayTimeParts(iso); return ( <> {day}{' '} {time} ); } export default function NdaInbox({ onOpenBuyer, onChanged, }: { onOpenBuyer: (buyerId: string) => void; /** Imported rounds land in Today's pending list, so the badge follows. */ onChanged: () => void; }) { const [since, setSince] = useState(() => isoDaysAgo(90)); const [inbox, setInbox] = useState(null); const [error, setError] = useState(null); const [loading, setLoading] = useState(false); const [syncing, setSyncing] = useState(false); const [syncResult, setSyncResult] = useState(null); // Business chosen per row; '' means "import the NDA without a deal". const [picked, setPicked] = useState>({}); const [busy, setBusy] = useState(null); const [notice, setNotice] = useState(null); const poll = useRef(null); const rows: InboxRow[] | null = inbox?.requests ?? null; const refreshing = inbox?.refresh_state === 'running'; const refreshError = inbox?.refresh_state?.startsWith('error:') ? inbox.refresh_state.slice('error:'.length) : null; /** Reads the mirror out of the database — this never talks to Dropbox. */ const load = useCallback(async () => { setLoading(true); try { const res = await api.ndaInbox(since); setInbox(res); setError(null); // Preselect the best suggestion, but never overwrite a manual choice. setPicked((current) => { const next = { ...current }; for (const row of res.requests) { if (next[row.signature_request_id] === undefined) { next[row.signature_request_id] = row.business_suggestions[0]?.id ?? ''; } } return next; }); return res; } catch (err) { setError((err as Error).message); return null; } finally { setLoading(false); } }, [since]); useEffect(() => { void load(); }, [load]); // Whenever a walk is in flight — started here or elsewhere — watch it until // it finishes, then show whatever it brought in. useEffect(() => { if (!refreshing) { if (poll.current !== null) { window.clearInterval(poll.current); poll.current = null; } return; } if (poll.current !== null) return; poll.current = window.setInterval(() => void load(), 3000); return () => { if (poll.current !== null) { window.clearInterval(poll.current); poll.current = null; } }; }, [refreshing, load]); async function refresh() { setError(null); try { await api.refreshInbox(since); // Flip to "running" at once so the poller starts without a round trip. setInbox((current) => (current ? { ...current, refresh_state: 'running' } : current)); } catch (err) { setError((err as Error).message); } } async function sync() { setSyncing(true); setSyncResult(null); setError(null); try { setSyncResult(await api.syncInbox()); await load(); onChanged(); } catch (err) { setError((err as Error).message); } finally { setSyncing(false); } } async function importRow(row: InboxRow) { setBusy(row.signature_request_id); setNotice(null); setError(null); try { const res = await api.importInbox( row.signature_request_id, row.known_buyer?.buyer_id, picked[row.signature_request_id] || undefined, ); setNotice( res.warning ?? `Imported ${row.signer.name || row.signer.email}` + `${res.created.buyer ? ' as a new buyer' : ' into the existing buyer'}` + `${res.nas_path ? ' · PDF filed' : ''}.`, ); await load(); onChanged(); } catch (err) { setError((err as Error).message); } finally { setBusy(null); } } return (
Last refreshed: {formatRelative(inbox?.last_refresh_at ?? null)} {loading && !refreshing && ' · loading…'}
{refreshing && (

Fetching from Dropbox Sign in the background — the table below stays usable.

)} {refreshError && (

Last refresh failed: {refreshError}

)} {syncResult && (

Checked {syncResult.checked} · newly signed {syncResult.signed} · declined{' '} {syncResult.declined} · failed {syncResult.failed} {syncResult.warnings.length > 0 && ( · {syncResult.warnings.join('; ')} )}

)} {notice &&

{notice}

} {error &&

{error}

} {rows?.map((row) => ( ))} {rows && rows.length === 0 && ( )}
Date Signer E-mail Status Title Buyer Deal
{row.signer.name || '—'} {row.signer.email || '—'} {row.status} {row.title_remainder || '—'} {row.known_buyer ? ( ) : ( new )} {row.imported ? ( ) : ( )} {row.imported ? ( ) : ( )}
{inbox?.last_refresh_at ? 'No NDA signature requests in this period.' : 'Nothing fetched yet — press Refresh to load the signature requests.'}
); }