/* ============ Dashboard ============ */ function Dashboard({ onNavigate }) { const { clients, signings, mileage, settings } = useStore(); const totalClients = clients.length; const totalRevenue = clients.reduce((s,c) => s + (parseFloat(c.revenue)||0), 0); const outstanding = clients.filter(c => !c.paid).reduce((s,c) => s + (parseFloat(c.revenue)||0), 0); const totalSignings = clients.filter(c => c.status === 'Won').length + signings.filter(s => s.status === 'Completed').length; const totalMiles = mileage.reduce((s,m) => s + (parseFloat(m.miles)||0) * (m.roundTrip ? 2 : 1), 0); const mileageDeduction = totalMiles * (settings.mileageRate || 0); // Today's appointments const today = todayISO(); const todays = signings.filter(s => s.date === today).sort((a,b) => (a.time||'').localeCompare(b.time||'')); const upcoming = signings.filter(s => s.date > today).sort((a,b) => (a.date+a.time).localeCompare(b.date+b.time)).slice(0, 4); // Charts const trendRef = React.useRef(null); const pipelineRef = React.useRef(null); React.useEffect(() => { // 6-month revenue trend from clients (synthetic, based on createdAt month grouping) const months = []; const now = new Date(); for (let i = 5; i >= 0; i--) { const d = new Date(now.getFullYear(), now.getMonth() - i, 1); months.push({ key: d.toISOString().slice(0,7), label: d.toLocaleDateString('en-US',{month:'short'}) }); } const byMonth = Object.fromEntries(months.map(m => [m.key, 0])); [...signings].forEach(s => { const k = (s.date||'').slice(0,7); if (byMonth[k] != null && s.status === 'Completed') byMonth[k] += parseFloat(s.fee)||0; }); clients.forEach(c => { const k = (c.createdAt||'').slice(0,7); if (byMonth[k] != null && c.paid) byMonth[k] += parseFloat(c.revenue)||0; }); // Fallback synthetic data if everything's 0 let vals = months.map(m => byMonth[m.key]); if (vals.every(v => v === 0)) vals = [3200, 4800, 4100, 6300, 5800, 7400]; const ink = '#1D1D1F'; const accent = getComputedStyle(document.body).getPropertyValue('--accent').trim(); const t = new Chart(trendRef.current, { type: 'line', data: { labels: months.map(m => m.label), datasets: [{ data: vals, borderColor: accent, backgroundColor: accent + '22', borderWidth: 2, tension: 0.35, fill: true, pointRadius: 0, pointHoverRadius: 4, pointBackgroundColor: accent }]}, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false }, tooltip: { backgroundColor: '#1C1C1E', cornerRadius: 8, padding: 10, displayColors: false, callbacks: { label: c => '$' + c.parsed.y.toLocaleString() } } }, scales: { y: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.05)', drawBorder: false }, ticks: { color: '#8E8E93', font: { size: 10 }, callback: v => '$' + (v >= 1000 ? (v/1000)+'k' : v) } }, x: { grid: { display: false }, ticks: { color: '#8E8E93', font: { size: 11 } } } } } }); // Pipeline const statusOrder = ['Not Contacted','Contacted','Awaiting Response','Pending','Won','Lost']; const counts = Object.fromEntries(statusOrder.map(s => [s, 0])); clients.forEach(c => { if (counts[c.status] != null) counts[c.status] += 1; }); const colors = ['#8E8E93','#007AFF','#FF9500','#AF52DE','#34C759','#FF3B30']; const p = new Chart(pipelineRef.current, { type: 'bar', data: { labels: statusOrder, datasets: [{ data: statusOrder.map(s => counts[s]), backgroundColor: colors, borderRadius: 6, barThickness: 22 }] }, options: { responsive: true, maintainAspectRatio: false, indexAxis: 'y', plugins: { legend: { display: false }, tooltip: { backgroundColor: '#1C1C1E', cornerRadius: 8, padding: 10, displayColors: false } }, scales: { x: { beginAtZero: true, grid: { color: 'rgba(0,0,0,0.05)', drawBorder: false }, ticks: { color: '#8E8E93', font: { size: 10 }, stepSize: 1 } }, y: { grid: { display: false }, ticks: { color: '#3A3A3C', font: { size: 11 } } } } } }); return () => { t.destroy(); p.destroy(); }; }, [clients, signings]); return (
Here's how the practice is performing today.