import { useState, useEffect, useCallback } from 'react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { format, parseISO, isToday } from 'date-fns'; import { X, Pencil, Trash2, Save, Clock, Calendar, Flag, Tag, Repeat, CheckSquare, AlertCircle, AlignLeft, } from 'lucide-react'; import api, { getErrorMessage } from '@/lib/api'; import type { Todo } from '@/types'; import { isTodoOverdue } from '@/lib/utils'; import { useConfirmAction } from '@/hooks/useConfirmAction'; import { formatUpdatedAt } from '@/components/shared/utils'; import CopyableField from '@/components/shared/CopyableField'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; import { Textarea } from '@/components/ui/textarea'; import { Select } from '@/components/ui/select'; import { Checkbox } from '@/components/ui/checkbox'; import { Label } from '@/components/ui/label'; import { Badge } from '@/components/ui/badge'; // --- Types --- export interface TodoCreateDefaults { category?: string; } interface TodoDetailPanelProps { todo: Todo | null; isCreating?: boolean; createDefaults?: TodoCreateDefaults | null; onClose: () => void; onSaved?: () => void; onDeleted?: () => void; } interface EditState { title: string; description: string; priority: string; due_date: string; due_time: string; category: string; recurrence_rule: string; } const priorityColors: Record = { none: 'bg-gray-500/20 text-gray-400', low: 'bg-green-500/20 text-green-400', medium: 'bg-yellow-500/20 text-yellow-400', high: 'bg-red-500/20 text-red-400', }; const recurrenceLabels: Record = { daily: 'Daily', weekly: 'Weekly', monthly: 'Monthly', }; function todayLocal(): string { const d = new Date(); const pad = (n: number) => n.toString().padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`; } function nowTimeLocal(): string { const d = new Date(); const pad = (n: number) => n.toString().padStart(2, '0'); return `${pad(d.getHours())}:${pad(d.getMinutes())}`; } const QUERY_KEYS = [['todos'], ['dashboard'], ['upcoming']] as const; function buildEditState(todo: Todo): EditState { return { title: todo.title, description: todo.description || '', priority: todo.priority, due_date: todo.due_date ? todo.due_date.slice(0, 10) : '', due_time: todo.due_time ? todo.due_time.slice(0, 5) : '', category: todo.category || '', recurrence_rule: todo.recurrence_rule || '', }; } function buildCreateState(defaults?: TodoCreateDefaults | null): EditState { return { title: '', description: '', priority: 'medium', due_date: todayLocal(), due_time: nowTimeLocal(), category: defaults?.category || '', recurrence_rule: '', }; } // --- Component --- export default function TodoDetailPanel({ todo, isCreating = false, createDefaults, onClose, onSaved, onDeleted, }: TodoDetailPanelProps) { const queryClient = useQueryClient(); const [isEditing, setIsEditing] = useState(isCreating); const [editState, setEditState] = useState(() => isCreating ? buildCreateState(createDefaults) : todo ? buildEditState(todo) : buildCreateState() ); // Reset state when todo changes useEffect(() => { setIsEditing(false); if (todo) setEditState(buildEditState(todo)); }, [todo?.id]); // Enter edit mode when creating useEffect(() => { if (isCreating) { setIsEditing(true); setEditState(buildCreateState(createDefaults)); } }, [isCreating, createDefaults]); const invalidateAll = useCallback(() => { QUERY_KEYS.forEach((key) => queryClient.invalidateQueries({ queryKey: [...key] })); }, [queryClient]); // --- Mutations --- const saveMutation = useMutation({ mutationFn: async (data: EditState) => { const payload = { title: data.title, description: data.description || null, priority: data.priority, due_date: data.due_date || null, due_time: data.due_time || null, category: data.category || null, recurrence_rule: data.recurrence_rule || null, }; if (todo && !isCreating) { return api.put(`/todos/${todo.id}`, payload); } else { return api.post('/todos', payload); } }, onSuccess: () => { invalidateAll(); toast.success(isCreating ? 'Todo created' : 'Todo updated'); if (isCreating) { onClose(); } else { setIsEditing(false); } onSaved?.(); }, onError: (error) => { toast.error(getErrorMessage(error, isCreating ? 'Failed to create todo' : 'Failed to update todo')); }, }); const toggleMutation = useMutation({ mutationFn: async () => { const { data } = await api.patch(`/todos/${todo!.id}/toggle`); return data; }, onSuccess: () => { invalidateAll(); toast.success(todo!.completed ? 'Todo marked incomplete' : 'Todo completed!'); }, onError: () => { toast.error('Failed to update todo'); }, }); const deleteMutation = useMutation({ mutationFn: async () => { await api.delete(`/todos/${todo!.id}`); }, onSuccess: () => { invalidateAll(); toast.success('Todo deleted'); onClose(); onDeleted?.(); }, onError: (error) => { toast.error(getErrorMessage(error, 'Failed to delete todo')); }, }); const executeDelete = useCallback(() => deleteMutation.mutate(), [deleteMutation]); const { confirming: confirmingDelete, handleClick: handleDeleteClick } = useConfirmAction(executeDelete); // --- Handlers --- const handleEditStart = () => { if (todo) setEditState(buildEditState(todo)); setIsEditing(true); }; const handleEditCancel = () => { setIsEditing(false); if (isCreating) { onClose(); } else if (todo) { setEditState(buildEditState(todo)); } }; const handleEditSave = () => { saveMutation.mutate(editState); }; const updateField = (key: K, value: EditState[K]) => { setEditState((s) => ({ ...s, [key]: value })); }; // Empty state if (!todo && !isCreating) { return (

Select a todo to view details

); } // View data const dueDate = todo?.due_date ? parseISO(todo.due_date) : null; const isDueToday = dueDate ? isToday(dueDate) : false; const isOverdue = todo ? isTodoOverdue(todo.due_date, todo.completed) : false; return (
{/* Header */}
{isEditing && !isCreating ? ( updateField('title', e.target.value)} className="h-8 text-base font-semibold flex-1" placeholder="Todo title" autoFocus /> ) : isCreating ? (

New Todo

) : (
e.stopPropagation()}> toggleMutation.mutate()} disabled={toggleMutation.isPending} />

{todo!.title}

)}
{(isEditing || isCreating) ? ( <> ) : ( <> {confirmingDelete ? ( ) : ( )} )}
{/* Body */}
{(isEditing || isCreating) ? ( /* Edit / Create mode */
{isCreating && (
updateField('title', e.target.value)} placeholder="Todo title" required autoFocus />
)}