/** * Tasks page — list with filter, create modal, detail. */ import React, { useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Button } from '@/components/ui/Button'; import { Input } from '@/components/ui/Input'; import { Select } from '@/components/ui/Select'; import { Modal } from '@/components/ui/Modal'; import { Badge } from '@/components/ui/Badge'; import { EmptyState } from '@/components/ui/EmptyState'; import { useToast } from '@/components/ui/Toast'; import { Loader2, Plus, CheckSquare, Clock, AlertCircle } from 'lucide-react'; import { useTasks, useCreateTask, useUpdateTask, useDeleteTask, useUpdateTaskStatus, type Task, type TaskFilter, } from '@/api/tasks'; const STATUS_COLORS: Record = { open: 'secondary', in_progress: 'info', done: 'success', }; const PRIORITY_COLORS: Record = { low: 'secondary', medium: 'info', high: 'warning', urgent: 'danger', }; function formatDate(dateStr: string | null): string { if (!dateStr) return '—'; try { return new Date(dateStr).toLocaleDateString(); } catch { return dateStr; } } function isOverdue(dateStr: string | null, status: string): boolean { if (!dateStr || status === 'done') return false; try { return new Date(dateStr) < new Date(); } catch { return false; } } export function TasksPage() { const { t } = useTranslation(); const toast = useToast(); const [page, setPage] = useState(1); const [filter, setFilter] = useState({}); const [search, setSearch] = useState(''); const [debouncedSearch, setDebouncedSearch] = useState(''); const [createModalOpen, setCreateModalOpen] = useState(false); const [editingTask, setEditingTask] = useState(null); const [selectedTask, setSelectedTask] = useState(null); // Debounce search React.useEffect(() => { const timer = setTimeout(() => setDebouncedSearch(search), 300); return () => clearTimeout(timer); }, [search]); const effectiveFilter = useMemo( () => ({ ...filter, search: debouncedSearch || undefined }), [filter, debouncedSearch], ); const { data, isLoading } = useTasks(page, 25, effectiveFilter); const createMutation = useCreateTask(); const updateMutation = useUpdateTask(); const deleteMutation = useDeleteTask(); const statusMutation = useUpdateTaskStatus(); const tasks = data?.items || []; const handleCreate = async (formData: Partial) => { try { await createMutation.mutateAsync({ title: formData.title || '', description: formData.description || null, priority: formData.priority || 'medium', status: formData.status || 'open', due_date: formData.due_date || null, contact_id: formData.contact_id || null, }); toast.success(t('tasks.created')); setCreateModalOpen(false); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const handleUpdate = async (formData: Partial) => { if (!editingTask) return; try { await updateMutation.mutateAsync({ id: editingTask.id, data: formData }); toast.success(t('tasks.updated')); setEditingTask(null); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const handleDelete = async (task: Task) => { if (!window.confirm(t('tasks.deleteConfirm'))) return; try { await deleteMutation.mutateAsync(task.id); toast.success(t('tasks.deleted')); setSelectedTask(null); } catch (err: any) { toast.error(err.message || t('common.error')); } }; const handleStatusChange = async (task: Task, newStatus: string) => { try { await statusMutation.mutateAsync({ id: task.id, status: newStatus }); toast.success(t('tasks.statusUpdated')); } catch (err: any) { toast.error(err.message || t('common.error')); } }; return (
{/* Header */}

{t('tasks.title')}

{/* Filters */}
setSearch(e.target.value)} className="w-48" /> { setFilter(f => ({ ...f, priority: e.target.value || undefined })); setPage(1); }} className="w-40" />
{/* List */}
{isLoading ? (
) : tasks.length === 0 ? ( ) : (
    {tasks.map((task) => { const overdue = isOverdue(task.due_date, task.status); return (
  • setSelectedTask(task)} data-testid={`task-item-${task.id}`} >
    {task.title} {overdue && ( )}
    {task.description && (

    {task.description}

    )}
    {t(`tasks.status${task.status.charAt(0).toUpperCase() + task.status.slice(1).replace('_', '')}`)} {t(`tasks.priority${task.priority.charAt(0).toUpperCase() + task.priority.slice(1)}`)} {task.due_date && ( {formatDate(task.due_date)} )}
    {task.status !== 'done' && ( )}
  • ); })}
)} {/* Pagination */} {data && data.total > 25 && (
{((page - 1) * 25) + 1}–{Math.min(page * 25, data.total)} / {data.total}
)}
{/* Create/Edit Modal */} { setCreateModalOpen(false); setEditingTask(null); }} task={editingTask} onSubmit={editingTask ? handleUpdate : handleCreate} /> {/* Detail Modal */} {selectedTask && ( setSelectedTask(null)} title={selectedTask.title} size="lg">
{t(`tasks.status${selectedTask.status.charAt(0).toUpperCase() + selectedTask.status.slice(1).replace('_', '')}`)} {t(`tasks.priority${selectedTask.priority.charAt(0).toUpperCase() + selectedTask.priority.slice(1)}`)}
{selectedTask.description && (

{selectedTask.description}

)}
{t('tasks.dueDate')}: {formatDate(selectedTask.due_date)}
{selectedTask.status !== 'done' && ( )}
)}
); } // ── Task Create/Edit Modal ── function TaskModal({ open, onClose, task, onSubmit, }: { open: boolean; onClose: () => void; task?: Task | null; onSubmit: (data: Partial) => void; }) { const { t } = useTranslation(); const [title, setTitle] = useState(task?.title || ''); const [description, setDescription] = useState(task?.description || ''); const [priority, setPriority] = useState(task?.priority || 'medium'); const [status, setStatus] = useState(task?.status || 'open'); const [dueDate, setDueDate] = useState(task?.due_date ? task.due_date.slice(0, 10) : ''); React.useEffect(() => { if (open) { setTitle(task?.title || ''); setDescription(task?.description || ''); setPriority(task?.priority || 'medium'); setStatus(task?.status || 'open'); setDueDate(task?.due_date ? task.due_date.slice(0, 10) : ''); } }, [open, task]); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!title.trim()) return; onSubmit({ title: title.trim(), description: description || null, priority: priority as any, status: status as any, due_date: dueDate ? new Date(dueDate).toISOString() : null, }); }; return (
setTitle(e.target.value)} required data-testid="task-title-input" />