feature: Tasks tab added for lab organizations. tasks now can be assigned and their status can be updated by the assignee.

This commit is contained in:
2026-06-28 22:56:56 +03:30
parent feb0b26ad0
commit 2a48946a51
29 changed files with 851 additions and 43 deletions

View File

@@ -1,14 +1,17 @@
'use client';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'next/navigation';
import { useTranslations } from 'next-intl';
import { ToastStack } from '@/components/ui/shared/Toast';
import { formatApiErrorMessage } from '@/components/shared/formatApiError';
import { useAuth } from '@/lib/hooks/useAuth';
import { useToast } from '@/lib/hooks/useToast';
import { hasPermission } from '@/components/shared/permissions';
import { canEditCases } from '@/components/shared/permissions';
import { Badge, type BadgeVariant } from '@/components/ui/shared/Badge';
import { casesApi } from '@/lib/api/cases';
import { Button } from '@/components/ui/shared/Button';
import { FORM_SELECT_CLASS } from '@/components/ui/shared/formSelectStyles';
import { SearchBar } from '@/components/ui/shared/SearchBar';
import type {
AssignableMember,
@@ -28,6 +31,18 @@ const TREATMENT_TYPE_KEYS = {
} as const;
const PAGE_SIZE = 20;
const PRIORITY_OPTIONS = [1, 2, 3, 4, 5] as const;
function taskStatusVariant(status: LabTaskStatus): BadgeVariant {
switch (status) {
case 'COMPLETED':
return 'success';
case 'IN_PROGRESS':
return 'default';
default:
return 'warning';
}
}
function formatPatientName(patient: { firstName: string; lastName: string }) {
return `${patient.firstName} ${patient.lastName}`.trim();
@@ -66,6 +81,7 @@ export default function CasesPage() {
const tCommon = useTranslations('common');
const { currentOrganization, user } = useAuth();
const toast = useToast();
const searchParams = useSearchParams();
const [search, setSearch] = useState('');
const [clinicId, setClinicId] = useState('');
@@ -93,7 +109,7 @@ export default function CasesPage() {
const [loadingDetail, setLoadingDetail] = useState(false);
const [updatingTaskId, setUpdatingTaskId] = useState<string | null>(null);
const canEdit = hasPermission(currentOrganization, 'TAB_CASES_EDIT');
const canEdit = canEditCases(currentOrganization);
const locale = user?.language ?? 'en';
const treatmentLabel = useCallback(
@@ -166,6 +182,13 @@ export default function CasesPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only initial fetch
}, []);
useEffect(() => {
const caseIdFromUrl = searchParams.get('caseId');
if (caseIdFromUrl) {
setSelectedCaseId(caseIdFromUrl);
}
}, [searchParams]);
useEffect(() => {
const timeout = setTimeout(() => {
void loadCases({
@@ -201,7 +224,7 @@ export default function CasesPage() {
async function handleTaskUpdate(
taskId: string,
payload: { assigneeUserId?: string | null; status?: LabTaskStatus },
payload: { assigneeUserId?: string | null; priority?: number },
) {
if (!selectedCaseId || !canEdit) return;
@@ -225,8 +248,7 @@ export default function CasesPage() {
}
}
const filterSelectClass =
'w-full rounded-md border border-border bg-background px-3 py-2 text-sm text-text-primary';
const filterSelectClass = `${FORM_SELECT_CLASS} w-full rounded-md px-3 py-2`;
return (
<div className="space-y-4">
@@ -475,24 +497,29 @@ export default function CasesPage() {
{group.tasks.map((task) => (
<li
key={task.id}
className="grid gap-2 sm:grid-cols-[1fr_160px_180px] items-center text-sm rounded bg-background p-2"
className="grid gap-2 sm:grid-cols-[minmax(0,1fr)_auto_88px_180px] items-center text-sm rounded bg-background p-2"
>
<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.status}
value={task.priority}
disabled={!canEdit || updatingTaskId === task.id}
onChange={(e) =>
void handleTaskUpdate(task.id, {
status: e.target.value as LabTaskStatus,
priority: Number(e.target.value),
})
}
className="rounded border border-border bg-surface px-2 py-1 text-sm disabled:opacity-60"
className={FORM_SELECT_CLASS}
aria-label={t('priorityLabel')}
>
{statusOptions.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
{PRIORITY_OPTIONS.map((value) => (
<option key={value} value={value}>
{value}
</option>
))}
</select>
@@ -504,7 +531,7 @@ export default function CasesPage() {
assigneeUserId: e.target.value || null,
})
}
className="rounded border border-border bg-surface px-2 py-1 text-sm disabled:opacity-60"
className={FORM_SELECT_CLASS}
>
<option value="">{t('unassigned')}</option>
{members.map((member) => (