improvement: sorts and filters updated for tasks feature.
This commit is contained in:
@@ -17,6 +17,7 @@ import { casesApi } from '@/lib/api/cases';
|
||||
import { tasksApi } from '@/lib/api/tasks';
|
||||
import { treatmentCatalogApi } from '@/lib/api/treatment-catalog';
|
||||
import { treatmentTypeLabelFromCatalog } from '@/components/shared/treatmentTypeDisplay';
|
||||
import { Badge } from '@/components/ui/shared/Badge';
|
||||
import { Button } from '@/components/ui/shared/Button';
|
||||
import { MobileDetailBackButton } from '@/components/ui/shared/MobileDetailBackButton';
|
||||
import { FORM_SELECT_CLASS } from '@/components/shared/formSelectStyles';
|
||||
@@ -216,6 +217,11 @@ export function CasesPage() {
|
||||
try {
|
||||
const response = await casesApi.setCaseImportant(selectedCaseId, isImportant);
|
||||
setSelectedCase(response.data);
|
||||
setCases((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === selectedCaseId ? { ...item, isImportant: response.data.isImportant } : item,
|
||||
),
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
setSelectedCase(previousCase);
|
||||
toast.showError(getUserFacingError(error, tErrors, t('errorUpdateTask')));
|
||||
@@ -345,8 +351,15 @@ export function CasesPage() {
|
||||
: 'border-border hover:border-primary/40'
|
||||
}`}
|
||||
>
|
||||
<div className="font-medium text-text-primary">
|
||||
{formatPatientName(item.patient)}
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<div className="font-medium text-text-primary">
|
||||
{formatPatientName(item.patient)}
|
||||
</div>
|
||||
{item.isImportant ? (
|
||||
<Badge variant="warning" fixedWidth={false}>
|
||||
{t('importantLabel')}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="text-xs text-text-muted mt-0.5">
|
||||
{item.patient.mobile}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
'use client';
|
||||
|
||||
import { MessageSquare } from 'lucide-react';
|
||||
import { Check, FolderOpen, MessageSquare } from 'lucide-react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Badge } from '@/components/ui/shared/Badge';
|
||||
import { Button } from '@/components/ui/shared/Button';
|
||||
import { FORM_SELECT_CLASS } from '@/components/shared/formSelectStyles';
|
||||
import { LabCaseCommentsPanel } from '@/components/ui/lab/LabCaseCommentsPanel';
|
||||
import {
|
||||
@@ -21,6 +22,8 @@ interface TaskRowProps {
|
||||
task: LabTaskListItem;
|
||||
locale: string;
|
||||
flatMode: boolean;
|
||||
highlighted?: boolean;
|
||||
exiting?: boolean;
|
||||
canEdit: boolean;
|
||||
statusOptions: { value: LabTaskStatus; label: string }[];
|
||||
updatingTaskId: string | null;
|
||||
@@ -29,6 +32,7 @@ interface TaskRowProps {
|
||||
onStatusUpdate: (taskId: string, status: LabTaskStatus) => void;
|
||||
onToggleComments: (taskId: string) => void;
|
||||
onCommentError: (message: string) => void;
|
||||
onShowInCase?: (task: LabTaskListItem) => void;
|
||||
}
|
||||
|
||||
function formatPatientName(patient: { firstName: string; lastName: string }) {
|
||||
@@ -39,6 +43,8 @@ export function TaskRow({
|
||||
task,
|
||||
locale,
|
||||
flatMode,
|
||||
highlighted = false,
|
||||
exiting = false,
|
||||
canEdit,
|
||||
statusOptions,
|
||||
updatingTaskId,
|
||||
@@ -47,6 +53,7 @@ export function TaskRow({
|
||||
onStatusUpdate,
|
||||
onToggleComments,
|
||||
onCommentError,
|
||||
onShowInCase,
|
||||
}: TaskRowProps) {
|
||||
const t = useTranslations('tasks');
|
||||
const taskDate = new Intl.DateTimeFormat(locale, {
|
||||
@@ -55,8 +62,18 @@ export function TaskRow({
|
||||
day: 'numeric',
|
||||
}).format(new Date(task.createdAt));
|
||||
|
||||
const rowClassName = [
|
||||
flatMode ? undefined : 'border-b border-border/40 last:border-b-0',
|
||||
exiting ? 'task-row-complete-exit' : undefined,
|
||||
!exiting && highlighted
|
||||
? 'relative bg-primary/10 ring-2 ring-inset ring-primary/50 shadow-[0_0_16px_rgba(99,102,241,0.2)]'
|
||||
: undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<li className={flatMode ? undefined : 'border-b border-border/40 last:border-b-0'}>
|
||||
<li id={`task-row-${task.id}`} className={rowClassName}>
|
||||
<div
|
||||
className={`flex flex-col gap-3 px-3 py-3 sm:grid sm:grid-cols-[minmax(0,1fr)_132px_auto] sm:items-center sm:gap-x-3 sm:gap-y-0.5 sm:py-2 ${
|
||||
flatMode ? '' : 'ps-5'
|
||||
@@ -67,7 +84,13 @@ export function TaskRow({
|
||||
<p className="text-sm font-medium text-text-primary">
|
||||
{task.stepOrder}. {task.stepLabel}
|
||||
</p>
|
||||
{flatMode && task.isImportant ? (
|
||||
{exiting ? (
|
||||
<span className="inline-flex items-center gap-1 text-[11px] font-medium text-emerald-700 dark:text-emerald-400">
|
||||
<Check className="h-3.5 w-3.5" />
|
||||
{t('taskCompletedFlash')}
|
||||
</span>
|
||||
) : null}
|
||||
{flatMode && task.isImportant && !exiting ? (
|
||||
<Badge variant="warning" fixedWidth={false}>
|
||||
{t('importantBadge')}
|
||||
</Badge>
|
||||
@@ -94,7 +117,7 @@ export function TaskRow({
|
||||
{canEdit ? (
|
||||
<select
|
||||
value={task.status}
|
||||
disabled={updatingTaskId === task.id}
|
||||
disabled={updatingTaskId === task.id || exiting}
|
||||
onChange={(e) => onStatusUpdate(task.id, e.target.value as LabTaskStatus)}
|
||||
className={`${FORM_SELECT_CLASS} w-full sm:max-w-[132px] font-medium`}
|
||||
style={labTaskStatusSelectStyle(task.status)}
|
||||
@@ -112,8 +135,20 @@ export function TaskRow({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 shrink-0 justify-between sm:justify-end">
|
||||
{canEdit ? (
|
||||
<div className="flex items-center gap-1.5 shrink-0 justify-between sm:justify-end flex-wrap">
|
||||
{flatMode && onShowInCase && !exiting ? (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-xs px-2 py-1 h-auto"
|
||||
onClick={() => onShowInCase(task)}
|
||||
>
|
||||
<FolderOpen className="h-3.5 w-3.5 me-1" />
|
||||
{t('showInCase')}
|
||||
</Button>
|
||||
) : null}
|
||||
{canEdit && !exiting ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onToggleComments(task.id)}
|
||||
@@ -144,7 +179,7 @@ export function TaskRow({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{commentsOpen && canEdit ? (
|
||||
{commentsOpen && canEdit && !exiting ? (
|
||||
<div className="px-3 pb-3 border-t border-border/50">
|
||||
<LabCaseCommentsPanel
|
||||
caseId={task.labCaseId}
|
||||
|
||||
@@ -3,12 +3,20 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Button } from '@/components/ui/shared/Button';
|
||||
import { Checkbox } from '@/components/ui/shared/Checkbox';
|
||||
import { FORM_SELECT_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 { groupTasksForDisplay } from '@/components/lab/taskListGrouping';
|
||||
import {
|
||||
buildDefaultLocateParams,
|
||||
DEFAULT_TASKS_VIEW,
|
||||
isDefaultTasksView,
|
||||
TASK_COMPLETE_EXIT_MS,
|
||||
} from '@/components/lab/tasksViewDefaults';
|
||||
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';
|
||||
@@ -31,7 +39,7 @@ export function TasksPage() {
|
||||
const t = useTranslations('tasks');
|
||||
const tErrors = useTranslations('errors');
|
||||
const { currentOrganization, user, isAuthReady } = useAuth();
|
||||
const { showError, setError, messages: toastMessages } = useToast();
|
||||
const { showError, showSuccess, setError } = useToast();
|
||||
|
||||
const [tasks, setTasks] = useState<LabTaskListItem[]>([]);
|
||||
const [pagination, setPagination] = useState<PaginatedLabTasks['pagination']>({
|
||||
@@ -45,17 +53,25 @@ export function TasksPage() {
|
||||
workflowSteps: [],
|
||||
});
|
||||
const [prosthesisCatalog, setProsthesisCatalog] = useState<ProsthesisCatalogEntry[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [page, setPage] = useState(DEFAULT_TASKS_VIEW.page);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [locatingCase, setLocatingCase] = useState(false);
|
||||
const [updatingTaskId, setUpdatingTaskId] = useState<string | null>(null);
|
||||
const [exitingTaskIds, setExitingTaskIds] = useState<Set<string>>(() => new Set());
|
||||
const [expandedCommentsTaskId, setExpandedCommentsTaskId] = useState<string | null>(null);
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [clinicId, setClinicId] = useState('');
|
||||
const [statusFilter, setStatusFilter] = useState<'' | LabTaskStatus>('IN_PROGRESS');
|
||||
const [stepCompleted, setStepCompleted] = useState('');
|
||||
const [sortBy, setSortBy] = useState<TaskSortField>('date');
|
||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('desc');
|
||||
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 [sortBy, setSortBy] = useState<TaskSortField>(DEFAULT_TASKS_VIEW.sortBy);
|
||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>(DEFAULT_TASKS_VIEW.sortDir);
|
||||
const [highlightTaskId, setHighlightTaskId] = useState<string | null>(
|
||||
DEFAULT_TASKS_VIEW.highlightTaskId,
|
||||
);
|
||||
|
||||
const canView = canViewTasks(currentOrganization);
|
||||
const canEdit = canEditTasks(currentOrganization);
|
||||
@@ -63,6 +79,7 @@ export function TasksPage() {
|
||||
|
||||
const tRef = useRef(t);
|
||||
tRef.current = t;
|
||||
const loadTasksRef = useRef<() => Promise<void>>(async () => {});
|
||||
|
||||
const statusOptions: { value: LabTaskStatus; label: string }[] = useMemo(
|
||||
() => [
|
||||
@@ -83,16 +100,41 @@ export function TasksPage() {
|
||||
if (clinicId) params.clinicOrganizationId = clinicId;
|
||||
if (statusFilter) params.status = statusFilter;
|
||||
if (stepCompleted) params.stepCompleted = stepCompleted;
|
||||
if (importantOnly) params.pinImportant = true;
|
||||
return params;
|
||||
}, [page, search, clinicId, statusFilter, stepCompleted, sortBy, sortDir]);
|
||||
}, [page, search, clinicId, statusFilter, stepCompleted, importantOnly, sortBy, sortDir]);
|
||||
|
||||
const displayModel = useMemo(
|
||||
() => groupTasksForDisplay(tasks, sortBy),
|
||||
[tasks, sortBy],
|
||||
);
|
||||
const displayModel = useMemo(() => groupTasksForDisplay(tasks, sortBy), [tasks, sortBy]);
|
||||
|
||||
const groupingDisabled = sortBy !== 'date';
|
||||
|
||||
const viewState = useMemo(
|
||||
() => ({
|
||||
search,
|
||||
clinicId,
|
||||
statusFilter,
|
||||
stepCompleted,
|
||||
sortBy,
|
||||
sortDir,
|
||||
importantOnly,
|
||||
page,
|
||||
highlightTaskId,
|
||||
}),
|
||||
[
|
||||
search,
|
||||
clinicId,
|
||||
statusFilter,
|
||||
stepCompleted,
|
||||
sortBy,
|
||||
sortDir,
|
||||
importantOnly,
|
||||
page,
|
||||
highlightTaskId,
|
||||
],
|
||||
);
|
||||
|
||||
const showReset = !isDefaultTasksView(viewState);
|
||||
|
||||
const loadTasks = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError('');
|
||||
@@ -107,6 +149,8 @@ export function TasksPage() {
|
||||
}
|
||||
}, [listParams, showError, setError, tErrors]);
|
||||
|
||||
loadTasksRef.current = loadTasks;
|
||||
|
||||
useEffect(() => {
|
||||
if (!canView) return;
|
||||
const timeout = setTimeout(() => void loadTasks(), search ? 300 : 0);
|
||||
@@ -129,6 +173,66 @@ export function TasksPage() {
|
||||
})();
|
||||
}, [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);
|
||||
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);
|
||||
setSortBy(DEFAULT_TASKS_VIEW.sortBy);
|
||||
setSortDir(DEFAULT_TASKS_VIEW.sortDir);
|
||||
setExpandedCommentsTaskId(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;
|
||||
@@ -136,14 +240,34 @@ export function TasksPage() {
|
||||
setError('');
|
||||
try {
|
||||
await tasksApi.updateStatus(taskId, status);
|
||||
await loadTasks();
|
||||
|
||||
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();
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
showError(getUserFacingError(error, tErrors, t('errorUpdateTask')));
|
||||
} finally {
|
||||
setUpdatingTaskId(null);
|
||||
}
|
||||
},
|
||||
[canEdit, loadTasks, setError, showError, t, tErrors],
|
||||
[canEdit, loadTasks, setError, showError, showSuccess, statusFilter, t, tErrors],
|
||||
);
|
||||
|
||||
const filterSelectClass = `${FORM_SELECT_CLASS} w-full rounded-md px-2 py-1.5 text-sm`;
|
||||
@@ -165,6 +289,37 @@ export function TasksPage() {
|
||||
}
|
||||
}, [sortBy]);
|
||||
|
||||
const applyFilterChange = useCallback(
|
||||
(apply: () => void) => {
|
||||
apply();
|
||||
setPage(1);
|
||||
clearFocus();
|
||||
},
|
||||
[clearFocus],
|
||||
);
|
||||
|
||||
const renderTaskRow = (task: LabTaskListItem, flatMode: boolean) => (
|
||||
<TaskRow
|
||||
key={task.id}
|
||||
task={task}
|
||||
locale={locale}
|
||||
flatMode={flatMode}
|
||||
highlighted={highlightTaskId === task.id}
|
||||
exiting={exitingTaskIds.has(task.id)}
|
||||
canEdit={canEdit}
|
||||
statusOptions={statusOptions}
|
||||
updatingTaskId={updatingTaskId}
|
||||
commentsOpen={expandedCommentsTaskId === task.id}
|
||||
prosthesisCatalog={prosthesisCatalog}
|
||||
onStatusUpdate={(id, status) => void handleStatusUpdate(id, status)}
|
||||
onToggleComments={(id) =>
|
||||
setExpandedCommentsTaskId((prev) => (prev === id ? null : id))
|
||||
}
|
||||
onCommentError={showError}
|
||||
onShowInCase={flatMode ? (row) => void handleShowInCase(row) : undefined}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!isAuthReady) {
|
||||
return <div className="text-sm text-text-muted">{t('loading')}</div>;
|
||||
}
|
||||
@@ -189,10 +344,7 @@ export function TasksPage() {
|
||||
<SearchBar
|
||||
embedded
|
||||
value={search}
|
||||
onChange={(v) => {
|
||||
setSearch(v);
|
||||
setPage(1);
|
||||
}}
|
||||
onChange={(v) => applyFilterChange(() => setSearch(v))}
|
||||
placeholder={t('searchPlaceholder')}
|
||||
/>
|
||||
<div className="grid gap-2 sm:grid-cols-2 lg:grid-cols-4">
|
||||
@@ -200,10 +352,7 @@ export function TasksPage() {
|
||||
<span className="text-xs text-text-muted">{t('filterClinic')}</span>
|
||||
<select
|
||||
value={clinicId}
|
||||
onChange={(e) => {
|
||||
setClinicId(e.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
onChange={(e) => applyFilterChange(() => setClinicId(e.target.value))}
|
||||
className={filterSelectClass}
|
||||
>
|
||||
<option value="">{t('filterClinicAll')}</option>
|
||||
@@ -218,28 +367,21 @@ export function TasksPage() {
|
||||
<span className="text-xs text-text-muted">{t('filterStatus')}</span>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => {
|
||||
setStatusFilter(e.target.value as '' | LabTaskStatus);
|
||||
setPage(1);
|
||||
}}
|
||||
onChange={(e) =>
|
||||
applyFilterChange(() => setStatusFilter(e.target.value as '' | LabTaskStatus))
|
||||
}
|
||||
className={filterSelectClass}
|
||||
>
|
||||
<option value="IN_PROGRESS">{t('statusInProgress')}</option>
|
||||
<option value="COMPLETED">{t('statusCompleted')}</option>
|
||||
<option value="">{t('filterStatusAll')}</option>
|
||||
{statusOptions.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="space-y-1">
|
||||
<span className="text-xs text-text-muted">{t('filterStepCompleted')}</span>
|
||||
<select
|
||||
value={stepCompleted}
|
||||
onChange={(e) => {
|
||||
setStepCompleted(e.target.value);
|
||||
setPage(1);
|
||||
}}
|
||||
onChange={(e) => applyFilterChange(() => setStepCompleted(e.target.value))}
|
||||
className={filterSelectClass}
|
||||
>
|
||||
<option value="">{t('filterStepCompletedAll')}</option>
|
||||
@@ -255,7 +397,10 @@ export function TasksPage() {
|
||||
<div className="flex gap-1.5">
|
||||
<select
|
||||
value={sortBy}
|
||||
onChange={(e) => setSortBy(e.target.value as TaskSortField)}
|
||||
onChange={(e) => {
|
||||
setSortBy(e.target.value as TaskSortField);
|
||||
clearFocus();
|
||||
}}
|
||||
className={`${filterSelectClass} min-w-0 flex-1`}
|
||||
>
|
||||
<option value="date">{t('sortDate')}</option>
|
||||
@@ -276,14 +421,29 @@ export function TasksPage() {
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
|
||||
<Checkbox
|
||||
checked={importantOnly}
|
||||
onChange={(checked) => applyFilterChange(() => setImportantOnly(checked))}
|
||||
label={t('importantOnly')}
|
||||
className="text-xs [&_span:last-child]:text-xs"
|
||||
/>
|
||||
{showReset ? (
|
||||
<Button type="button" variant="ghost" size="sm" onClick={resetView}>
|
||||
{t('resetView')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
{groupingDisabled && sortHintKey ? (
|
||||
<p className="text-[11px] text-text-muted">{t(sortHintKey)}</p>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className="surface-card min-h-[280px]">
|
||||
{loading && tasks.length === 0 ? (
|
||||
<p className="p-3 text-sm text-text-muted">{t('loading')}</p>
|
||||
{(loading || locatingCase) && tasks.length === 0 ? (
|
||||
<p className="p-3 text-sm text-text-muted">
|
||||
{locatingCase ? t('locatingCase') : t('loading')}
|
||||
</p>
|
||||
) : tasks.length === 0 ? (
|
||||
<p className="p-3 text-sm text-text-muted">{t('emptyList')}</p>
|
||||
) : displayModel.mode === 'grouped' ? (
|
||||
@@ -301,24 +461,7 @@ export function TasksPage() {
|
||||
prosthesisCatalog={prosthesisCatalog}
|
||||
/>
|
||||
<ul>
|
||||
{prosthesisGroup.tasks.map((task) => (
|
||||
<TaskRow
|
||||
key={task.id}
|
||||
task={task}
|
||||
locale={locale}
|
||||
flatMode={false}
|
||||
canEdit={canEdit}
|
||||
statusOptions={statusOptions}
|
||||
updatingTaskId={updatingTaskId}
|
||||
commentsOpen={expandedCommentsTaskId === task.id}
|
||||
prosthesisCatalog={prosthesisCatalog}
|
||||
onStatusUpdate={(id, status) => void handleStatusUpdate(id, status)}
|
||||
onToggleComments={(id) =>
|
||||
setExpandedCommentsTaskId((prev) => (prev === id ? null : id))
|
||||
}
|
||||
onCommentError={showError}
|
||||
/>
|
||||
))}
|
||||
{prosthesisGroup.tasks.map((task) => renderTaskRow(task, false))}
|
||||
</ul>
|
||||
</div>
|
||||
))}
|
||||
@@ -327,29 +470,12 @@ export function TasksPage() {
|
||||
</div>
|
||||
) : (
|
||||
<ul className="divide-y divide-border">
|
||||
{displayModel.tasks.map((task) => (
|
||||
<TaskRow
|
||||
key={task.id}
|
||||
task={task}
|
||||
locale={locale}
|
||||
flatMode
|
||||
canEdit={canEdit}
|
||||
statusOptions={statusOptions}
|
||||
updatingTaskId={updatingTaskId}
|
||||
commentsOpen={expandedCommentsTaskId === task.id}
|
||||
prosthesisCatalog={prosthesisCatalog}
|
||||
onStatusUpdate={(id, status) => void handleStatusUpdate(id, status)}
|
||||
onToggleComments={(id) =>
|
||||
setExpandedCommentsTaskId((prev) => (prev === id ? null : id))
|
||||
}
|
||||
onCommentError={showError}
|
||||
/>
|
||||
))}
|
||||
{displayModel.tasks.map((task) => renderTaskRow(task, true))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
{pagination.totalPages > 1 && (
|
||||
{pagination.totalPages > 1 ? (
|
||||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||
<p className="text-sm text-text-muted">
|
||||
{t('pageSummary', {
|
||||
@@ -363,7 +489,10 @@ export function TasksPage() {
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={page <= 1 || loading}
|
||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||
onClick={() => {
|
||||
setPage((p) => Math.max(1, p - 1));
|
||||
clearFocus();
|
||||
}}
|
||||
>
|
||||
←
|
||||
</Button>
|
||||
@@ -371,13 +500,16 @@ export function TasksPage() {
|
||||
type="button"
|
||||
variant="secondary"
|
||||
disabled={page >= pagination.totalPages || loading}
|
||||
onClick={() => setPage((p) => p + 1)}
|
||||
onClick={() => {
|
||||
setPage((p) => p + 1);
|
||||
clearFocus();
|
||||
}}
|
||||
>
|
||||
→
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user