import { useState } from "react"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { ArrowUpRight, Calendar, Flag, Plus, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { format, parseISO } from "date-fns"; import { api, useApiQuery } from "@/lib/api"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Sheet, SheetContent, SheetHeader, SheetTitle } from "@/components/ui/sheet"; import { Textarea } from "@/components/ui/textarea"; import { TagManager } from "@/components/entities/tag-manager"; import { PRIORITY } from "@/lib/status-colors"; import { cn } from "@/lib/utils"; import type { State, Task } from "@/lib/types"; interface TaskDetailPanelProps { taskId: string | null; open: boolean; onOpenChange: (open: boolean) => void; onOpenFullPage?: (task: Task) => void; } function errorMessage(err: unknown): string { return err instanceof Error ? err.message : "Something went wrong"; } export function TaskDetailPanel({ taskId, open, onOpenChange, onOpenFullPage }: TaskDetailPanelProps) { const queryClient = useQueryClient(); const [newSubtask, setNewSubtask] = useState(""); const { data: task, isLoading } = useApiQuery(["task", taskId || ""], "/tasks/" + taskId, { enabled: open && !!taskId, }); const { data: statesData } = useApiQuery<{ items: State[] }>( ["states", task?.projectId || ""], "/states?projectId=" + (task?.projectId || ""), { enabled: open && !!task?.projectId } ); const projectStates = statesData?.items || []; const currentState = task?.stateId ? projectStates.find((s) => s.id === task.stateId) : null; const refresh = () => { queryClient.invalidateQueries({ queryKey: ["tasks"] }); queryClient.invalidateQueries({ queryKey: ["task-groups"] }); if (taskId) queryClient.invalidateQueries({ queryKey: ["task", taskId] }); if (task?.projectId) queryClient.invalidateQueries({ queryKey: ["project", task.projectId] }); }; const patchMutation = useMutation({ mutationFn: (data: Record) => api.patch(`/tasks/${taskId}`, data), onSuccess: refresh, onError: (err) => toast.error(errorMessage(err)), }); const addSubtaskMutation = useMutation({ mutationFn: (title: string) => api.post("/tasks", { title, parentId: taskId, projectId: task?.projectId || null, domain: task?.domainId }), onSuccess: () => { setNewSubtask(""); refresh(); }, onError: (err) => toast.error(errorMessage(err)), }); const deleteMutation = useMutation({ mutationFn: () => api.delete(`/tasks/${taskId}`), onSuccess: () => { toast.success("Task deleted"); refresh(); onOpenChange(false); }, onError: (err) => toast.error(errorMessage(err)), }); const subtasks = task?.subtasks || []; return ( Task details {isLoading || !task ? (

{isLoading ? "Loading..." : "Select a task"}

) : (
{ const v = e.target.value.trim(); if (v && v !== task.title) patchMutation.mutate({ title: v }); }} onKeyDown={(e) => { if (e.key === "Enter") (e.target as HTMLInputElement).blur(); }} className="h-9 text-base font-semibold" aria-label="Task title" />
{currentState ? ( {currentState.name} ) : ( No state )} {PRIORITY[task.priority]?.label ?? task.priority} {task.dueDate && ( {format(parseISO(task.dueDate), "MMM d, yyyy")} )}
patchMutation.mutate({ dueDate: e.target.value ? new Date(e.target.value).toISOString() : null })} />
{ const v = e.target.value ? parseInt(e.target.value, 10) : null; patchMutation.mutate({ estimatedMinutes: v }); }} />