// Dashboard — the hero screen // ─── Helpers compartilhados de dashboard ──────────────────── function getDayProgress(state) { const tasks = (state && state.tasks) || []; const habits = (state && state.habits) || []; const tasksToday = tasks.filter(t => t.date === 'hoje'); const tasksDone = tasksToday.filter(t => t.done).length; const habitsDone = habits.filter(h => h.done).length; const total = tasksToday.length + habits.length; const done = tasksDone + habitsDone; return { total, done, percent: total > 0 ? (done / total) * 100 : 0 }; } function getNextEvent(state) { const events = (state && state.events) || []; const now = new Date(); const nowMins = now.getHours() * 60 + now.getMinutes(); const enriched = events.map(e => { const startH = Math.floor(e.start || 0); const startM = Math.round(((e.start || 0) % 1) * 60); return { ...e, startMins: startH * 60 + startM }; }); const future = enriched .filter(e => e.startMins > nowMins) .sort((a, b) => a.startMins - b.startMins); return future[0] || null; } function fmtRelativeMins(eventStartMins) { const now = new Date(); const nowMins = now.getHours() * 60 + now.getMinutes(); const diff = eventStartMins - nowMins; if (diff <= 0) return 'agora'; if (diff < 60) return `começa em ${diff} min`; if (diff < 60 * 4) { const h = Math.floor(diff / 60); const m = diff % 60; return `começa em ${h}h${m > 0 ? ` ${m}m` : ''}`; } const startH = Math.floor(eventStartMins / 60); const startM = eventStartMins % 60; return `começa às ${String(startH).padStart(2, '0')}:${String(startM).padStart(2, '0')}`; } // Expor pros outros arquivos (web) sem duplicar window.cdvGetDayProgress = getDayProgress; window.cdvGetNextEvent = getNextEvent; window.cdvFmtRelativeMins = fmtRelativeMins; // ─── Frase motivacional por período do dia ───────────────── const CDV_PHRASES = { manha: [ 'Comece o dia com uma vitória pequena.', 'Sua melhor hora é entre 9h e 11h — use bem.', 'Manhãs são pra prioridades, tardes pra tarefas.', 'Defina sua vitória do dia agora.', 'Tarefas curtas antes do café te dão impulso.', ], tarde: [ 'Foco profundo às 14h rende mais que 2h dispersas.', 'Você está no ritmo — continue.', 'Termine 1 tarefa importante antes do fim da tarde.', 'Faça uma pausa de 5 min se passou 50 focado.', 'Feito é melhor que perfeito.', ], noite: [ 'Feche o dia revisando o que conquistou.', 'Você fez mais do que percebe hoje.', 'Planeje amanhã antes de dormir.', 'Um hábito pequeno antes de dormir vira sequência.', 'Cinco minutos de gratidão antes de descansar.', ], madrugada: [ 'Descansar também é produtividade.', 'Sono profundo > 1h extra trabalhando.', 'Recarregue. Amanhã tem mais.', ], }; function getMotivationalPhrase() { const h = new Date().getHours(); let bucket; if (h >= 5 && h < 12) bucket = 'manha'; else if (h >= 12 && h < 18) bucket = 'tarde'; else if (h >= 18 && h < 22) bucket = 'noite'; else bucket = 'madrugada'; const list = CDV_PHRASES[bucket]; const day = new Date().getDate(); return list[day % list.length]; } // ─── Histórico agregado de hábitos (últimos 7 dias) ──────── function getHabitsSparkData(habits, days = 7) { if (!habits || !habits.length) return []; const out = []; for (let i = 0; i < days; i++) { let count = 0; habits.forEach(h => { const hist = h.history || []; const idx = hist.length - days + i; if (idx >= 0 && hist[idx]) count++; }); out.push(count); } // Se todos os pontos são zero, considera "sem dados" e retorna vazio const sum = out.reduce((a, b) => a + b, 0); return sum > 0 ? out : []; } // ─── Meta com prazo mais próximo ─────────────────────────── const CDV_MONTHS = { jan: 0, fev: 1, mar: 2, abr: 3, mai: 4, jun: 5, jul: 6, ago: 7, set: 8, out: 9, nov: 10, dez: 11 }; function parseDeadline(s) { if (!s) return null; const lower = String(s).toLowerCase().trim(); // "Dez 2026" / "Jun 2027" const m1 = lower.match(/^(jan|fev|mar|abr|mai|jun|jul|ago|set|out|nov|dez)[a-z\.]*\s+(\d{4})$/i); if (m1) { const mo = CDV_MONTHS[m1[1].slice(0, 3)]; const yr = parseInt(m1[2], 10); if (mo != null && !isNaN(yr)) return new Date(yr, mo + 1, 0); // último dia do mês } // "1 mês", "3 meses", "1 ano" const m2 = lower.match(/(\d+)\s*(m[eê]s|meses|ano|anos)/i); if (m2) { const n = parseInt(m2[1], 10); const d = new Date(); if (/^m/.test(m2[2])) d.setMonth(d.getMonth() + n); else d.setFullYear(d.getFullYear() + n); return d; } return null; } function getNearestGoal(state) { const goals = (state && state.goals) || []; const now = new Date(); return goals .map(g => ({ g, date: parseDeadline(g.deadline) })) .filter(x => x.date && x.date > now) .sort((a, b) => a.date - b.date)[0] || null; } function daysUntil(date) { if (!date) return null; return Math.max(0, Math.ceil((date - new Date()) / (1000 * 60 * 60 * 24))); } window.cdvGetMotivationalPhrase = getMotivationalPhrase; window.cdvGetHabitsSparkData = getHabitsSparkData; window.cdvGetNearestGoal = getNearestGoal; window.cdvDaysUntil = daysUntil; // ─── Sugestão contextual da Sofia (regras sobre o state) ─── function getSofiaSuggestion(state, opts = {}) { const userName = opts.userName || 'você'; const tasks = (state && state.tasks) || []; const habits = (state && state.habits) || []; const events = (state && state.events) || []; const finance = (state && state.finance) || {}; const now = new Date(); const nowMins = now.getHours() * 60 + now.getMinutes(); const hour = now.getHours(); const greeting = hour < 12 ? `Bom dia, ${userName}` : hour < 18 ? `Boa tarde, ${userName}` : `Boa noite, ${userName}`; // 1) Tarefas alta prioridade pendentes hoje const altaPendentes = tasks.filter(t => t.date === 'hoje' && t.priority === 'alta' && !t.done); if (altaPendentes.length > 0) { return { label: `${greeting} · Sofia sugere`, text: `Você tem ${altaPendentes.length} tarefa${altaPendentes.length === 1 ? '' : 's'} de alta prioridade hoje. Que tal começar por "${altaPendentes[0].title}"?`, action: { kind: 'route', route: 'tasks', label: 'Ver tarefas' }, }; } // 2) Próximo evento começando em menos de 60 min const nextEv = events .map(e => { const sh = Math.floor(e.start || 0); const sm = Math.round(((e.start || 0) % 1) * 60); return { ...e, mins: sh * 60 + sm }; }) .filter(e => e.mins > nowMins && e.mins - nowMins < 60) .sort((a, b) => a.mins - b.mins)[0]; if (nextEv) { const diff = nextEv.mins - nowMins; return { label: `${greeting} · Sofia sugere`, text: `"${nextEv.title}" começa em ${diff} min. Que tal já preparar o que vai precisar?`, action: { kind: 'route', route: 'calendar', label: 'Ver agenda' }, }; } // 3) Cartão acima de 70% do limite if (finance.cartao && finance.cartaoLimite && finance.cartao / finance.cartaoLimite > 0.7) { const pct = Math.round((finance.cartao / finance.cartaoLimite) * 100); return { label: `${greeting} · Alerta financeiro`, text: `Seu cartão está em ${pct}% do limite. Quer ver onde estão os maiores gastos do mês?`, action: { kind: 'route', route: 'finance', label: 'Ver finanças' }, }; } // 4) Tarde/noite com hábitos não feitos const habitosPendentes = habits.filter(h => !h.done); if (hour >= 17 && habitosPendentes.length > 0 && habitosPendentes.length < habits.length) { const lista = habitosPendentes.slice(0, 2).map(h => h.name).join(' e '); return { label: `${greeting} · Sofia sugere`, text: `Faltam ${habitosPendentes.length} hábito${habitosPendentes.length === 1 ? '' : 's'} pra fechar o dia: ${lista}. Bora?`, action: { kind: 'route', route: 'habits', label: 'Ver hábitos' }, }; } // 5) Tarefa com horário próximo (próximas 2h) const taskProx = tasks .filter(t => t.date === 'hoje' && !t.done && t.time && /^\d{1,2}:\d{2}$/.test(t.time)) .map(t => { const [h, m] = t.time.split(':').map(Number); return { ...t, mins: h * 60 + m }; }) .filter(t => t.mins > nowMins && t.mins - nowMins < 120) .sort((a, b) => a.mins - b.mins)[0]; if (taskProx) { const diff = taskProx.mins - nowMins; return { label: `${greeting} · Sofia sugere`, text: `"${taskProx.title}" começa em ${diff < 60 ? `${diff} min` : `${Math.floor(diff / 60)}h${diff % 60 > 0 ? ` ${diff % 60}m` : ''}`}. Iniciar um foco profundo?`, action: { kind: 'focus', label: 'Iniciar foco' }, }; } // 6) Manhã com progresso muito baixo do dia const tasksToday = tasks.filter(t => t.date === 'hoje'); const total = tasksToday.length + habits.length; const done = tasksToday.filter(t => t.done).length + habits.filter(h => h.done).length; const pct = total > 0 ? (done / total) : 0; if (hour < 12 && pct < 0.2 && total > 0) { return { label: `${greeting} · Sofia sugere`, text: 'Comece o dia com uma vitória rápida: marque um hábito ou conclua uma tarefa curta antes do almoço.', action: { kind: 'route', route: 'tasks', label: 'Ver tarefas' }, }; } // 7) Meta com prazo próximo e progresso < 50% const nearest = getNearestGoal(state); if (nearest && nearest.g.progress < 50) { const days = daysUntil(nearest.date); return { label: `${greeting} · Sofia sugere`, text: `Sua meta "${nearest.g.title}" está em ${nearest.g.progress}% e vence em ${days} dias. Que tal avançar uma etapa hoje?`, action: { kind: 'route', route: 'goals', label: 'Ver metas' }, }; } // 8) Default: tudo em dia return { label: `${greeting} · Sofia`, text: 'Tudo em dia por aqui ✨ Que tal revisar suas metas ativas ou planejar amanhã?', action: { kind: 'route', route: 'goals', label: 'Ver metas' }, }; } window.cdvGetSofiaSuggestion = getSofiaSuggestion; // ─── Sparkline ────────────────────────────────────────────── const Sparkline = ({ data, color, height = 22, width = 60 }) => { if (!data || data.length < 2) return null; const max = Math.max(...data, 1); const min = Math.min(...data, 0); const range = (max - min) || 1; const points = data.map((v, i) => { const x = (i / (data.length - 1)) * width; const y = height - ((v - min) / range) * (height - 2) - 1; return `${x.toFixed(1)},${y.toFixed(1)}`; }).join(' '); const lastX = width; const lastY = height - ((data[data.length - 1] - min) / range) * (height - 2) - 1; return ( ); }; window.Sparkline = Sparkline; // ─── Anel animado de progresso do dia ─────────────────────── const DayProgressRing = ({ percent, size = 64, stroke = 6, idSuffix = 'm' }) => { const target = Math.max(0, Math.min(100, percent || 0)); const [shown, setShown] = React.useState(0); React.useEffect(() => { let raf; const startTime = Date.now(); const duration = 900; const startVal = 0; const tick = () => { const elapsed = Date.now() - startTime; const t = Math.min(elapsed / duration, 1); const eased = 1 - Math.pow(1 - t, 3); setShown(startVal + (target - startVal) * eased); if (t < 1) raf = requestAnimationFrame(tick); }; raf = requestAnimationFrame(tick); return () => cancelAnimationFrame(raf); }, [target]); const r = (size - stroke) / 2; const c = 2 * Math.PI * r; const offset = c * (1 - shown / 100); const uid = `dpr-grad-${idSuffix}-${size}`; return (
70 ? 18 : 14, fontWeight: 700, color: CDV.text, letterSpacing: -0.3, backgroundImage: 'linear-gradient(135deg, #C9B5FF, #FF8FB1)', WebkitBackgroundClip: 'text', backgroundClip: 'text', WebkitTextFillColor: 'transparent', color: 'transparent', }}> {Math.round(shown)}%
); }; // ─── Card de sugestão da Sofia ───────────────────────────── const SofiaSuggestionCard = ({ state, userName, onOpenAI, onAction, onDismiss }) => { const sug = getSofiaSuggestion(state, { userName }); return (
{sug.label}
{sug.text}
); }; const DashboardScreen = ({ state, setState, openAI, navigate, showToast, user, profile }) => { const { tasks, habits, finance, xp, level, streak } = state; const tasksToday = tasks.filter(t => t.date === 'hoje'); const tasksDone = tasksToday.filter(t => t.done).length; const habitsDone = habits.filter(h => h.done).length; const dia = new Date().toLocaleDateString('pt-BR', { weekday: 'long', day: 'numeric', month: 'long' }); // Resolve nome do usuário (user → profile → state → fallback) const rawName = (user && user.name) || (profile && profile.name) || (state.profile && state.profile.name) || (user && user.email && user.email.split('@')[0]) || 'você'; const userName = String(rawName).trim().split(/\s+/)[0]; const [showFocus, setShowFocus] = React.useState(false); const dayProgress = getDayProgress(state); const nextEvent = getNextEvent(state); const dispatchIntent = (intent) => { if (intent === 'new-task') { navigate('tasks'); setTimeout(() => window.dispatchEvent(new CustomEvent('cdv:open-new-task')), 60); } else if (intent === 'new-transaction') { navigate('finance'); setTimeout(() => window.dispatchEvent(new CustomEvent('cdv:open-new-transaction')), 60); } else if (intent === 'new-habit') { navigate('habits'); setTimeout(() => window.dispatchEvent(new CustomEvent('cdv:open-new-habit')), 60); } }; const toggleTask = (id) => { setState(s => ({ ...s, tasks: s.tasks.map(t => { if (t.id !== id) return t; const nextDone = !t.done; const today = new Date().toISOString().slice(0, 10); return { ...t, done: nextDone, lastCompletedAt: nextDone && t.recurrence ? today : t.lastCompletedAt, }; }) })); showToast('Tarefa atualizada'); }; const toggleHabit = (id) => { setState(s => ({ ...s, habits: s.habits.map(h => h.id === id ? { ...h, done: !h.done, current: h.done ? Math.max(0, h.current - 1) : Math.min(h.target, h.current + 1) } : h) })); showToast('Hábito marcado'); }; return (
{/* Greeting + day progress */}
{dia}
{(() => { const h = new Date().getHours(); return h < 12 ? 'Bom dia' : h < 18 ? 'Boa tarde' : 'Boa noite'; })()}, {userName.charAt(0).toUpperCase() + userName.slice(1)}
{getMotivationalPhrase()}
{/* Anel de progresso + avatar */}
navigate('profile')} style={{ position: 'relative', cursor: 'pointer' }}> {/* mini avatar dot */}
{(userName || 'L').charAt(0).toUpperCase()}
Progresso do dia
{/* Sofia — sugestão dinâmica baseada no state */}
{ if (!action) return; if (action.kind === 'route' && action.route) { navigate(action.route); } else if (action.kind === 'focus') { setShowFocus(true); } }} onDismiss={() => showToast('Ok, deixo pra próxima ✨')} />
{/* A seguir + Atalhos rápidos */}
navigate('calendar')} />
{/* Stats grid */}
} onClick={() => navigate('tasks')} /> ↗ 12% vs mês passado} icon="wallet" iconColor={CDV.mint} onClick={() => navigate('finance')} /> } sparkData={getHabitsSparkData(habits)} sparkColor={CDV.flame} onClick={() => navigate('habits')} /> navigate('achievements')} />
{/* Quick actions complementares (voz + foco) */}
setShowFocus(true)} /> navigate('finance')} />
{/* Today's agenda */} navigate('calendar')} />
{tasksToday.filter(t => !t.done).slice(0, 3).map(t => ( ))}
{/* Habits today */} navigate('habits')} />
{habits.map(h => ( ))}
{/* Active goal */} {state.goals && state.goals.length > 0 && ( <> navigate('goals')} />
navigate('goals')} />
)} {/* Meta com prazo mais próximo (se for diferente da principal) */} navigate('goals')} /> setShowFocus(false)} title="Modo Foco · Pomodoro"> { showToast && showToast('Pomodoro concluído ✨'); setShowFocus(false); }} />
); }; // ── Widget "Meta com prazo mais próximo" ──────────────────── const NearestGoalWidget = ({ state, onClick }) => { const nearest = getNearestGoal(state); if (!nearest) return null; // Se for igual à meta principal já exibida, não duplica const principal = (state.goals && state.goals[0]) || null; if (principal && nearest.g.id === principal.id) return null; const g = nearest.g; const days = daysUntil(nearest.date); const catColor = (window.GOAL_CAT_COLOR && window.GOAL_CAT_COLOR[g.category]) || (g.category === 'financeiro' ? CDV.mint : g.category === 'saude' ? CDV.coral : g.category === 'viagem' ? '#FF8FB1' : CDV.sky); return ( <>
{g.category}
{g.title}
{g.progress}% concluído
{days === 0 ? 'vence hoje' : days === 1 ? 'vence amanhã' : `${days} dias restantes`}
); }; // ── Pomodoro Timer ────────────────────────────────────────── const PomodoroTimer = ({ onFinish }) => { const FOCUS = 25 * 60; const BREAK = 5 * 60; const [mode, setMode] = React.useState('focus'); // focus | break const [remaining, setRemaining] = React.useState(FOCUS); const [running, setRunning] = React.useState(false); const [cycles, setCycles] = React.useState(0); const intervalRef = React.useRef(null); React.useEffect(() => { if (!running) return; intervalRef.current = setInterval(() => { setRemaining(r => { if (r <= 1) { clearInterval(intervalRef.current); if (mode === 'focus') { setCycles(c => c + 1); setMode('break'); setRunning(false); onFinish && onFinish(); return BREAK; } else { setMode('focus'); setRunning(false); return FOCUS; } } return r - 1; }); }, 1000); return () => clearInterval(intervalRef.current); }, [running, mode]); const reset = () => { setRunning(false); setRemaining(mode === 'focus' ? FOCUS : BREAK); }; const total = mode === 'focus' ? FOCUS : BREAK; const pct = ((total - remaining) / total) * 100; const mm = String(Math.floor(remaining / 60)).padStart(2, '0'); const ss = String(remaining % 60).padStart(2, '0'); const tint = mode === 'focus' ? CDV.coral : CDV.mint; return (
{mode === 'focus' ? 'Foco profundo · 25 min' : 'Pausa curta · 5 min'}
{mm}:{ss}
{cycles} ciclo{cycles === 1 ? '' : 's'} hoje
Sem distrações · O cronômetro segue mesmo se fechar este painel
); }; // ── Card "A seguir" ──────────────────────────────────────── const NextUpCard = ({ event, onClick }) => { const empty = !event; const c = CDV.brand; return (
A seguir
{empty ? ( <>
Nada na agenda agora
aproveite o tempo livre
) : ( <>
{event.title}
{fmtRelativeMins(event.startMins)}
)}
); }; // ── 3 atalhos rápidos do dashboard ───────────────────────── const DashShortcuts = ({ onDispatch }) => { const items = [ { id: 'new-task', icon: 'plus', label: 'Tarefa', color: CDV.brand }, { id: 'new-transaction', icon: 'wallet', label: 'Gasto', color: CDV.coral }, { id: 'new-habit', icon: 'flame', label: 'Hábito', color: CDV.flame }, ]; return (
{items.map(it => ( ))}
); }; // ── Stat tile (premium) ──────────────────────────────────── const StatTile = ({ label, big, extra, ring, icon, iconColor, onClick, sparkData, sparkColor }) => { const c = iconColor || CDV.brand; const [pressed, setPressed] = React.useState(false); return ( ); }; // ── Quick action chip ────────────────────────────────────── const QuickAction = ({ icon, label, color = CDV.text, onClick }) => ( ); // ── Task inline row ──────────────────────────────────────── const TaskInlineRow = ({ task, onToggle, onClick }) => { const catColor = CDV.cats[task.category] || CDV.brand; return (
onToggle(task.id)} color={catColor} />
{task.title}
{task.time}
{task.priority === 'alta' && Alta} {task.aiSuggested && IA}
); }; // ── Habit chip ───────────────────────────────────────────── const HabitChip = ({ habit, onToggle }) => { const done = habit.done; return ( ); }; // ── Goal hero card ───────────────────────────────────────── const GoalHeroCard = ({ goal, onClick }) => (
Meta principal
{goal.title}
R$ {goal.savedReais.toLocaleString('pt-BR')} / R$ {goal.targetReais.toLocaleString('pt-BR')}
{goal.progress}%
Prazo: {goal.deadline} · No ritmo certo
); Object.assign(window, { DashboardScreen });