// Savings Recommendations page — Cai-curated, actionable
function SavingsPage() {
const [filter, setFilter] = useState('all'); // all | easy | high
const [accepted, setAccepted] = useState({});
const [dismissed, setDismissed] = useState({});
const [expanded, setExpanded] = useState(null);
const [busy, setBusy] = useState(false);
const savings = D.savings || {};
const allItems = savings.items || [];
const aiConfigured = D.aiStatus?.configured;
const hasReport = allItems.length > 0;
const items = useMemo(() => {
let arr = allItems.filter(i => !dismissed[i.id]);
if (filter === 'easy') arr = arr.filter(i => i.effort === 'easy');
if (filter === 'high') arr = arr.filter(i => i.impact === 'high');
if (filter === 'subs') arr = arr.filter(i => i.category === 'חשבונות ומנויים');
// sort by impact * confidence desc
const score = (i) => (i.annual || 0) * (i.confidence || 0.5) * (i.effort === 'easy' ? 1.2 : i.effort === 'medium' ? 1 : 0.8);
return arr.sort((a, b) => score(b) - score(a));
}, [filter, dismissed, hasReport]);
const acceptedItems = allItems.filter(i => accepted[i.id]);
const acceptedAnnual = acceptedItems.reduce((s, i) => s + (i.annual || 0), 0);
const acceptedMonthly = acceptedItems.reduce((s, i) => s + (i.monthly || 0), 0);
const totalAvailableAnnual = items.reduce((s, i) => s + (i.annual || 0), 0);
const generate = async () => {
if (!aiConfigured) {
alert('AI לא מוגדר. הגדר ANTHROPIC_API_KEY ב-environment והפעל את השרת מחדש.');
return;
}
if (busy) return;
if (hasReport && !confirm('הניתוח הקיים יוחלף בניתוח חדש (יקח כדקה). להמשיך?')) return;
setBusy(true);
try {
const r = await apiFetch('/api/ai/savings', { method: 'POST' });
if (!r.ok) {
const err = await r.json().catch(() => ({}));
throw new Error(err.detail?.message || 'הניתוח נכשל');
}
await window.APP_DATA_REFRESH();
} catch (err) {
alert('שגיאה בניתוח: ' + err.message);
} finally {
setBusy(false);
}
};
const updatedLabel = savings.createdAt
? new Date(savings.createdAt).toLocaleString('he-IL', { dateStyle: 'short', timeStyle: 'short' })
: null;
const modelLabel = savings.model || D.aiStatus?.model || 'Cai';
return (
{/* Header */}
איפה אפשר לחסוך
המלצות מבוססות נתונים · 12 חודשים אחרונים · {updatedLabel ? `עודכן ${updatedLabel}` : 'לא נוצר עדיין'} · {modelLabel}
{busy ? '⟳ Cai מנתח…' : (hasReport ? '↻ נתח שוב' : '✦ נתח עכשיו')}
{/* Hero — total potential */}
פוטנציאל חיסכון שנתי
{savings.summary}
{/* Accepted progress bar */}
{acceptedItems.length > 0 && (
✓
סימנת {acceptedItems.length} מתוך {allItems.length} המלצות לביצוע
חיסכון מצטבר: {fmtILS(acceptedMonthly)}/חודש · {fmtILS(acceptedAnnual)} בשנה
)}
{/* Filter chips */}
setFilter('all')} />
setFilter('easy')} icon="⚡" />
setFilter('high')} icon="↑" />
setFilter('subs')} />
מציג {items.length} · סך פוטנציאל {fmtILS(totalAvailableAnnual)}/שנה
{/* Recommendations list */}
{items.map((it, i) => (
setExpanded(expanded === it.id ? null : it.id)}
onAccept={() => setAccepted(a => ({ ...a, [it.id]: !a[it.id] }))}
onDismiss={() => setDismissed(d => ({ ...d, [it.id]: true }))}
/>
))}
{items.length === 0 && (
)}
{/* Footer note */}
איך החישוב נעשה: {' '}
ההמלצות מבוססות על ניתוח של 12 חודשים אחרונים, הצלבה עם תעריפי שוק, ודפוסי שימוש שזוהו אוטומטית. רמת הביטחון משקפת את איכות הנתונים — לא הבטחה. אתה תמיד שומר על שליטה: שום פעולה לא מתבצעת אוטומטית.
);
}
function SavingsHeroStat({ label, value, sub, accent }) {
return (
);
}
function FilterChip({ label, on, onClick, icon }) {
return (
{icon && {icon} }{label}
);
}
function SavingItem({ item, rank, isAccepted, isExpanded, onToggle, onAccept, onDismiss }) {
const cat = D.categories[item.category] || { color: 'var(--ink-soft)', icon: '·' };
const effortLabel = { easy: 'קל', medium: 'בינוני', hard: 'מורכב' }[item.effort];
const impactLabel = { low: 'נמוך', medium: 'בינוני', high: 'גבוה' }[item.impact];
const impactColor = { low: 'var(--ink-soft)', medium: 'var(--accent)', high: 'var(--positive)' }[item.impact];
return (
{/* Rank + category icon */}
{/* Body */}
{item.title}
{isAccepted &&
}
{item.category} · {item.sub}
{/* Meta strip */}
{/* Annual savings */}
בשנה
{fmtILS(item.annual)}
{isExpanded ? '⌃ סגור' : '⌄ הרחב'}
{/* Expanded detail */}
{isExpanded && (
{/* Why */}
{/* Evidence */}
{item.evidence && (
ראיות
{item.evidence.map((e, i) => {e} )}
)}
{/* Actions */}
{ e.stopPropagation(); onAccept(); }}
style={{
padding: '9px 16px', borderRadius: 9,
background: isAccepted ? 'var(--surface)' : 'var(--ink)',
color: isAccepted ? 'var(--ink)' : 'var(--bg)',
border: isAccepted ? '0.5px solid var(--rule)' : 'none',
fontSize: 12.5, fontWeight: 600, cursor: 'pointer',
display: 'inline-flex', alignItems: 'center', gap: 6,
}}>
{isAccepted ? '↩ בטל סימון' : `✓ ${item.action}`}
הזכר לי בעוד שבוע
{ e.stopPropagation(); onDismiss(); }}
style={{
padding: '9px 14px', borderRadius: 9, background: 'transparent',
border: 'none', fontSize: 12, cursor: 'pointer', color: 'var(--ink-soft)',
marginInlineStart: 'auto',
}}>לא רלוונטי
)}
);
}
function Meta({ label, value, accent, dot }) {
return (
{label}
{dot && }
{value}
);
}
function MetaDivider() {
return ;
}
function ConfidenceBar({ value }) {
const pct = Math.round(value * 100);
return (
);
}
function Pill({ color, label }) {
return (
{label}
);
}
window.SavingsPage = SavingsPage;