forked from sagnik/Velocity-OS
Initial commit: Velocity-OS migration
This commit is contained in:
104
webos/src/pillars/pipeline/client360/tabs/Tasks.tsx
Normal file
104
webos/src/pillars/pipeline/client360/tabs/Tasks.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user