'use client'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import { useTranslations } from 'next-intl'; import { Button } from '@/components/ui/shared/Button'; import { Checkbox } from '@/components/ui/shared/Checkbox'; import { FORM_SELECT_CLASS, FORM_SELECT_COMPACT_CLASS } from '@/components/shared/formSelectStyles'; import { SearchBar } from '@/components/ui/shared/SearchBar'; import { TaskCaseGroupHeader } from '@/components/ui/lab/TaskCaseGroupHeader'; import { TaskProsthesisGroupHeader } from '@/components/ui/lab/TaskProsthesisGroupHeader'; import { TaskRow } from '@/components/ui/lab/TaskRow'; import { LabCaseCommentsPanel } from '@/components/ui/lab/LabCaseCommentsPanel'; import { groupTasksForDisplay } from '@/components/lab/taskListGrouping'; import { buildDefaultLocateParams, DEFAULT_TASKS_VIEW, isDefaultTasksView, TASK_COMPLETE_EXIT_MS, } from '@/components/lab/tasksViewDefaults'; import { parseTasksSearchParams } from '@/components/lab/parseTasksSearchParams'; import { useMarkTabReadOnVisit } from '@/lib/hooks/useTabBadgeCounts'; import { notifyTabBadgesChanged } from '@/lib/tabBadgeUtils'; import { scrollWithinMainScrollContainer } from '@/components/shared/scrollWithinMain'; 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 { prosthesisCatalogApi } from '@/lib/api/prosthesis-catalog'; import { tasksApi } from '@/lib/api/tasks'; import type { LabTaskListItem, LabTaskStatus, ListLabTasksParams, PaginatedLabTasks, TaskFilterOptions, TaskSortField, } from '@/types/cases'; import type { ProsthesisCatalogEntry } from '@/types/treatment-catalog'; const PAGE_SIZE = 50; export function TasksPage() { const t = useTranslations('tasks'); const tErrors = useTranslations('errors'); const searchParams = useSearchParams(); const { currentOrganization, user, isAuthReady } = useAuth(); const { showError, showSuccess, setError } = useToast(); const [tasks, setTasks] = useState([]); const [pagination, setPagination] = useState({ page: 1, limit: PAGE_SIZE, total: 0, totalPages: 1, }); const [filterOptions, setFilterOptions] = useState({ clinics: [], workflowSteps: [], }); const [prosthesisCatalog, setProsthesisCatalog] = useState([]); const [page, setPage] = useState(DEFAULT_TASKS_VIEW.page); const [loading, setLoading] = useState(false); const [locatingCase, setLocatingCase] = useState(false); const [updatingTaskId, setUpdatingTaskId] = useState(null); const [exitingTaskIds, setExitingTaskIds] = useState>(() => new Set()); const [expandedCommentsTaskId, setExpandedCommentsTaskId] = useState(null); const [expandedCommentsCaseId, setExpandedCommentsCaseId] = useState(null); const [search, setSearch] = useState(DEFAULT_TASKS_VIEW.search); const [clinicId, setClinicId] = useState(DEFAULT_TASKS_VIEW.clinicId); const [statusFilter, setStatusFilter] = useState<'' | LabTaskStatus>( DEFAULT_TASKS_VIEW.statusFilter, ); const [stepCompleted, setStepCompleted] = useState(DEFAULT_TASKS_VIEW.stepCompleted); const [importantOnly, setImportantOnly] = useState(DEFAULT_TASKS_VIEW.importantOnly); const [assignedToMe, setAssignedToMe] = useState(DEFAULT_TASKS_VIEW.assignedToMe); const [overdueOnly, setOverdueOnly] = useState(DEFAULT_TASKS_VIEW.overdueOnly); const [unassignedOnly, setUnassignedOnly] = useState(DEFAULT_TASKS_VIEW.unassignedOnly); const [prosthesisTypeCode, setProsthesisTypeCode] = useState( DEFAULT_TASKS_VIEW.prosthesisTypeCode, ); const [sortBy, setSortBy] = useState(DEFAULT_TASKS_VIEW.sortBy); const [sortDir, setSortDir] = useState<'asc' | 'desc'>(DEFAULT_TASKS_VIEW.sortDir); const [highlightTaskId, setHighlightTaskId] = useState( DEFAULT_TASKS_VIEW.highlightTaskId, ); const canView = canViewTasks(currentOrganization); const canEdit = canEditTasks(currentOrganization); const locale = user?.language ?? 'en'; useMarkTabReadOnVisit(); const tRef = useRef(t); tRef.current = t; const loadTasksRef = useRef<() => Promise>(async () => {}); 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 (stepCompleted) params.stepCompleted = stepCompleted; if (importantOnly) params.pinImportant = true; if (assignedToMe) params.assignedToMe = true; if (overdueOnly) params.overdue = true; if (unassignedOnly) params.unassignedOnly = true; if (prosthesisTypeCode) params.prosthesisTypeCode = prosthesisTypeCode; return params; }, [page, search, clinicId, statusFilter, stepCompleted, importantOnly, assignedToMe, overdueOnly, unassignedOnly, prosthesisTypeCode, sortBy, sortDir]); const displayModel = useMemo(() => groupTasksForDisplay(tasks, sortBy), [tasks, sortBy]); const groupingDisabled = sortBy !== 'date'; const viewState = useMemo( () => ({ search, clinicId, statusFilter, stepCompleted, sortBy, sortDir, importantOnly, assignedToMe, overdueOnly, unassignedOnly, prosthesisTypeCode, page, highlightTaskId, }), [ search, clinicId, statusFilter, stepCompleted, sortBy, sortDir, importantOnly, assignedToMe, overdueOnly, unassignedOnly, prosthesisTypeCode, page, highlightTaskId, ], ); const showReset = !isDefaultTasksView(viewState); useEffect(() => { const fromUrl = parseTasksSearchParams(searchParams); if (fromUrl.importantOnly !== undefined) setImportantOnly(fromUrl.importantOnly); if (fromUrl.overdueOnly !== undefined) setOverdueOnly(fromUrl.overdueOnly); if (fromUrl.unassignedOnly !== undefined) setUnassignedOnly(fromUrl.unassignedOnly); if (fromUrl.statusFilter !== undefined) setStatusFilter(fromUrl.statusFilter); if (fromUrl.prosthesisTypeCode !== undefined) setProsthesisTypeCode(fromUrl.prosthesisTypeCode); if (fromUrl.sortBy !== undefined) setSortBy(fromUrl.sortBy); if (fromUrl.sortDir !== undefined) setSortDir(fromUrl.sortDir); setPage(1); }, [searchParams]); 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, tErrors]); loadTasksRef.current = loadTasks; useEffect(() => { if (!canView) return; const timeout = setTimeout(() => void loadTasks(), search ? 300 : 0); return () => clearTimeout(timeout); }, [canView, loadTasks, search]); useEffect(() => { if (!canView) return; void (async () => { try { const [optionsRes, catalogRes] = await Promise.all([ tasksApi.filterOptions(), prosthesisCatalogApi.list(), ]); setFilterOptions(optionsRes.data); setProsthesisCatalog(catalogRes.data); } catch { // Non-blocking — filters fall back to empty options. } })(); }, [canView]); useEffect(() => { if (!highlightTaskId || loading || locatingCase) return; const frame = requestAnimationFrame(() => { const el = document.getElementById(`task-row-${highlightTaskId}`); scrollWithinMainScrollContainer(el, { padding: 24 }); }); return () => cancelAnimationFrame(frame); }, [highlightTaskId, loading, locatingCase, tasks, displayModel]); const clearFocus = useCallback(() => { setHighlightTaskId(null); }, []); const resetView = useCallback(() => { setSearch(DEFAULT_TASKS_VIEW.search); setClinicId(DEFAULT_TASKS_VIEW.clinicId); setStatusFilter(DEFAULT_TASKS_VIEW.statusFilter); setStepCompleted(DEFAULT_TASKS_VIEW.stepCompleted); setImportantOnly(DEFAULT_TASKS_VIEW.importantOnly); setAssignedToMe(DEFAULT_TASKS_VIEW.assignedToMe); setOverdueOnly(DEFAULT_TASKS_VIEW.overdueOnly); setUnassignedOnly(DEFAULT_TASKS_VIEW.unassignedOnly); setProsthesisTypeCode(DEFAULT_TASKS_VIEW.prosthesisTypeCode); setSortBy(DEFAULT_TASKS_VIEW.sortBy); setSortDir(DEFAULT_TASKS_VIEW.sortDir); setPage(DEFAULT_TASKS_VIEW.page); setHighlightTaskId(DEFAULT_TASKS_VIEW.highlightTaskId); setExpandedCommentsTaskId(null); }, []); const handleShowInCase = useCallback( async (task: LabTaskListItem) => { setLocatingCase(true); setError(''); setSearch(DEFAULT_TASKS_VIEW.search); setClinicId(DEFAULT_TASKS_VIEW.clinicId); setStatusFilter(DEFAULT_TASKS_VIEW.statusFilter); setStepCompleted(DEFAULT_TASKS_VIEW.stepCompleted); setImportantOnly(DEFAULT_TASKS_VIEW.importantOnly); setAssignedToMe(DEFAULT_TASKS_VIEW.assignedToMe); setOverdueOnly(DEFAULT_TASKS_VIEW.overdueOnly); setUnassignedOnly(DEFAULT_TASKS_VIEW.unassignedOnly); setProsthesisTypeCode(DEFAULT_TASKS_VIEW.prosthesisTypeCode); setSortBy(DEFAULT_TASKS_VIEW.sortBy); setSortDir(DEFAULT_TASKS_VIEW.sortDir); setExpandedCommentsTaskId(null); setExpandedCommentsCaseId(null); setHighlightTaskId(task.id); try { const response = await tasksApi.locatePage(buildDefaultLocateParams(task.id, PAGE_SIZE)); if (!response.data.found) { showError(t('showInCaseNotFound')); setHighlightTaskId(null); setPage(1); return; } setPage(response.data.page); } catch (error: unknown) { showError(getUserFacingError(error, tErrors, t('showInCaseError'))); setHighlightTaskId(null); setPage(1); } finally { setLocatingCase(false); } }, [setError, showError, t, tErrors], ); const handleStatusUpdate = useCallback( async (taskId: string, status: LabTaskStatus) => { if (!canEdit) return; setUpdatingTaskId(taskId); setError(''); try { await tasksApi.updateStatus(taskId, status); const willLeaveList = status === 'COMPLETED' && statusFilter === 'IN_PROGRESS'; if (willLeaveList) { setTasks((prev) => prev.map((task) => (task.id === taskId ? { ...task, status: 'COMPLETED' } : task)), ); setExitingTaskIds((prev) => new Set(prev).add(taskId)); showSuccess(t('taskCompletedToast')); window.setTimeout(() => { setExitingTaskIds((prev) => { const next = new Set(prev); next.delete(taskId); return next; }); void loadTasksRef.current(); }, TASK_COMPLETE_EXIT_MS); } else { await loadTasks(); } notifyTabBadgesChanged(); } catch (error: unknown) { showError(getUserFacingError(error, tErrors, t('errorUpdateTask'))); } finally { setUpdatingTaskId(null); } }, [canEdit, loadTasks, setError, showError, showSuccess, statusFilter, t, tErrors], ); const filterSelectClass = `${FORM_SELECT_CLASS} w-full min-w-0 rounded-md py-1.5 text-xs sm:py-2 sm:text-sm`; const sortHintKey = useMemo(() => { switch (sortBy) { case 'clinic': return 'groupingOffClinic'; case 'patient': return 'groupingOffPatient'; case 'prosthesis': return 'groupingOffProsthesis'; case 'taskType': return 'groupingOffTaskType'; case 'status': return 'groupingOffStatus'; case 'dueDate': return 'groupingOffDueDate'; default: return null; } }, [sortBy]); const applyFilterChange = useCallback( (apply: () => void) => { apply(); setPage(1); clearFocus(); }, [clearFocus], ); const renderTaskRow = (task: LabTaskListItem, flatMode: boolean) => ( void handleStatusUpdate(id, status)} onToggleComments={(id) => setExpandedCommentsTaskId((prev) => (prev === id ? null : id)) } onCommentError={showError} onShowInCase={flatMode ? (row) => handleShowInCase(row) : undefined} /> ); if (!isAuthReady) { return
{t('loading')}
; } if (!canView) { return (

{t('noPermissionTitle')}

{t('noPermissionBody')}

); } return (

{t('title')}

{t('subtitle')}

applyFilterChange(() => setSearch(v))} placeholder={t('searchPlaceholder')} />
applyFilterChange(() => setImportantOnly(checked))} label={t('importantOnly')} className="text-xs [&_span:last-child]:text-xs" /> applyFilterChange(() => setAssignedToMe(checked))} label={t('assignedToMe')} className="text-xs [&_span:last-child]:text-xs" /> applyFilterChange(() => setOverdueOnly(checked))} label={t('overdueOnly')} className="text-xs [&_span:last-child]:text-xs" /> applyFilterChange(() => setUnassignedOnly(checked))} label={t('unassignedOnly')} className="text-xs [&_span:last-child]:text-xs" /> {showReset ? ( ) : null}
{groupingDisabled && sortHintKey ? (

{t(sortHintKey)}

) : null}
{(loading || locatingCase) && tasks.length === 0 ? (

{locatingCase ? t('locatingCase') : t('loading')}

) : tasks.length === 0 ? (

{t('emptyList')}

) : displayModel.mode === 'grouped' ? (
{displayModel.cases.map((caseGroup) => (
setExpandedCommentsCaseId((prev) => prev === caseGroup.labCaseId ? null : caseGroup.labCaseId, ) } /> {expandedCommentsCaseId === caseGroup.labCaseId && canEdit ? (
{ const r = await tasksApi.listComments(caseGroup.labCaseId); return r.data; }} onPost={async (body, visibleToClinic) => { const r = await tasksApi.addComment(caseGroup.labCaseId, { body, visibleToClinic, }); notifyTabBadgesChanged(); return r.data; }} onToggleVisibility={async (commentId, visible) => { const r = await tasksApi.setCommentVisibility(commentId, visible); return r.data; }} onError={showError} />
) : null} {caseGroup.prosthesisGroups.map((prosthesisGroup) => (
    {prosthesisGroup.tasks.map((task) => renderTaskRow(task, false))}
))}
))}
) : (
    {displayModel.tasks.map((task) => renderTaskRow(task, true))}
)}
{pagination.totalPages > 1 ? (

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

) : null}
); }