Files
dyolink/frontend/src/components/ui/lab/CaseTasksFocusView.tsx

218 lines
8.1 KiB
TypeScript

'use client';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslations } from 'next-intl';
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 { canEditLabTaskStatus } from '@/components/lab/labTaskStatusDisplay';
import { getUserFacingError } from '@/components/shared/formatApiError';
import { asApiError } from '@/types/api';
import { useAuth } from '@/lib/hooks/useAuth';
import { useToast } from '@/lib/hooks/useToast';
import { labCaseAccessApi, type LabCaseAccessSession } from '@/lib/api/lab-case-access';
import { prosthesisCatalogApi } from '@/lib/api/prosthesis-catalog';
import { tasksApi } from '@/lib/api/tasks';
import { notifyTabBadgesChanged } from '@/lib/tabBadgeUtils';
import type { LabTaskListItem, LabTaskStatus } from '@/types/cases';
import type { ProsthesisCatalogEntry } from '@/types/treatment-catalog';
interface CaseTasksFocusViewProps {
token: string;
}
export function CaseTasksFocusView({ token }: CaseTasksFocusViewProps) {
const t = useTranslations('labCaseAccess');
const tTasks = useTranslations('tasks');
const tErrors = useTranslations('errors');
const { user, isAuthReady } = useAuth();
const { showError, showSuccess } = useToast();
const [session, setSession] = useState<LabCaseAccessSession | null>(null);
const [tasks, setTasks] = useState<LabTaskListItem[]>([]);
const [loading, setLoading] = useState(true);
const [accessDenied, setAccessDenied] = useState(false);
const [updatingTaskId, setUpdatingTaskId] = useState<string | null>(null);
const [prosthesisCatalog, setProsthesisCatalog] = useState<ProsthesisCatalogEntry[]>([]);
const locale = user?.language ?? 'en';
const canEditTasks = session?.canEditTaskStatus ?? false;
const canPostComments = session?.canPostComments ?? false;
const canToggleCommentVisibility = session?.canToggleCommentVisibility ?? false;
const statusOptions: { value: LabTaskStatus; label: string }[] = useMemo(
() => [
{ value: 'IN_PROGRESS', label: tTasks('statusInProgress') },
{ value: 'COMPLETED', label: tTasks('statusCompleted') },
],
[tTasks],
);
const loadData = useCallback(async () => {
setLoading(true);
setAccessDenied(false);
try {
const [sessionRes, tasksRes] = await Promise.all([
labCaseAccessApi.resolve(token),
labCaseAccessApi.listTasks(token),
]);
setSession(sessionRes.data);
setTasks(tasksRes.data.items);
} catch (error: unknown) {
if (asApiError(error)?.code === 'LAB_CASE_ACCESS_DENIED') {
setAccessDenied(true);
} else {
showError(getUserFacingError(error, tErrors, t('accessDenied')));
}
setSession(null);
setTasks([]);
} finally {
setLoading(false);
}
}, [showError, t, tErrors, token]);
useEffect(() => {
if (!isAuthReady || !user) return;
void loadData();
}, [isAuthReady, user, loadData]);
useEffect(() => {
void prosthesisCatalogApi
.list()
.then((response) => setProsthesisCatalog(response.data))
.catch(() => {});
}, []);
const displayModel = useMemo(() => groupTasksForDisplay(tasks, 'date'), [tasks]);
const handleStatusUpdate = useCallback(
async (taskId: string, status: LabTaskStatus) => {
if (!canEditTasks || !user?.id) return;
const task = tasks.find((item) => item.id === taskId);
if (!task || !canEditLabTaskStatus(task, user.id, canEditTasks)) return;
setUpdatingTaskId(taskId);
try {
await tasksApi.updateStatus(taskId, status);
await loadData();
notifyTabBadgesChanged();
if (status === 'COMPLETED') {
showSuccess(tTasks('taskCompletedToast'));
}
} catch (error: unknown) {
showError(getUserFacingError(error, tErrors, tTasks('errorUpdateTask')));
} finally {
setUpdatingTaskId(null);
}
},
[canEditTasks, loadData, showError, showSuccess, tErrors, tTasks, tasks, user?.id],
);
if (!isAuthReady || loading) {
return <p className="text-sm text-text-muted">{t('loading')}</p>;
}
if (accessDenied) {
return (
<div className="rounded-md border border-border bg-background-secondary/40 p-4 sm:p-6 text-center space-y-2 min-w-0">
<p className="text-sm font-medium text-text-primary">{t('accessDeniedTitle')}</p>
<p className="text-sm text-text-muted break-words">{t('accessDenied')}</p>
</div>
);
}
if (!session || displayModel.mode !== 'grouped' || displayModel.cases.length === 0) {
return <p className="text-sm text-text-muted">{t('emptyTasks')}</p>;
}
const caseGroup = displayModel.cases[0];
return (
<div className="space-y-4 min-w-0">
<header className="space-y-1 border-b border-border pb-3 min-w-0">
<h1 className="text-xl sm:text-2xl font-semibold text-text-primary">{t('pageTitle')}</h1>
<p className="text-sm text-text-muted break-words">
{t('caseSummary', {
clinic: session.clinic.name,
patient: `${session.patient.firstName} ${session.patient.lastName}`.trim(),
})}
</p>
{session.lab ? (
<p className="text-sm text-text-muted break-words">
{t('labName', { name: session.lab.name })}
</p>
) : null}
</header>
<section className="surface-card min-h-[280px] min-w-0 overflow-x-hidden">
<section className="border-b border-border last:border-b-0">
<TaskCaseGroupHeader caseGroup={caseGroup} locale={locale} />
{caseGroup.prosthesisGroups.map((prosthesisGroup) => (
<div key={prosthesisGroup.key} className="border-t border-border/50 first:border-t-0">
<TaskProsthesisGroupHeader
group={prosthesisGroup}
prosthesisCatalog={prosthesisCatalog}
/>
<ul>
{prosthesisGroup.tasks.map((task) => (
<TaskRow
key={task.id}
task={task}
locale={locale}
flatMode={false}
canEdit={canEditTasks}
currentUserId={user?.id}
statusOptions={statusOptions}
updatingTaskId={updatingTaskId}
commentsOpen={false}
showCommentsButton={false}
prosthesisCatalog={prosthesisCatalog}
onStatusUpdate={(id, status) => void handleStatusUpdate(id, status)}
onToggleComments={() => {}}
onCommentError={showError}
/>
))}
</ul>
</div>
))}
</section>
</section>
<section className="surface-card p-3 sm:p-4 min-w-0 overflow-x-hidden">
<LabCaseCommentsPanel
caseId={session.labCaseId}
viewerSide={session.accessMode === 'lab' ? 'LAB' : 'CLINIC'}
canPost={canPostComments}
canToggleVisibility={canToggleCommentVisibility}
loadComments={async () => {
const r = await labCaseAccessApi.listComments(token);
return r.data;
}}
onPost={async (body, visibleToClinic) => {
const r = await labCaseAccessApi.addComment(token, {
body,
visibleToClinic,
});
notifyTabBadgesChanged();
return r.data;
}}
onToggleVisibility={
canToggleCommentVisibility
? async (commentId, visible) => {
const r = await labCaseAccessApi.setCommentVisibility(
token,
commentId,
visible,
);
return r.data;
}
: undefined
}
onError={showError}
/>
</section>
</div>
);
}