'use client'; import { useState, useEffect } from 'react'; import { toast } from 'sonner'; import { Sheet, SheetContent, SheetHeader, SheetTitle, } from '@/components/ui/sheet'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { Badge } from '@/components/ui/badge'; import { Calendar, Loader2, Play, Square } from 'lucide-react'; interface TaskDetail { id: string; title: string; description?: string | null; status: 'todo' | 'in_progress' | 'done' | 'cancelled'; priority: 'low' | 'medium' | 'high' | 'urgent'; domainId: string; projectId?: string | null; sectionId?: string | null; parentId?: string | null; dueDate?: string | null; completedAt?: string | null; estimatedMinutes?: number | null; trackedMinutes?: number | null; order: number; recurrenceRule?: string | null; customFields?: Record | null; createdAt: string; updatedAt: string; subtasks: any[]; tags: { id: string; name: string; color: string | null }[]; dependencies: { id: string; title: string; status: string }[]; dependents: { id: string; title: string; status: string }[]; } interface TaskDetailPanelProps { taskId: string; domainId: string; open: boolean; onOpenChange: (open: boolean) => void; onUpdate: () => void; } const RECURRENCE_OPTIONS = [ { label: 'None', value: '' }, { label: 'Daily', value: 'FREQ=DAILY' }, { label: 'Weekly', value: 'FREQ=WEEKLY' }, { label: 'Monthly', value: 'FREQ=MONTHLY' }, { label: 'Custom (rrule)', value: 'custom' }, ] as const; export function TaskDetailPanel({ taskId, domainId, open, onOpenChange, onUpdate, }: TaskDetailPanelProps) { const [task, setTask] = useState(null); const [loading, setLoading] = useState(true); const [title, setTitle] = useState(''); const [description, setDescription] = useState(''); const [status, setStatus] = useState<'todo' | 'in_progress' | 'done' | 'cancelled'>('todo'); const [priority, setPriority] = useState<'low' | 'medium' | 'high' | 'urgent'>('medium'); const [dueDate, setDueDate] = useState(''); const [estimatedMinutes, setEstimatedMinutes] = useState(''); const [recurrenceType, setRecurrenceType] = useState(''); const [customRrule, setCustomRrule] = useState(''); const [customFieldValues, setCustomFieldValues] = useState>({}); const [saving, setSaving] = useState(false); const [deleteOpen, setDeleteOpen] = useState(false); const [deleting, setDeleting] = useState(false); const [timerRunning, setTimerRunning] = useState(false); const [timerStartedAt, setTimerStartedAt] = useState(null); const [elapsedSeconds, setElapsedSeconds] = useState(0); // Fetch task details when panel opens useEffect(() => { if (!open || !taskId || !domainId) return; setLoading(true); fetch(`/api/domains/${domainId}/tasks/${taskId}`) .then((res) => { if (!res.ok) throw new Error('Unable to load task'); return res.json(); }) .then((data: TaskDetail) => { setTask(data); setTitle(data.title); setDescription(data.description || ''); setStatus(data.status); setPriority(data.priority); setDueDate(data.dueDate ? data.dueDate.split('T')[0] : ''); setEstimatedMinutes(data.estimatedMinutes?.toString() || ''); // Recurrence if (data.recurrenceRule) { const isPreset = RECURRENCE_OPTIONS.some( (o) => o.value !== 'custom' && o.value !== '' && o.value === data.recurrenceRule ); if (isPreset) { setRecurrenceType(data.recurrenceRule); setCustomRrule(''); } else { setRecurrenceType('custom'); setCustomRrule(data.recurrenceRule); } } else { setRecurrenceType(''); setCustomRrule(''); } // Custom fields setCustomFieldValues(data.customFields || {}); }) .catch((err) => { console.error('Failed to load task:', err); toast.error('Unable to load task details'); }) .finally(() => setLoading(false)); }, [open, taskId, domainId]); function getRecurrenceRule(): string | null { if (!recurrenceType) return null; if (recurrenceType === 'custom') return customRrule || null; return recurrenceType; } async function handleSave() { if (!task || !domainId) return; setSaving(true); try { const body: Record = { title, status, priority, }; if (description !== (task.description || '')) body.description = description || null; if (dueDate !== (task.dueDate ? task.dueDate.split('T')[0] : '')) { body.dueDate = dueDate ? new Date(dueDate).toISOString() : null; } if (estimatedMinutes !== (task.estimatedMinutes?.toString() || '')) { body.estimatedMinutes = estimatedMinutes ? parseInt(estimatedMinutes, 10) : null; } const recurrenceRule = getRecurrenceRule(); if (recurrenceRule !== (task.recurrenceRule || null)) { body.recurrenceRule = recurrenceRule; } // Include custom fields if changed const currentCustomFields = task.customFields || {}; if (JSON.stringify(customFieldValues) !== JSON.stringify(currentCustomFields)) { body.customFields = customFieldValues; } const response = await fetch(`/api/domains/${domainId}/tasks/${task.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!response.ok) throw new Error('Unable to save task'); onUpdate(); onOpenChange(false); toast.success('Task saved'); } catch (error) { console.error('Failed to update task:', error); toast.error('Unable to save task'); } finally { setSaving(false); } } async function handleDelete() { if (!task || !domainId) return; setDeleting(true); try { const response = await fetch(`/api/domains/${domainId}/tasks/${task.id}`, { method: 'DELETE', }); if (!response.ok) throw new Error('Unable to delete task'); onUpdate(); setDeleteOpen(false); onOpenChange(false); toast.success('Task deleted'); } catch (error) { console.error('Failed to delete task:', error); toast.error('Unable to delete task'); } finally { setDeleting(false); } } // Timer tick effect useEffect(() => { if (!timerRunning || !timerStartedAt) { setElapsedSeconds(0); return; } const interval = setInterval(() => { setElapsedSeconds(Math.floor((Date.now() - timerStartedAt.getTime()) / 1000)); }, 1000); return () => clearInterval(interval); }, [timerRunning, timerStartedAt]); function formatDuration(seconds: number): string { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = seconds % 60; if (h > 0) return `${h}h ${m}m`; if (m > 0) return `${m}m ${s}s`; return `${s}s`; } async function handleStartTimer() { setTimerRunning(true); setTimerStartedAt(new Date()); } async function handleStopTimer() { if (!timerStartedAt || !task) return; const deltaMinutes = Math.round((Date.now() - timerStartedAt.getTime()) / 60000); if (deltaMinutes < 1) { setTimerRunning(false); setTimerStartedAt(null); return; } const newTracked = (task.trackedMinutes || 0) + deltaMinutes; try { const response = await fetch(`/api/domains/${domainId}/tasks/${task.id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ trackedMinutes: newTracked }), }); if (!response.ok) throw new Error('Unable to save tracked time'); setTask({ ...task, trackedMinutes: newTracked }); onUpdate(); toast.success(`Tracked ${deltaMinutes}m`); } catch (error) { console.error('Failed to save tracked time:', error); toast.error('Unable to save tracked time'); } finally { setTimerRunning(false); setTimerStartedAt(null); } } return ( Task Details {loading ? (
) : !task ? (

Task not found

) : (
setTitle(e.target.value)} placeholder="Task title" />