forked from sagnik/Velocity-OS
105 lines
3.0 KiB
TypeScript
105 lines
3.0 KiB
TypeScript
import { motion } from 'framer-motion';
|
|
import { useClientTasks } from '../../../../shared/hooks/useClient360';
|
|
import styles from './Tasks.module.css';
|
|
|
|
/**
|
|
* Tasks Tab
|
|
* Timeline: TODAY → UPCOMING → COMPLETED
|
|
* Tasks are AI-generated or manually created reminders.
|
|
* "Done" and "Snooze" are the only actions — no task configuration UI.
|
|
*/
|
|
|
|
interface TasksTabProps {
|
|
personId: string;
|
|
}
|
|
|
|
export function TasksTab({ personId }: TasksTabProps) {
|
|
const { tasks, isLoading, markDone, snooze } = useClientTasks(personId);
|
|
|
|
const today = tasks.filter(t => t.group === 'today');
|
|
const upcoming = tasks.filter(t => t.group === 'upcoming');
|
|
const completed = tasks.filter(t => t.group === 'completed');
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className={styles.root}>
|
|
{[0, 1, 2].map(i => <div key={i} className={`${styles.skeleton} shimmer`} />)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={styles.root}>
|
|
{today.length > 0 && (
|
|
<TaskGroup label="TODAY" tasks={today} onDone={markDone} onSnooze={snooze} />
|
|
)}
|
|
{upcoming.length > 0 && (
|
|
<TaskGroup label="UPCOMING" tasks={upcoming} onDone={markDone} onSnooze={snooze} />
|
|
)}
|
|
{completed.length > 0 && (
|
|
<TaskGroup label="COMPLETED" tasks={completed} dim />
|
|
)}
|
|
{tasks.length === 0 && (
|
|
<p className={styles.empty}>No tasks yet — create one to stay on track.</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
interface Task {
|
|
id: string;
|
|
label: string;
|
|
dueAt?: string;
|
|
group: 'today' | 'upcoming' | 'completed';
|
|
isAIGenerated?: boolean;
|
|
}
|
|
|
|
function TaskGroup({
|
|
label, tasks, dim = false, onDone, onSnooze,
|
|
}: {
|
|
label: string;
|
|
tasks: Task[];
|
|
dim?: boolean;
|
|
onDone?: (id: string) => void;
|
|
onSnooze?: (id: string) => void;
|
|
}) {
|
|
return (
|
|
<motion.div
|
|
className={styles.group}
|
|
initial={{ opacity: 0 }}
|
|
animate={{ opacity: dim ? 0.55 : 1 }}
|
|
transition={{ duration: 0.3 }}
|
|
>
|
|
<h3 className={styles.groupLabel}>{label}</h3>
|
|
{tasks.map((task, i) => (
|
|
<motion.div
|
|
key={task.id}
|
|
className={styles.taskRow}
|
|
initial={{ opacity: 0, x: -8 }}
|
|
animate={{ opacity: 1, x: 0 }}
|
|
transition={{ delay: i * 0.05 }}
|
|
>
|
|
<span className={dim ? styles.checkDone : styles.checkOpen}>
|
|
{dim ? '✓' : '○'}
|
|
</span>
|
|
<div className={styles.taskContent}>
|
|
<span className={styles.taskLabel}>{task.label}</span>
|
|
{task.dueAt && (
|
|
<span className={styles.taskDue}>{task.dueAt}</span>
|
|
)}
|
|
{task.isAIGenerated && (
|
|
<span className={styles.aiChip}>✦ AI</span>
|
|
)}
|
|
</div>
|
|
{!dim && onDone && onSnooze && (
|
|
<div className={styles.taskActions}>
|
|
<button className="btn-ghost" onClick={() => onDone(task.id)}>Done</button>
|
|
<button className="btn-ghost" onClick={() => onSnooze(task.id)}>Snooze</button>
|
|
</div>
|
|
)}
|
|
</motion.div>
|
|
))}
|
|
</motion.div>
|
|
);
|
|
}
|