// Tasks screen — list, filter, IA suggestions, new task sheet
// Helpers de recorrência (compartilhados via window)
const todayISO = () => new Date().toISOString().slice(0, 10);
const RECURRENCE_LABEL = {
diaria: 'Todos os dias',
'dias-uteis': 'Dias úteis',
'fim-de-semana': 'Fim de semana',
semanal: 'Semanal',
mensal: 'Mensal',
};
// Decide se uma tarefa recorrente deve aparecer hoje
function recurrenceAppliesToday(rec) {
if (!rec) return true;
const dow = new Date().getDay(); // 0 dom .. 6 sab
if (rec === 'diaria') return true;
if (rec === 'dias-uteis') return dow >= 1 && dow <= 5;
if (rec === 'fim-de-semana') return dow === 0 || dow === 6;
if (rec === 'semanal' || rec === 'mensal') return true; // simplificado: sempre visível
return true;
}
// Reseta tarefas recorrentes cujo lastCompletedAt é de outro dia
function resetRecurringTasks(tasks) {
const today = todayISO();
let changed = false;
const next = tasks.map(t => {
if (!t.recurrence) return t;
if (t.lastCompletedAt && t.lastCompletedAt !== today && t.done) {
changed = true;
return { ...t, done: false };
}
return t;
});
return changed ? next : tasks;
}
window.recurrenceAppliesToday = recurrenceAppliesToday;
window.resetRecurringTasks = resetRecurringTasks;
window.RECURRENCE_LABEL = RECURRENCE_LABEL;
const TasksScreen = ({ state, setState, openAI, navigate, showToast }) => {
const [filter, setFilter] = React.useState('hoje');
const [showNew, setShowNew] = React.useState(false);
const [detailId, setDetailId] = React.useState(null);
// Reset diário ao abrir a tela
React.useEffect(() => {
const next = resetRecurringTasks(state.tasks);
if (next !== state.tasks) setState(s => ({ ...s, tasks: next }));
}, []);
// Intent: abrir modal de nova tarefa via evento global (atalho do dashboard)
React.useEffect(() => {
const handler = () => setShowNew(true);
window.addEventListener('cdv:open-new-task', handler);
return () => window.removeEventListener('cdv:open-new-task', handler);
}, []);
const filtered = state.tasks.filter(t => {
if (t.date !== filter) return false;
if (t.recurrence && !recurrenceAppliesToday(t.recurrence)) return false;
return true;
});
const done = filtered.filter(t => t.done).length;
const pct = filtered.length ? (done / filtered.length) * 100 : 0;
const detailTask = state.tasks.find(t => t.id === detailId) || null;
const toggleTask = (id) => {
setState(s => ({
...s,
tasks: s.tasks.map(t => {
if (t.id !== id) return t;
const nextDone = !t.done;
return {
...t,
done: nextDone,
lastCompletedAt: nextDone && t.recurrence ? todayISO() : t.lastCompletedAt,
};
})
}));
showToast('Tarefa atualizada');
};
const toggleSubtask = (taskId, subtaskId) => {
setState(s => ({
...s,
tasks: s.tasks.map(t =>
t.id === taskId
? { ...t, subtasks: t.subtasks.map(sub => sub.id === subtaskId ? { ...sub, done: !sub.done } : sub) }
: t
)
}));
};
const addSubtask = (taskId, title) => {
setState(s => ({
...s,
tasks: s.tasks.map(t =>
t.id === taskId
? { ...t, subtasks: [...(t.subtasks || []), { id: 'st' + Date.now(), title, done: false }] }
: t
)
}));
};
const deleteTask = (id) => {
setState(s => ({ ...s, tasks: s.tasks.filter(t => t.id !== id) }));
setDetailId(null);
showToast('Tarefa excluída');
};
const updateTaskField = (id, patch) => {
setState(s => ({ ...s, tasks: s.tasks.map(t => t.id === id ? { ...t, ...patch } : t) }));
};
return (
setShowNew(true)} style={{
background: CDV.brandGrad, border: 'none', width: 38, height: 38, borderRadius: 99,
display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff',
boxShadow: '0 6px 20px -6px rgba(177,151,252,0.6)',
}}>
} />
{/* Progress strip */}
{done}
de {filtered.length} concluídas
+85 XP
{/* Filter pills */}
{[
{ id: 'hoje', label: 'Hoje' },
{ id: 'amanha', label: 'Amanhã' },
{ id: 'semana', label: 'Esta semana' },
{ id: 'todos', label: 'Todos' },
].map(f => (
))}
{/* AI suggestion banner */}
Sofia sugere reorganizar suas tarefas
Você produz mais entre 9h e 11h. Posso reagendar 2 tarefas?
{/* Task groups */}
{filtered.filter(t => t.priority === 'alta').map(t => (
setDetailId(t.id)} />
))}
{filtered.filter(t => t.priority === 'alta').length === 0 && Nenhuma tarefa de alta prioridade.}
{filtered.filter(t => t.priority !== 'alta').map(t => (
setDetailId(t.id)} />
))}
setShowNew(false)} title="Nova tarefa">
{
setState(s => ({ ...s, tasks: [...s.tasks, { ...task, id: 'tn' + Date.now() }] }));
setShowNew(false);
showToast('Tarefa criada');
}}
onVoice={() => {
setShowNew(false);
openAI();
}}
/>
setDetailId(null)} title="Detalhes">
{detailTask && (
setDetailId(null)}
/>
)}
);
};
const TaskRow = ({ task, onToggle, onOpenDetail }) => {
const catColor = CDV.cats[task.category] || CDV.brand;
const [pressed, setPressed] = React.useState(false);
return (
setPressed(true)}
onTouchEnd={() => setPressed(false)}
onMouseDown={() => setPressed(true)}
onMouseUp={() => setPressed(false)}
onMouseLeave={() => setPressed(false)}
style={{
borderRadius: 16,
background: task.done
? CDV.surface
: `linear-gradient(135deg, ${catColor}0d 0%, ${CDV.surface} 50%)`,
border: `1px solid ${task.done ? CDV.stroke : catColor + '22'}`,
overflow: 'hidden', cursor: 'pointer', position: 'relative',
transform: pressed ? 'scale(0.98)' : 'scale(1)',
transition: 'transform .15s, box-shadow .2s',
boxShadow: task.done ? 'none' : `0 4px 14px -10px ${catColor}40`,
opacity: task.done ? 0.7 : 1,
}}
>
{/* Barra lateral categoria */}
{!task.done && (
)}
e.stopPropagation()}>
onToggle(task.id)} color={catColor} />
{task.title}
{task.category}
{task.time}
{task.duration && ` · ${task.duration}min`}
{task.priority === 'alta' &&
Alta}
{task.aiSuggested &&
IA}
{task.recurrence && (
{RECURRENCE_LABEL[task.recurrence] || 'recorrente'}
)}
{task.subtasks && task.subtasks.length > 0 && (
{task.subtasks.filter(s => s.done).length}/{task.subtasks.length} subtarefas
)}
);
};
// ── Task detail (sheet) ──────────────────────────────────────
const TaskDetail = ({ task, onToggle, onToggleSubtask, onAddSubtask, onUpdate, onDelete, onClose }) => {
const catColor = CDV.cats[task.category] || CDV.brand;
const priColor = task.priority === 'alta' ? CDV.coral : task.priority === 'media' ? CDV.amber : CDV.sky;
const [newSub, setNewSub] = React.useState('');
const [editing, setEditing] = React.useState(false);
const [draftTitle, setDraftTitle] = React.useState(task.title);
const [draftTime, setDraftTime] = React.useState(task.time === '—' ? '' : task.time);
const [draftPriority, setDraftPriority] = React.useState(task.priority);
const [draftCategory, setDraftCategory] = React.useState(task.category);
const [draftRecurrence, setDraftRecurrence] = React.useState(task.recurrence || 'unico');
const cats = ['trabalho', 'pessoal', 'saude', 'estudo', 'casa', 'lazer', 'outros'];
const subs = task.subtasks || [];
const doneSubs = subs.filter(s => s.done).length;
const saveEdit = () => {
onUpdate && onUpdate(task.id, {
title: draftTitle.trim() || task.title,
time: draftTime || '—',
priority: draftPriority,
category: draftCategory,
recurrence: draftRecurrence === 'unico' ? null : draftRecurrence,
});
setEditing(false);
};
const confirmDelete = () => {
if (window.confirm && window.confirm('Excluir esta tarefa? Esta ação não pode ser desfeita.')) {
onDelete && onDelete(task.id);
}
};
return (
{/* Status / Título */}
{!editing ? (
<>
{task.category}
{task.priority === 'alta' ? 'Alta prioridade' : task.priority === 'media' ? 'Média' : 'Baixa'}
{task.recurrence && (
{RECURRENCE_LABEL[task.recurrence] || 'recorrente'}
)}
{task.aiSuggested &&
Sugerido pela IA}
{task.title}
>
) : (
<>
setDraftTitle(e.target.value)} placeholder="Título"
style={{
width: '100%', height: 54, borderRadius: 14, border: `1px solid ${CDV.stroke}`,
background: CDV.surfaceHi, color: CDV.text, padding: '0 16px', fontSize: 16,
outline: 'none', fontFamily: 'inherit', marginBottom: 12,
}} />
Categoria
{cats.map(c => {
const active = draftCategory === c;
const cc = CDV.cats[c] || CDV.brand;
return (
);
})}
Prioridade
{[{ id: 'baixa', label: 'Baixa', c: CDV.sky }, { id: 'media', label: 'Média', c: CDV.amber }, { id: 'alta', label: 'Alta', c: CDV.coral }].map(p => {
const active = draftPriority === p.id;
return (
);
})}
Recorrência
{RECURRENCE_OPTIONS.map(r => {
const active = draftRecurrence === r.id;
return (
);
})}
>
)}
{/* Stats */}
{!editing ? (
) : (
)}
{/* Subtarefas */}
{(subs.length > 0 || !editing) && (
<>
Subtarefas {subs.length > 0 && `· ${doneSubs}/${subs.length}`}
{subs.length > 0 && (
)}
{subs.map(s => (
onToggleSubtask && onToggleSubtask(task.id, s.id)} color={catColor} size={18} />
{s.title}
))}
setNewSub(e.target.value)} placeholder="+ Adicionar subtarefa"
onKeyDown={e => { if (e.key === 'Enter' && newSub.trim()) { onAddSubtask(task.id, newSub.trim()); setNewSub(''); } }}
style={{
flex: 1, height: 42, borderRadius: 10, border: `1px solid ${CDV.stroke}`,
background: CDV.surfaceHi, color: CDV.text, padding: '0 12px', fontSize: 13,
outline: 'none', fontFamily: 'inherit',
}} />
{newSub.trim() && (
)}
>
)}
{/* Ações principais */}
{!editing ? (
<>
>
) : (
<>
>
)}
{!editing && (
)}
);
};
const Empty = ({ children }) => (
{children}
);
const RECURRENCE_OPTIONS = [
{ id: 'unico', label: 'Único', icon: 'check' },
{ id: 'diaria', label: 'Todos os dias', icon: 'refresh' },
{ id: 'dias-uteis', label: 'Dias úteis', icon: 'briefcase' },
{ id: 'fim-de-semana', label: 'Fim de semana', icon: 'star' },
{ id: 'semanal', label: 'Semanal', icon: 'calendar' },
{ id: 'mensal', label: 'Mensal', icon: 'calendar' },
];
const NewTaskForm = ({ onSubmit, onVoice }) => {
const [title, setTitle] = React.useState('');
const [cat, setCat] = React.useState('trabalho');
const [priority, setPriority] = React.useState('media');
const [time, setTime] = React.useState('14:00');
const [recurrence, setRecurrence] = React.useState('unico');
const cats = ['trabalho', 'pessoal', 'saude', 'estudo', 'casa', 'lazer', 'outros'];
const pris = [{ id: 'baixa', label: 'Baixa', c: CDV.sky }, { id: 'media', label: 'Média', c: CDV.amber }, { id: 'alta', label: 'Alta', c: CDV.coral }];
return (
setTitle(e.target.value)} placeholder="Ex: Ligar para o Roberto às 15h"
style={{
width: '100%', height: 54, borderRadius: 16, border: `1px solid ${CDV.stroke}`,
background: CDV.surfaceHi, color: CDV.text, padding: '0 50px 0 16px', fontSize: 15,
outline: 'none', fontFamily: 'inherit',
}} />
A IA detecta automaticamente data, hora e prioridade
Categoria
{cats.map(c => {
const active = cat === c;
const cc = CDV.cats[c];
return (
);
})}
Prioridade
{pris.map(p => {
const active = priority === p.id;
return (
);
})}
Recorrência
{RECURRENCE_OPTIONS.map(r => {
const active = recurrence === r.id;
return (
);
})}
);
};
const FieldCard = ({ label, value, icon }) => (
);
const TimeInputCard = ({ label, value, onChange, icon }) => {
const ref = React.useRef(null);
const openPicker = () => {
if (ref.current) {
try { ref.current.showPicker && ref.current.showPicker(); } catch (e) {}
ref.current.focus();
}
};
return (
{label}
onChange(e.target.value)}
style={{
background: 'transparent', border: 'none', outline: 'none', padding: 0, marginTop: 1,
color: CDV.text, fontSize: 14, fontWeight: 500, fontFamily: 'inherit',
colorScheme: 'dark', width: '100%',
}}
/>
);
};
Object.assign(window, { TasksScreen, TaskRow, NewTaskForm, Empty, FieldCard, TimeInputCard });