/* ============ Reports ============ */ function Reports() { const { clients, signings, mileage, settings } = useStore(); const year = new Date().getFullYear(); const toast = useToast(); // Re-render when backups change const [, setBackupTick] = React.useState(0); React.useEffect(() => Store.subscribe(() => setBackupTick(t => t+1)), []); const backups = Store.listBackups(); // Annual summary const yearStart = `${year}-01-01`; const yearSignings = signings.filter(s => s.date >= yearStart && s.status === 'Completed'); const yearMileage = mileage.filter(m => m.date >= yearStart); const grossRevenue = yearSignings.reduce((s,x) => s + (parseFloat(x.fee)||0), 0) + clients.filter(c => c.paid).reduce((s,c) => s + (parseFloat(c.revenue)||0), 0); const totalMiles = yearMileage.reduce((s,m) => s + (parseFloat(m.miles)||0) * (m.roundTrip ? 2 : 1), 0); const deduction = totalMiles * (settings.mileageRate||0); const netIncome = grossRevenue - deduction; // Monthly breakdown const months = []; for (let i = 0; i < 12; i++) { const d = new Date(year, i, 1); months.push({ key: d.toISOString().slice(0,7), label: d.toLocaleDateString('en-US', { month: 'short' }) }); } const monthly = months.map(mo => { const monthSignings = yearSignings.filter(s => s.date.startsWith(mo.key)); const monthMileage = yearMileage.filter(m => m.date.startsWith(mo.key)); const rev = monthSignings.reduce((s,x) => s + (parseFloat(x.fee)||0), 0); const miles = monthMileage.reduce((s,m) => s + (parseFloat(m.miles)||0) * (m.roundTrip ? 2 : 1), 0); return { ...mo, signings: monthSignings.length, revenue: rev, miles, deduction: miles * (settings.mileageRate||0) }; }); const exportClients = () => { const rows = [ ['ID','Name','Phone','Email','Source','Status','Priority','Revenue','Paid','Notes'], ...clients.map(c => [c.id, c.name, c.phone, c.email, c.source, c.status, c.priority, c.revenue, c.paid ? 'Yes' : 'No', c.notes||'']) ]; downloadCSV('clients', rows); }; const exportSignings = () => { const rows = [ ['ID','Date','Time','Title','Client','DocType','Location','DurationMin','Fee','Status','Notes'], ...signings.map(s => { const client = clients.find(c => c.id === s.clientId); return [s.id, s.date, s.time, s.title, client?.name || '', s.docType, s.location, s.durationMin, s.fee, s.status, s.notes||'']; }) ]; downloadCSV('signings', rows); }; const exportAll = () => { const blob = new Blob([JSON.stringify({ clients, signings, mileage, settings }, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `notary-studio-backup-${todayISO()}.json`; a.click(); URL.revokeObjectURL(url); }; const importAll = (e) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { try { const payload = JSON.parse(reader.result); if (confirm('Replace ALL current data with the imported file? This cannot be undone.')) { Store.importJSON(payload); } } catch(err) { alert('Could not parse file: ' + err.message); } }; reader.readAsText(file); e.target.value = ''; }; return (
!c.paid).length} with balance`} iconName="people" iconColor="var(--orange)" />
{monthly.map(mo => ( ))}
Month Signings Revenue Miles Deduction Net
{mo.label} {year} {mo.signings} {fmt.money(mo.revenue)} {mo.miles.toFixed(1)} {fmt.money(mo.deduction, 2)} {fmt.money(mo.revenue - mo.deduction)}
Year total {yearSignings.length} {fmt.money(grossRevenue)} {totalMiles.toFixed(1)} {fmt.money(deduction)} {fmt.money(netIncome)}
{ Store.snapshotNow('Manual snapshot'); toast('Snapshot saved'); }}>Snapshot now}> {backups.length === 0 ? ( ) : (
{backups.map(b => ( ))}
Snapshot Taken Clients Signings Trips
{b.label || (b.date === todayISO() ? 'Today' : fmt.date(b.date))}
{b.id}
{new Date(b.createdAt).toLocaleString('en-US', { month:'short', day:'numeric', hour:'numeric', minute:'2-digit' })}
{fmt.relativeTime(new Date(b.createdAt).getTime())}
{b.counts?.clients ?? '—'} {b.counts?.signings ?? '—'} {b.counts?.mileage ?? '—'}
downloadBackup(b)} /> { if (confirm(`Restore snapshot from ${fmt.date(b.date)}? Your current data will be replaced.`)) { // Take a safety snapshot first so this restore is itself undoable Store.snapshotNow('Before restore — ' + new Date().toLocaleString()); Store.restoreBackup(b.id); toast('Restored from ' + fmt.date(b.date)); } }} /> { if (confirm('Delete this snapshot?')) { Store.deleteBackup(b.id); toast('Snapshot deleted'); } }} />
)}
Snapshots are stored on this device only. For off-device safety, click Backup everything (JSON) above and keep the file in cloud storage.
); } window.Reports = Reports; function downloadBackup(b) { const blob = new Blob([JSON.stringify(b.data, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `notary-studio-${b.date}-${b.id.slice(-6)}.json`; a.click(); URL.revokeObjectURL(url); }