"use client"; import { useMemo, useCallback, useState } from "react"; import { useTranslations } from "next-intl"; import { format, parseISO, isPast, isToday, isTomorrow } from "date-fns"; import { Check, Flag, CalendarDays, ListTodo, Plus } from "lucide-react"; import { cn } from "@/lib/utils"; import type { CalendarTask, Calendar } from "@/lib/jmap/types"; import type { TaskViewFilter } from "@/stores/task-store"; import { useSettingsStore } from "@/stores/settings-store"; interface TaskListViewProps { tasks: CalendarTask[]; calendars: Calendar[]; selectedCalendarIds: string[]; filter: TaskViewFilter; showCompleted: boolean; onSelectTask: (task: CalendarTask) => void; onToggleComplete: (task: CalendarTask) => void; selectedTaskId?: string | null; onQuickCreate?: (title: string) => void; } function getTaskPriorityIcon(priority: number) { if (priority >= 1 && priority <= 4) return ; if (priority === 5) return ; if (priority >= 6 && priority <= 9) return ; return null; } function getDueDateLabel(due: string, showWithoutTime: boolean, t: ReturnType, timeFormat: string): { label: string; className: string } { const dueDate = parseISO(due); const overdue = isPast(dueDate) && !isToday(dueDate); if (isToday(dueDate)) { return { label: t("tasks.due_today"), className: "text-blue-600 dark:text-blue-400", }; } if (isTomorrow(dueDate)) { return { label: t("tasks.due_tomorrow"), className: "text-muted-foreground", }; } if (overdue) { return { label: t("tasks.overdue"), className: "text-red-600 dark:text-red-400", }; } const formatted = showWithoutTime ? format(dueDate, "MMM d") : format(dueDate, timeFormat === "12h" ? "MMM d, h:mm a" : "MMM d, HH:mm"); return { label: formatted, className: "text-muted-foreground", }; } export function TaskListView({ tasks, calendars, selectedCalendarIds, filter, showCompleted, onSelectTask, onToggleComplete, selectedTaskId, onQuickCreate, }: TaskListViewProps) { const t = useTranslations("calendar"); const timeFormat = useSettingsStore((s) => s.timeFormat); const [quickAddTitle, setQuickAddTitle] = useState(""); const filteredTasks = useMemo(() => { let result = tasks.filter(task => { const calIds = Object.keys(task.calendarIds); return calIds.some(id => selectedCalendarIds.includes(id)); }); if (!showCompleted) { result = result.filter(task => task.progress !== "completed" && task.progress !== "cancelled"); } switch (filter) { case "pending": result = result.filter(task => task.progress === "needs-action" || task.progress === "in-process"); break; case "completed": result = result.filter(task => task.progress === "completed"); break; case "overdue": result = result.filter(task => { if (!task.due || task.progress === "completed" || task.progress === "cancelled") return false; return isPast(parseISO(task.due)) && !isToday(parseISO(task.due)); }); break; } // Sort: overdue first, then by due date (no due date last), then by priority result.sort((a, b) => { // Completed tasks at the bottom if (a.progress === "completed" && b.progress !== "completed") return 1; if (a.progress !== "completed" && b.progress === "completed") return -1; // Tasks with due dates before those without if (a.due && !b.due) return -1; if (!a.due && b.due) return 1; if (a.due && b.due) { const dateCompare = new Date(a.due).getTime() - new Date(b.due).getTime(); if (dateCompare !== 0) return dateCompare; } // Higher priority first (lower number = higher priority, but 0 = no priority goes last) const aPri = a.priority || 10; const bPri = b.priority || 10; return aPri - bPri; }); return result; }, [tasks, selectedCalendarIds, filter, showCompleted]); const handleToggle = useCallback((e: React.MouseEvent, task: CalendarTask) => { e.stopPropagation(); onToggleComplete(task); }, [onToggleComplete]); if (filteredTasks.length === 0) { return ( {onQuickCreate && ( setQuickAddTitle(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && quickAddTitle.trim()) { onQuickCreate(quickAddTitle.trim()); setQuickAddTitle(""); } }} placeholder={t("tasks.quick_add_placeholder")} className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" /> )} {t("tasks.no_tasks")} ); } return ( {onQuickCreate && ( setQuickAddTitle(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && quickAddTitle.trim()) { onQuickCreate(quickAddTitle.trim()); setQuickAddTitle(""); } }} placeholder={t("tasks.quick_add_placeholder")} className="flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground" /> )} {filteredTasks.map(task => { const cal = calendars.find(c => task.calendarIds[c.id]); const isCompleted = task.progress === "completed"; const priorityIcon = getTaskPriorityIcon(task.priority); const dueDateInfo = task.due ? getDueDateLabel(task.due, task.showWithoutTime, t, timeFormat) : null; return ( onSelectTask(task)} className={cn( "flex items-start gap-3 px-4 py-3 cursor-pointer hover:bg-muted/50 transition-colors", selectedTaskId === task.id && "bg-muted", )} > {/* Checkbox */} handleToggle(e, task)} className={cn( "mt-0.5 flex-shrink-0 w-5 h-5 rounded-full border-2 flex items-center justify-center transition-colors", isCompleted ? "bg-success border-success text-success-foreground" : "border-muted-foreground/40 hover:border-primary" )} aria-label={isCompleted ? t("tasks.mark_incomplete") : t("tasks.mark_complete")} > {isCompleted && } {/* Content */} {task.title || t("tasks.no_title")} {priorityIcon} {dueDateInfo && ( {dueDateInfo.label} )} {cal && ( {cal.name} )} ); })} ); }
{t("tasks.no_tasks")}