import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { toast } from 'sonner'; import { format, formatDistanceToNow, parseISO } from 'date-fns'; import { Pencil, Trash2, Plus, MessageSquare, ClipboardList, Calendar, User, Flag, Activity, Send, X, Save, } from 'lucide-react'; import api, { getErrorMessage } from '@/lib/api'; import { formatUpdatedAt } from '@/components/shared/utils'; import type { ProjectTask, TaskComment, Person } from '@/types'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Checkbox } from '@/components/ui/checkbox'; import { Textarea } from '@/components/ui/textarea'; import { Input } from '@/components/ui/input'; import { DatePicker } from '@/components/ui/date-picker'; import { Select } from '@/components/ui/select'; const taskStatusColors: Record = { pending: 'bg-gray-500/10 text-gray-400 border-gray-500/20', in_progress: 'bg-blue-500/10 text-blue-400 border-blue-500/20', completed: 'bg-green-500/10 text-green-400 border-green-500/20', blocked: 'bg-red-500/10 text-red-400 border-red-500/20', review: 'bg-yellow-500/10 text-yellow-400 border-yellow-500/20', on_hold: 'bg-orange-500/10 text-orange-400 border-orange-500/20', }; const taskStatusLabels: Record = { pending: 'Pending', in_progress: 'In Progress', completed: 'Completed', blocked: 'Blocked', review: 'Review', on_hold: 'On Hold', }; 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', }; interface TaskDetailPanelProps { task: ProjectTask | null; projectId: number; onDelete: (taskId: number) => void; onAddSubtask: (parentId: number) => void; onClose?: () => void; onSelectTask?: (taskId: number) => void; } interface EditState { title: string; status: string; priority: string; due_date: string; person_id: string; description: string; } 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 buildEditState(task: ProjectTask): EditState { return { title: task.title, status: task.status, priority: task.priority, // Slice to YYYY-MM-DD for date input; backend may return full ISO string due_date: task.due_date ? task.due_date.slice(0, 10) : '', person_id: task.person_id != null ? String(task.person_id) : '', description: task.description ?? '', }; } export default function TaskDetailPanel({ task, projectId, onDelete, onAddSubtask, onClose, onSelectTask, }: TaskDetailPanelProps) { const queryClient = useQueryClient(); const [commentText, setCommentText] = useState(''); const [isEditing, setIsEditing] = useState(false); const [editState, setEditState] = useState(() => task ? buildEditState(task) : { title: '', status: 'pending', priority: 'none', due_date: todayLocal(), person_id: '', description: '' } ); const { data: people = [] } = useQuery({ queryKey: ['people'], queryFn: async () => { const { data } = await api.get('/people'); return data; }, }); // --- Mutations --- const toggleSubtaskMutation = useMutation({ mutationFn: async ({ taskId, status }: { taskId: number; status: string }) => { const newStatus = status === 'completed' ? 'pending' : 'completed'; const { data } = await api.put(`/projects/${projectId}/tasks/${taskId}`, { status: newStatus }); return data; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['projects', projectId.toString()] }); }, }); const updateTaskMutation = useMutation({ mutationFn: async (payload: Record) => { const { data } = await api.put(`/projects/${projectId}/tasks/${task!.id}`, payload); return data; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['projects', projectId.toString()] }); setIsEditing(false); toast.success('Task updated'); }, onError: (error) => { toast.error(getErrorMessage(error, 'Failed to update task')); }, }); const deleteSubtaskMutation = useMutation({ mutationFn: async (subtaskId: number) => { await api.delete(`/projects/${projectId}/tasks/${subtaskId}`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['projects', projectId.toString()] }); toast.success('Subtask deleted'); }, onError: (error) => { toast.error(getErrorMessage(error, 'Failed to delete subtask')); }, }); const addCommentMutation = useMutation({ mutationFn: async (content: string) => { const { data } = await api.post( `/projects/${projectId}/tasks/${task!.id}/comments`, { content } ); return data; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['projects', projectId.toString()] }); setCommentText(''); }, onError: (error) => { toast.error(getErrorMessage(error, 'Failed to add comment')); }, }); const deleteCommentMutation = useMutation({ mutationFn: async (commentId: number) => { await api.delete(`/projects/${projectId}/tasks/${task!.id}/comments/${commentId}`); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['projects', projectId.toString()] }); toast.success('Comment deleted'); }, }); // --- Handlers --- const handleAddComment = () => { const trimmed = commentText.trim(); if (!trimmed) return; addCommentMutation.mutate(trimmed); }; const handleEditStart = () => { if (!task) return; setEditState(buildEditState(task)); setIsEditing(true); }; const handleEditCancel = () => { setIsEditing(false); if (task) setEditState(buildEditState(task)); }; const handleEditSave = () => { if (!task) return; const payload: Record = { title: editState.title.trim() || task.title, status: editState.status, priority: editState.priority, due_date: editState.due_date || null, person_id: editState.person_id ? Number(editState.person_id) : null, description: editState.description || null, }; updateTaskMutation.mutate(payload); }; const handleDeleteSubtask = (subtaskId: number, subtaskTitle: string) => { if (!window.confirm(`Delete subtask "${subtaskTitle}"?`)) return; deleteSubtaskMutation.mutate(subtaskId); }; // --- Empty state --- if (!task) { return (

Select a task to view details

); } const assignedPerson = task.person_id ? people.find((p) => p.id === task.person_id) : null; const comments = task.comments || []; return (
{/* Header */}
{isEditing ? ( setEditState((s) => ({ ...s, title: e.target.value }))} className="h-8 text-base font-semibold flex-1" autoFocus /> ) : (

{task.title}

)}
{isEditing ? ( <> ) : ( <> {onClose && ( )} )}
{/* Scrollable content */}
{/* Fields grid */}
{/* Status */}
Status
{isEditing ? ( ) : ( {taskStatusLabels[task.status] ?? task.status} )}
{/* Priority */}
Priority
{isEditing ? ( ) : ( {task.priority} )}
{/* Due Date */}
Due Date
{isEditing ? ( setEditState((s) => ({ ...s, due_date: v }))} className="h-8 text-xs" /> ) : (

{task.due_date ? format(parseISO(task.due_date), 'MMM d, yyyy') : '—'}

)}
{/* Assigned */}
Assigned
{isEditing ? ( ) : (

{assignedPerson ? assignedPerson.name : '—'}

)}
{/* Description */} {isEditing ? (

Description