'use client'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslations } from 'next-intl'; import { MessageSquare } from 'lucide-react'; import { Badge } from '@/components/ui/shared/Badge'; import { Button } from '@/components/ui/shared/Button'; import { FORM_SELECT_CLASS } from '@/components/shared/formSelectStyles'; import { SearchBar } from '@/components/ui/shared/SearchBar'; import { LabCaseCommentsPanel } from '@/components/ui/lab/LabCaseCommentsPanel'; import { labTaskStatusSelectStyle, labTaskStatusVariant, } from '@/components/lab/labTaskStatusDisplay'; import { formatToothList, prosthesisTypeBadgeStyle, } from '@/components/treatment/prosthesisTypeDisplay'; import { getUserFacingError } from '@/components/shared/formatApiError'; import { canEditTasks, canViewTasks } from '@/components/shared/permissions'; import { useAuth } from '@/lib/hooks/useAuth'; import { useToast } from '@/lib/hooks/useToast'; import { tasksApi } from '@/lib/api/tasks'; import type { LabTaskListItem, LabTaskStatus, ListLabTasksParams, PaginatedLabTasks, TaskSortField, } from '@/types/cases'; const PAGE_SIZE = 50; function formatPatientName(patient: { firstName: string; lastName: string }) { return `${patient.firstName} ${patient.lastName}`.trim(); } export function TasksPage() { const t = useTranslations('tasks'); const tErrors = useTranslations('errors'); const { currentOrganization, user, isAuthReady } = useAuth(); const { showError, setError, messages: toastMessages } = useToast(); const [tasks, setTasks] = useState([]); const [pagination, setPagination] = useState({ page: 1, limit: PAGE_SIZE, total: 0, totalPages: 1, }); const [page, setPage] = useState(1); const [loading, setLoading] = useState(false); const [updatingTaskId, setUpdatingTaskId] = useState(null); const [expandedCommentsTaskId, setExpandedCommentsTaskId] = useState(null); const [search, setSearch] = useState(''); const [clinicId, setClinicId] = useState(''); const [statusFilter, setStatusFilter] = useState<'' | LabTaskStatus>('IN_PROGRESS'); const [sentFrom, setSentFrom] = useState(''); const [sentTo, setSentTo] = useState(''); const [sortBy, setSortBy] = useState('date'); const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc'); const canView = canViewTasks(currentOrganization); const canEdit = canEditTasks(currentOrganization); const locale = user?.language ?? 'en'; const tRef = useRef(t); tRef.current = t; const statusOptions: { value: LabTaskStatus; label: string }[] = useMemo( () => [ { value: 'IN_PROGRESS', label: t('statusInProgress') }, { value: 'COMPLETED', label: t('statusCompleted') }, ], [t], ); const listParams = useMemo((): ListLabTasksParams => { const params: ListLabTasksParams = { page, limit: PAGE_SIZE, sortBy, sortDir, }; if (search.trim()) params.q = search.trim(); if (clinicId) params.clinicOrganizationId = clinicId; if (statusFilter) params.status = statusFilter; if (sentFrom) params.sentFrom = sentFrom; if (sentTo) params.sentTo = sentTo; return params; }, [page, search, clinicId, statusFilter, sentFrom, sentTo, sortBy, sortDir]); const clinicOptions = useMemo(() => { const map = new Map(); for (const task of tasks) { map.set(task.clinic.id, task.clinic.name); } return [...map.entries()].map(([id, name]) => ({ id, name })); }, [tasks]); const loadTasks = useCallback(async () => { setLoading(true); setError(''); try { const response = await tasksApi.list(listParams); setTasks(response.data.items); setPagination(response.data.pagination); } catch (error: unknown) { showError(getUserFacingError(error, tErrors, tRef.current('errorLoadList'))); } finally { setLoading(false); } }, [listParams, showError, setError]); useEffect(() => { if (!canView) return; const timeout = setTimeout(() => void loadTasks(), search ? 300 : 0); return () => clearTimeout(timeout); }, [canView, loadTasks, search]); async function handleStatusUpdate(taskId: string, status: LabTaskStatus) { if (!canEdit) return; setUpdatingTaskId(taskId); setError(''); try { await tasksApi.updateStatus(taskId, status); await loadTasks(); } catch (error: unknown) { showError(getUserFacingError(error, tErrors, t('errorUpdateTask'))); } finally { setUpdatingTaskId(null); } } function formatTaskDate(value: string) { return new Intl.DateTimeFormat(locale, { year: 'numeric', month: 'short', day: 'numeric', }).format(new Date(value)); } const filterSelectClass = `${FORM_SELECT_CLASS} w-full rounded-md px-2 py-1.5 text-sm`; if (!isAuthReady) { return
{t('loading')}
; } if (!canView) { return (

{t('noPermissionTitle')}

{t('noPermissionBody')}

); } return (

{t('title')}

{t('subtitle')}

{ setSearch(v); setPage(1); }} placeholder={t('searchPlaceholder')} />
{loading && tasks.length === 0 ? (

{t('loading')}

) : tasks.length === 0 ? (

{t('emptyList')}

) : (
    {tasks.map((task, index) => { const commentsOpen = expandedCommentsTaskId === task.id; return (
  • {task.stepOrder}. {task.stepLabel}

    {task.isImportant ? ( {t('importantBadge')} ) : null}

    {t('fromClinic', { name: task.clinic.name })} ·{' '} {formatPatientName(task.patient)} ·{' '} {t('teethLabel', { teeth: formatToothList(task.teeth) })}

    {t('taskDate', { date: formatTaskDate(task.createdAt) })} {task.lastStatusChangedBy ? ( <> · {t('lastUpdatedBy', { name: task.lastStatusChangedBy.name })} ) : null}

    {canEdit ? ( ) : ( {statusOptions.find((opt) => opt.value === task.status)?.label ?? task.status} )}
    {canEdit ? ( ) : null} {task.prosthesisTypeLabel}
    {commentsOpen && canEdit ? (
    { const r = await tasksApi.listComments(task.labCaseId); return r.data; }} onPost={async (body, visibleToClinic) => { const r = await tasksApi.addComment(task.labCaseId, { body, visibleToClinic, }); return r.data; }} onToggleVisibility={async (commentId, visible) => { const r = await tasksApi.setCommentVisibility(commentId, visible); return r.data; }} onError={showError} />
    ) : null}
  • ); })}
)}
{pagination.totalPages > 1 && (

{t('pageSummary', { page: pagination.page, totalPages: pagination.totalPages, total: pagination.total, })}

)}
); }