217 lines
8.0 KiB
TypeScript
217 lines
8.0 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}
|
||
|
|
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>
|
||
|
|
);
|
||
|
|
}
|