improvement: tasks and cases feature updated based on the new prosthesis types and their steps. the whole assignment proccess removed from the flow.
This commit is contained in:
@@ -17,16 +17,18 @@ import { FORM_SELECT_CLASS } from '@/components/ui/shared/formSelectStyles';
|
||||
import { SearchBar } from '@/components/ui/shared/SearchBar';
|
||||
import type { TreatmentCatalogEntry } from '@/types/treatment-catalog';
|
||||
import type {
|
||||
AssignableMember,
|
||||
CasesFilterOptions,
|
||||
LabCaseDetail,
|
||||
LabCaseListItem,
|
||||
LabTaskStatus,
|
||||
PaginatedLabCases,
|
||||
} from '@/types/cases';
|
||||
import {
|
||||
formatToothList,
|
||||
prosthesisTypeBadgeStyle,
|
||||
} from '@/components/ui/treatment/prosthesisTypeDisplay';
|
||||
|
||||
const PAGE_SIZE = 20;
|
||||
const PRIORITY_OPTIONS = [1, 2, 3, 4, 5] as const;
|
||||
|
||||
function taskStatusVariant(status: LabTaskStatus): BadgeVariant {
|
||||
switch (status) {
|
||||
@@ -100,7 +102,6 @@ export default function CasesPage() {
|
||||
|
||||
const [selectedCaseId, setSelectedCaseId] = useState<string | null>(null);
|
||||
const [selectedCase, setSelectedCase] = useState<LabCaseDetail | null>(null);
|
||||
const [members, setMembers] = useState<AssignableMember[]>([]);
|
||||
const [loadingList, setLoadingList] = useState(false);
|
||||
const [loadingDetail, setLoadingDetail] = useState(false);
|
||||
const [updatingTaskId, setUpdatingTaskId] = useState<string | null>(null);
|
||||
@@ -115,7 +116,6 @@ export default function CasesPage() {
|
||||
|
||||
const statusOptions: { value: LabTaskStatus; label: string }[] = useMemo(
|
||||
() => [
|
||||
{ value: 'PENDING', label: t('statusPending') },
|
||||
{ value: 'IN_PROGRESS', label: t('statusInProgress') },
|
||||
{ value: 'COMPLETED', label: t('statusCompleted') },
|
||||
],
|
||||
@@ -171,7 +171,6 @@ export default function CasesPage() {
|
||||
|
||||
useEffect(() => {
|
||||
void casesApi.listFilterOptions().then((r) => setFilterOptions(r.data)).catch(() => {});
|
||||
void casesApi.listAssignableMembers().then((r) => setMembers(r.data)).catch(() => {});
|
||||
void treatmentCatalogApi.list().then((r) => setTreatmentCatalog(r.data)).catch(() => {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only initial fetch
|
||||
}, []);
|
||||
@@ -216,25 +215,14 @@ export default function CasesPage() {
|
||||
setPage(1);
|
||||
}
|
||||
|
||||
async function handleTaskUpdate(
|
||||
taskId: string,
|
||||
payload: { assigneeUserId?: string | null; priority?: number },
|
||||
) {
|
||||
async function handleImportantToggle(taskId: string, isImportant: boolean) {
|
||||
if (!selectedCaseId || !canEdit) return;
|
||||
|
||||
setUpdatingTaskId(taskId);
|
||||
toast.setError('');
|
||||
try {
|
||||
await casesApi.updateTask(selectedCaseId, taskId, payload);
|
||||
await casesApi.setTaskImportant(selectedCaseId, taskId, isImportant);
|
||||
await loadDetail(selectedCaseId);
|
||||
await loadCases({
|
||||
q: search,
|
||||
clinicOrganizationId: clinicId,
|
||||
treatmentType,
|
||||
sentFrom,
|
||||
sentTo,
|
||||
page,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
toast.showError(formatApiErrorMessage(error, t('errorUpdateTask')));
|
||||
} finally {
|
||||
@@ -476,65 +464,65 @@ export default function CasesPage() {
|
||||
{selectedCase.tasksByTooth.length === 0 ? (
|
||||
<p className="text-sm text-text-muted">{t('noTasks')}</p>
|
||||
) : (
|
||||
selectedCase.tasksByTooth.map((group) => (
|
||||
selectedCase.tasksByTooth.map((group, groupIndex) => (
|
||||
<div
|
||||
key={`${group.tooth}-${group.treatmentType}`}
|
||||
key={`${group.treatmentDetailId}-${group.prosthesisTypeCode}`}
|
||||
className="rounded-md border border-border p-3 space-y-2"
|
||||
>
|
||||
<div className="text-sm font-medium text-text-primary">
|
||||
{t('toothGroupTitle', {
|
||||
tooth: group.tooth,
|
||||
prosthesis: group.prosthesisTypeLabel,
|
||||
type: treatmentLabel(group.treatmentType),
|
||||
})}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
className="inline-flex items-center rounded px-2 py-0.5 text-xs font-medium border"
|
||||
style={prosthesisTypeBadgeStyle(group.prosthesisTypeCode, groupIndex)}
|
||||
>
|
||||
{group.prosthesisTypeLabel}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-text-primary">
|
||||
{t('toothGroupTitle', {
|
||||
teeth: formatToothList(group.teeth),
|
||||
prosthesis: group.prosthesisTypeLabel,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<ul className="space-y-2">
|
||||
{group.tasks.map((task) => (
|
||||
<li
|
||||
key={task.id}
|
||||
className="grid gap-2 sm:grid-cols-[minmax(0,1fr)_auto_88px_180px] items-center text-sm rounded bg-background p-2"
|
||||
className="rounded bg-background p-2 text-sm space-y-1"
|
||||
>
|
||||
<span>
|
||||
{task.stepOrder}. {task.stepLabel}
|
||||
</span>
|
||||
<Badge variant={taskStatusVariant(task.status)} fixedWidth={false}>
|
||||
{statusOptions.find((opt) => opt.value === task.status)?.label ??
|
||||
task.status}
|
||||
</Badge>
|
||||
<select
|
||||
value={task.priority}
|
||||
disabled={!canEdit || updatingTaskId === task.id}
|
||||
onChange={(e) =>
|
||||
void handleTaskUpdate(task.id, {
|
||||
priority: Number(e.target.value),
|
||||
})
|
||||
}
|
||||
className={FORM_SELECT_CLASS}
|
||||
aria-label={t('priorityLabel')}
|
||||
>
|
||||
{PRIORITY_OPTIONS.map((value) => (
|
||||
<option key={value} value={value}>
|
||||
{value}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
value={task.assigneeUserId ?? ''}
|
||||
disabled={!canEdit || updatingTaskId === task.id}
|
||||
onChange={(e) =>
|
||||
void handleTaskUpdate(task.id, {
|
||||
assigneeUserId: e.target.value || null,
|
||||
})
|
||||
}
|
||||
className={FORM_SELECT_CLASS}
|
||||
>
|
||||
<option value="">{t('unassigned')}</option>
|
||||
{members.map((member) => (
|
||||
<option key={member.userId} value={member.userId}>
|
||||
{member.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<span className="min-w-0 flex-1">
|
||||
{task.stepOrder}. {task.stepLabel}
|
||||
</span>
|
||||
<Badge variant={taskStatusVariant(task.status)} fixedWidth={false}>
|
||||
{statusOptions.find((opt) => opt.value === task.status)?.label ??
|
||||
task.status}
|
||||
</Badge>
|
||||
{canEdit ? (
|
||||
<label className="flex items-center gap-1.5 text-xs cursor-pointer shrink-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={task.isImportant}
|
||||
disabled={updatingTaskId === task.id}
|
||||
onChange={(e) =>
|
||||
void handleImportantToggle(task.id, e.target.checked)
|
||||
}
|
||||
/>
|
||||
{t('importantLabel')}
|
||||
</label>
|
||||
) : task.isImportant ? (
|
||||
<Badge variant="warning" fixedWidth={false}>
|
||||
{t('importantLabel')}
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
<p className="text-[11px] text-text-muted">
|
||||
{task.lastStatusChangedBy
|
||||
? t('lastUpdatedBy', { name: task.lastStatusChangedBy.name })
|
||||
: t('lastUpdatedUnknown')}
|
||||
{task.lastStatusChangedAt
|
||||
? ` · ${formatDateTime(task.lastStatusChangedAt, locale)}`
|
||||
: ''}
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user