// Dashboard — the hero page function Dashboard() { const { setPage, setDrawerTxn, dataVersion } = useApp(); const [scrapeOpen, setScrapeOpen] = useState(false); const [scrapeProvider, setScrapeProvider] = useState('max'); const [aiBusy, setAiBusy] = useState(false); const openConnect = (provider) => { setScrapeProvider(provider); setScrapeOpen(true); }; const [range, setRange] = useState('1m'); // '1m' | '3m' | '1y' const monthsAll = D.months || []; // Default end-month = most recent month that actually has transactions // (so the user lands on real data instead of an empty current month). const initialIdx = useMemo(() => { for (let i = monthsAll.length - 1; i >= 0; i--) { if ((monthsAll[i].txns?.length || 0) > 0) return i; } return Math.max(0, monthsAll.length - 1); }, [dataVersion]); const [monthIdx, setMonthIdx] = useState(initialIdx); // Re-clamp on data refresh (lengths may change after a scrape). useEffect(() => { if (monthIdx >= monthsAll.length) setMonthIdx(Math.max(0, monthsAll.length - 1)); }, [dataVersion]); // Compute the list of months in the active range, ending at monthIdx. const rangeLen = range === '1y' ? 12 : range === '3m' ? 3 : 1; const activeMonths = useMemo(() => { const end = monthIdx + 1; const start = Math.max(0, end - rangeLen); return monthsAll.slice(start, end); }, [monthIdx, range, dataVersion]); const endMonth = monthsAll[monthIdx] || D.current; const startMonth = activeMonths[0] || endMonth; // Aggregate stats across activeMonths. const agg = useMemo(() => { const expenses = activeMonths.reduce((s, m) => s + (m.summary?.expenses || 0), 0); const income = activeMonths.reduce((s, m) => s + (m.summary?.income || 0), 0); const txns = activeMonths.flatMap(m => m.txns || []); const byCat = {}; txns.filter(t => t.amount < 0).forEach(t => { byCat[t.category] = (byCat[t.category] || 0) + (-t.amount); }); return { expenses, income, net: income - expenses, byCat, txns, count: txns.length }; }, [activeMonths]); // Previous comparable period (same length, immediately before start). const prevPeriod = useMemo(() => { const endIdx = Math.max(0, monthIdx - rangeLen + 1); const startIdx = Math.max(0, endIdx - rangeLen); const prevMs = monthsAll.slice(startIdx, endIdx); const expenses = prevMs.reduce((s, m) => s + (m.summary?.expenses || 0), 0); return { expenses, months: prevMs }; }, [monthIdx, range, dataVersion]); const now = new Date(); const isCurrentMonth = endMonth?.year === now.getFullYear() && endMonth?.month === now.getMonth() + 1; const daysInMonth = endMonth ? new Date(endMonth.year, endMonth.month, 0).getDate() : 30; const today = isCurrentMonth ? Math.min(now.getDate(), daysInMonth) : daysInMonth; const daily = useDailyPace(endMonth || { txns: [] }, today); // Projection only meaningful for current month, single-month view. const showProjection = range === '1m' && isCurrentMonth && today > 0 && today < daysInMonth; const projected = showProjection ? (daily[today-1] || 0) / today * daysInMonth : 0; const projectedDelta = prevPeriod.expenses ? (((showProjection ? projected : agg.expenses) - prevPeriod.expenses) / prevPeriod.expenses) * 100 : 0; const topCats = useMemo( () => Object.entries(agg.byCat).sort((a,b) => b[1]-a[1]).slice(0, 5), [agg] ); const recent = useMemo( () => agg.txns.slice().sort((a,b) => b.date.localeCompare(a.date)).slice(0, 6), [agg] ); const featuredInsight = D.insights?.bullets?.[0]; const hasSavings = (D.savings?.items?.length || 0) > 0; const hasAccounts = (D.accounts?.length || 0) > 0; // Title text depends on the active range. const HEB_MONTHS = ['ינואר','פברואר','מרץ','אפריל','מאי','יוני','יולי','אוגוסט','ספטמבר','אוקטובר','נובמבר','דצמבר']; const heroLabel = range === '1m' ? (endMonth ? `${HEB_MONTHS[endMonth.month-1]} ${endMonth.year}` : '') : range === '3m' ? `3 חודשים אחרונים` : `12 חודשים אחרונים`; const titleText = range === '1m' ? (isCurrentMonth ? `הנה איך ${HEB_MONTHS[endMonth.month-1]} הולך` : `סקירת ${HEB_MONTHS[endMonth.month-1]} ${endMonth.year}`) : range === '3m' ? 'סקירת 3 החודשים האחרונים' : 'סקירת 12 החודשים האחרונים'; return (
{/* Page header */}
{(() => { const h = new Date().getHours(); const greet = h < 5 ? 'לילה טוב' : h < 12 ? 'בוקר טוב' : h < 17 ? 'צהריים טובים' : h < 21 ? 'ערב טוב' : 'לילה טוב'; const name = (window.APP_USER?.name || '').split(' ')[0] || ''; return name ? `${greet}, ${name}` : greet; })()}

{titleText}

{D.aiStatus?.configured && ( )} {scrapeOpen && setScrapeOpen(false)} />}
{/* Month picker — selects single month (1m) or end-month of range (3m/1y) */}
{monthsAll.map((m, i) => { const active = i === monthIdx; const isEmpty = (m.txns?.length || 0) === 0; // Highlight months that are part of the current range (not just the end month). const inRange = !active && i >= Math.max(0, monthIdx - rangeLen + 1) && i <= monthIdx; return ( ); })}
{/* Hero — chart adapts: pace (1m) or multi-month line (3m/1y) */}
{range === '1m' ? `הוצאות מצטברות · ${heroLabel}` : `סך הוצאות · ${heroLabel}`}
{showProjection ? ( <> צפי לסוף החודש {fmtILS(projected)} ) : prevPeriod.expenses > 0 ? ( <> תקופה קודמת {fmtILS(prevPeriod.expenses)} ) : ( אין השוואה זמינה )} {(showProjection || prevPeriod.expenses > 0) && ( 0 ? 'rgba(216,58,58,0.1)' : 'rgba(48,164,108,0.1)', color: projectedDelta > 0 ? 'var(--alert)' : 'var(--positive)', }}> {projectedDelta > 0 ? '↑' : '↓'} {Math.abs(projectedDelta).toFixed(1)}% )}
{range === '1m' && (
{showProjection && }
)}
{range === '1m' ? ( ) : ( )}
{range === '1m' ? (
{isCurrentMonth ? 'ימים שנותרו בחודש' : 'מספר עסקאות'}
{isCurrentMonth ? `${Math.max(0, daysInMonth - today)} ימים` : `${agg.count}`}
{endMonth?.label}
) : (
טווח
{startMonth?.label} – {endMonth?.label}
{agg.count} עסקאות
)}
{/* Savings opportunity teaser — only show if Cai found something */} {hasSavings && setPage('savings')} style={{ cursor: 'pointer', marginBottom: 16, padding: 0, overflow: 'hidden', }} padded={false} >
Cai מצא איפה אפשר לחסוך
{D.savings.items.length} הזדמנויות זוהו · עד {fmtILS(D.savings.annualPotential)} בשנה
{/* Quick wins highlight */} {D.savings.items.slice(0, 3).map((s) => { const cat = D.categories[s.category] || { icon: '·', color: 'var(--ink-soft)' }; return (
{cat.icon}
{s.title}
{fmtILS(s.annual)}/שנה
); })}
ראה הכל ‹
} {/* Featured AI insight + Accounts */}
setPage('ai')} style={{ cursor: 'pointer', position: 'relative', overflow: 'hidden' }}>
דוח Cai · {(() => { const c = D.insights?.createdAt; if (!c) return 'עוד לא נוצר'; const diff = Math.max(0, Date.now() - new Date(c).getTime()); const m = Math.round(diff / 60000); if (m < 1) return 'עודכן זה עתה'; if (m < 60) return `עודכן לפני ${m} דקות`; const h = Math.round(m / 60); if (h < 24) return `עודכן לפני ${h} שעות`; const d = Math.round(h / 24); return d === 1 ? 'עודכן אתמול' : `עודכן לפני ${d} ימים`; })()}
{D.insights?.summary}
{featuredInsight && (
{featuredInsight.kind === 'warning' ? '!' : featuredInsight.kind === 'good' ? '✓' : 'i'}
{featuredInsight.title}
{featuredInsight.text}
)}
{/* Cats + Recent */}
איפה הכסף הולך
5 קטגוריות מובילות
setPage('budgets')} style={{ fontSize: 11, color: 'var(--accent)', cursor: 'pointer' }}>הצג הכל ‹
{topCats.map(([cat, val]) => { const lim = D.budgets[cat]; const pct = lim ? val/lim : 0; const c = D.categories[cat] || {}; const over = pct > 1; return (
{c.icon} {cat} {fmtILS(val)}
{lim && {Math.round(pct*100)}%}
); })}
פעילות אחרונה
{agg.count} עסקאות {range === '1m' ? 'החודש' : 'בתקופה'}
setPage('txns')} style={{ fontSize: 11, color: 'var(--accent)', cursor: 'pointer' }}>הצג הכל ‹
{recent.map((t, i) => { const c = D.categories[t.category] || {}; return (
setDrawerTxn(t)} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 22px', cursor: 'pointer', borderTop: i === 0 ? '0.5px solid var(--rule)' : '0.5px solid var(--rule)', transition: 'background .14s', }} onMouseEnter={(e) => e.currentTarget.style.background = 'var(--bg-deep)'} onMouseLeave={(e) => e.currentTarget.style.background = 'transparent'}>
{c.icon}
{t.anomaly && !} {t.description}
{fmtDate(t.date)} · {t.category}
0 ? 'var(--positive)' : 'var(--ink)' }}> {fmtILS(t.amount)}
); })}
); } // Per-provider connection list with Sync/Connect buttons. // All 4 supported providers always render — connected ones with their accounts, // disconnected ones with a "התחבר" CTA that opens the modal pre-selected. function ConnectionsCard({ accounts, onConnect }) { const PROVIDERS = [ { id: 'max', label: 'Max', color: '#0a84ff', sub: 'מקס' }, { id: 'visaCal', label: 'Visa Cal', color: '#5856d6', sub: 'ויזה כאל' }, { id: 'isracard', label: 'Isracard', color: '#ff375f', sub: 'ישראכרט (כולל Visa)' }, { id: 'amex', label: 'American Express', color: '#1a73e8', sub: 'אמריקן אקספרס' }, ]; // Group accounts by source const byProv = {}; accounts.forEach(a => { (byProv[a.source] ||= []).push(a); }); const connectedCount = PROVIDERS.filter(p => byProv[p.id]).length; return ( <>
חיבורים
{connectedCount} מתוך {PROVIDERS.length} מחוברים
{PROVIDERS.map(p => { const accts = byProv[p.id] || []; const isConnected = accts.length > 0; const totalBalance = accts.reduce((s, a) => s + a.balance, 0); return (
{p.label.slice(0,3).toUpperCase()}
{p.label} {isConnected && }
{isConnected ? accts.map(a => a.holder).join(' · ') : p.sub}
{isConnected ? ( <>
{fmtILS(totalBalance)}
) : ( )}
); })}
); } function LegendDot({ label, filled, dashed }) { return ( {filled && } {dashed && } {label} ); } function SideStat({ label, value, positive, muted }) { return (
{label}
); } window.Dashboard = Dashboard;