// 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 });