improvement: duedate added for shipped cases. cases and tasks ui and ux updated accordingly.
This commit is contained in:
@@ -416,6 +416,8 @@
|
||||
"selectCaseHint": "Select a case from the list to view tasks.",
|
||||
"fromClinic": "From {name}",
|
||||
"sentAt": "Sent {date}",
|
||||
"dueDateLabel": "Due {date}",
|
||||
"overdueBadge": "Overdue",
|
||||
"taskProgressLabel": "Tasks: {completed} of {total} completed",
|
||||
"taskProgressShort": "{progress} tasks",
|
||||
"treatmentDetails": "Treatment details",
|
||||
@@ -488,6 +490,7 @@
|
||||
"showCompleted": "Show completed",
|
||||
"importantOnly": "Important first",
|
||||
"assignedToMe": "Assigned to me",
|
||||
"overdueOnly": "Overdue cases only",
|
||||
"assignedToStaff": "Assigned to {name}",
|
||||
"resetView": "Reset filters & sort",
|
||||
"showInCase": "Show in case",
|
||||
@@ -500,6 +503,7 @@
|
||||
"filterSentTo": "To",
|
||||
"sortBy": "Sort by",
|
||||
"sortDate": "Date",
|
||||
"sortDueDate": "Case due date",
|
||||
"sortStatus": "Status",
|
||||
"sortClinic": "Clinic",
|
||||
"sortPatient": "Patient",
|
||||
@@ -512,7 +516,10 @@
|
||||
"groupingOffProsthesis": "Sorted by prosthesis type — case grouping is off.",
|
||||
"groupingOffTaskType": "Sorted by task type — case grouping is off.",
|
||||
"groupingOffStatus": "Sorted by status — case grouping is off.",
|
||||
"groupingOffDueDate": "Sorted by due date — case grouping is off.",
|
||||
"caseReceivedAt": "Received {date}",
|
||||
"caseDueDate": "Due {date}",
|
||||
"overdueBadge": "Overdue",
|
||||
"caseTaskProgress": "{completed}/{total} tasks on this page",
|
||||
"clearFilters": "Clear filters",
|
||||
"commentsButton": "Comments",
|
||||
@@ -678,6 +685,10 @@
|
||||
"selectLab": "Destination lab",
|
||||
"selectLabPlaceholder": "Choose a linked lab…",
|
||||
"sendToLab": "Send to lab",
|
||||
"dueDateLabel": "Due date",
|
||||
"dueDateOptional": "optional",
|
||||
"dueDateLockedCompleted": "Due date cannot be changed after all tasks are completed.",
|
||||
"dueDateUpdateError": "Failed to update due date.",
|
||||
"saveLabShipments": "Save lab shipments",
|
||||
"labDispatchSaveHint": "Saves shipment grouping without sending.",
|
||||
"successLabShipmentsSaved": "Lab shipments saved.",
|
||||
|
||||
@@ -416,6 +416,8 @@
|
||||
"selectCaseHint": "برای مشاهده وظایف، یک پرونده از فهرست انتخاب کنید.",
|
||||
"fromClinic": "از {name}",
|
||||
"sentAt": "ارسال {date}",
|
||||
"dueDateLabel": "موعد {date}",
|
||||
"overdueBadge": "عقبافتاده",
|
||||
"taskProgressLabel": "وظایف: {completed} از {total} انجام شده",
|
||||
"taskProgressShort": "{progress} وظیفه",
|
||||
"treatmentDetails": "جزئیات درمان",
|
||||
@@ -489,6 +491,7 @@
|
||||
"showCompleted": "نمایش تکمیلشدهها",
|
||||
"importantOnly": "مهمها در ابتدا",
|
||||
"assignedToMe": "واگذار شده به من",
|
||||
"overdueOnly": "فقط پروندههای عقبافتاده",
|
||||
"assignedToStaff": "واگذار شده به {name}",
|
||||
"resetView": "بازنشانی فیلترها و مرتبسازی",
|
||||
"showInCase": "نمایش در پرونده",
|
||||
@@ -501,6 +504,7 @@
|
||||
"filterSentTo": "تا",
|
||||
"sortBy": "مرتبسازی بر اساس",
|
||||
"sortDate": "تاریخ",
|
||||
"sortDueDate": "موعد پرونده",
|
||||
"sortStatus": "وضعیت",
|
||||
"sortClinic": "کلینیک",
|
||||
"sortPatient": "بیمار",
|
||||
@@ -513,7 +517,10 @@
|
||||
"groupingOffProsthesis": "مرتبسازی بر اساس نوع پروتز — گروهبندی پرونده غیرفعال است.",
|
||||
"groupingOffTaskType": "مرتبسازی بر اساس نوع کار — گروهبندی پرونده غیرفعال است.",
|
||||
"groupingOffStatus": "مرتبسازی بر اساس وضعیت — گروهبندی پرونده غیرفعال است.",
|
||||
"groupingOffDueDate": "مرتبسازی بر اساس موعد — گروهبندی پرونده غیرفعال است.",
|
||||
"caseReceivedAt": "دریافت {date}",
|
||||
"caseDueDate": "موعد {date}",
|
||||
"overdueBadge": "عقبافتاده",
|
||||
"caseTaskProgress": "{completed}/{total} کار در این صفحه",
|
||||
"clearFilters": "پاک کردن فیلترها",
|
||||
"commentsButton": "نظرات",
|
||||
@@ -679,6 +686,10 @@
|
||||
"selectLab": "لابراتوار مقصد",
|
||||
"selectLabPlaceholder": "یک لابراتوار متصل انتخاب کنید…",
|
||||
"sendToLab": "ارسال به لابراتوار",
|
||||
"dueDateLabel": "موعد تحویل",
|
||||
"dueDateOptional": "اختیاری",
|
||||
"dueDateLockedCompleted": "پس از تکمیل همه وظایف، موعد قابل تغییر نیست.",
|
||||
"dueDateUpdateError": "بهروزرسانی موعد ناموفق بود.",
|
||||
"saveLabShipments": "ذخیره محمولههای لاب",
|
||||
"labDispatchSaveHint": "گروهبندی محموله را بدون ارسال ذخیره میکند.",
|
||||
"successLabShipmentsSaved": "محمولههای لاب ذخیره شد.",
|
||||
|
||||
@@ -416,6 +416,8 @@
|
||||
"selectCaseHint": "Selecteer een dossier uit de lijst om taken te bekijken.",
|
||||
"fromClinic": "Van {name}",
|
||||
"sentAt": "Verzonden {date}",
|
||||
"dueDateLabel": "Vervaldatum {date}",
|
||||
"overdueBadge": "Te laat",
|
||||
"taskProgressLabel": "Taken: {completed} van {total} voltooid",
|
||||
"taskProgressShort": "{progress} taken",
|
||||
"treatmentDetails": "Behandeldetails",
|
||||
@@ -489,6 +491,7 @@
|
||||
"showCompleted": "Voltooide tonen",
|
||||
"importantOnly": "Belangrijke cases eerst",
|
||||
"assignedToMe": "Toegewezen aan mij",
|
||||
"overdueOnly": "Alleen te late cases",
|
||||
"assignedToStaff": "Toegewezen aan {name}",
|
||||
"resetView": "Filters en sortering resetten",
|
||||
"showInCase": "In case tonen",
|
||||
@@ -501,6 +504,7 @@
|
||||
"filterSentTo": "Tot",
|
||||
"sortBy": "Sorteren op",
|
||||
"sortDate": "Datum",
|
||||
"sortDueDate": "Vervaldatum case",
|
||||
"sortStatus": "Status",
|
||||
"sortClinic": "Kliniek",
|
||||
"sortPatient": "Patiënt",
|
||||
@@ -513,7 +517,9 @@
|
||||
"groupingOffProsthesis": "Gesorteerd op prothesetype — casagroepering is uit.",
|
||||
"groupingOffTaskType": "Gesorteerd op taaktype — casagroepering is uit.",
|
||||
"groupingOffStatus": "Gesorteerd op status — casagroepering is uit.",
|
||||
"groupingOffDueDate": "Gesorteerd op vervaldatum — casagroepering is uit.",
|
||||
"caseReceivedAt": "Ontvangen {date}",
|
||||
"caseDueDate": "Vervaldatum {date}",
|
||||
"caseTaskProgress": "{completed}/{total} taken op deze pagina",
|
||||
"clearFilters": "Filters wissen",
|
||||
"commentsButton": "Opmerkingen",
|
||||
@@ -679,6 +685,10 @@
|
||||
"selectLab": "Bestemmingslab",
|
||||
"selectLabPlaceholder": "Kies een gekoppeld lab…",
|
||||
"sendToLab": "Versturen naar lab",
|
||||
"dueDateLabel": "Vervaldatum",
|
||||
"dueDateOptional": "optioneel",
|
||||
"dueDateLockedCompleted": "Vervaldatum kan niet worden gewijzigd nadat alle taken zijn voltooid.",
|
||||
"dueDateUpdateError": "Vervaldatum bijwerken mislukt.",
|
||||
"saveLabShipments": "Labzendingen opslaan",
|
||||
"labDispatchSaveHint": "Slaat groepering op zonder te verzenden.",
|
||||
"successLabShipmentsSaved": "Labzendingen opgeslagen.",
|
||||
|
||||
29
frontend/src/components/lab/LabCaseDueDateBadge.tsx
Normal file
29
frontend/src/components/lab/LabCaseDueDateBadge.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
'use client';
|
||||
|
||||
import { Badge } from '@/components/ui/shared/Badge';
|
||||
import {
|
||||
dueDateBadgeVariantFromIso,
|
||||
formatLabCaseDueDate,
|
||||
} from '@/components/lab/labCaseDueDateDisplay';
|
||||
|
||||
interface LabCaseDueDateBadgeProps {
|
||||
dueDate: string | null | undefined;
|
||||
locale: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function LabCaseDueDateBadge({ dueDate, locale, className }: LabCaseDueDateBadgeProps) {
|
||||
const label = formatLabCaseDueDate(dueDate, locale);
|
||||
if (!label) return null;
|
||||
|
||||
return (
|
||||
<Badge
|
||||
variant={dueDateBadgeVariantFromIso(dueDate)}
|
||||
fixedWidth={false}
|
||||
className={className}
|
||||
title={label}
|
||||
>
|
||||
{label}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
46
frontend/src/components/lab/labCaseDueDateDisplay.ts
Normal file
46
frontend/src/components/lab/labCaseDueDateDisplay.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { BadgeVariant } from '@/components/ui/shared/Badge';
|
||||
|
||||
export function toDateInputValue(iso: string | null | undefined): string {
|
||||
if (!iso) return '';
|
||||
const date = new Date(iso);
|
||||
if (Number.isNaN(date.getTime())) return '';
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export function startOfUtcDay(date = new Date()): Date {
|
||||
const d = new Date(date);
|
||||
d.setUTCHours(0, 0, 0, 0);
|
||||
return d;
|
||||
}
|
||||
|
||||
export function formatLabCaseDueDate(
|
||||
iso: string | null | undefined,
|
||||
locale: string,
|
||||
): string | null {
|
||||
if (!iso) return null;
|
||||
const date = new Date(iso);
|
||||
if (Number.isNaN(date.getTime())) return null;
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
/** Whole calendar days from today (UTC) until due date. Negative = overdue. */
|
||||
export function daysUntilDue(iso: string | null | undefined): number | null {
|
||||
if (!iso) return null;
|
||||
const due = new Date(iso);
|
||||
if (Number.isNaN(due.getTime())) return null;
|
||||
const msPerDay = 24 * 60 * 60 * 1000;
|
||||
return Math.round((startOfUtcDay(due).getTime() - startOfUtcDay().getTime()) / msPerDay);
|
||||
}
|
||||
|
||||
/** Green: >7 days · Yellow: 2–7 days · Red: ≤1 day (today, tomorrow, or overdue). */
|
||||
export function dueDateBadgeVariantFromIso(iso: string | null | undefined): BadgeVariant {
|
||||
const days = daysUntilDue(iso);
|
||||
if (days === null) return 'default';
|
||||
if (days <= 1) return 'danger';
|
||||
if (days <= 7) return 'warning';
|
||||
return 'success';
|
||||
}
|
||||
@@ -14,6 +14,8 @@ export type CaseTaskGroup = {
|
||||
clinic: LabTaskListItem['clinic'];
|
||||
patient: LabTaskListItem['patient'];
|
||||
caseSentAt: string | null;
|
||||
caseDueDate: string | null;
|
||||
isCaseOverdue: boolean;
|
||||
isImportant: boolean;
|
||||
prosthesisGroups: ProsthesisTaskGroup[];
|
||||
};
|
||||
@@ -47,6 +49,8 @@ export function groupTasksForDisplay(
|
||||
clinic: task.clinic,
|
||||
patient: task.patient,
|
||||
caseSentAt: task.caseSentAt ?? null,
|
||||
caseDueDate: task.caseDueDate ?? null,
|
||||
isCaseOverdue: task.isCaseOverdue,
|
||||
isImportant: task.isImportant,
|
||||
prosthesisGroups: [],
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ export type TasksViewState = {
|
||||
sortDir: 'asc' | 'desc';
|
||||
importantOnly: boolean;
|
||||
assignedToMe: boolean;
|
||||
overdueOnly: boolean;
|
||||
page: number;
|
||||
highlightTaskId: string | null;
|
||||
};
|
||||
@@ -22,6 +23,7 @@ export const DEFAULT_TASKS_VIEW: TasksViewState = {
|
||||
sortDir: 'desc',
|
||||
importantOnly: false,
|
||||
assignedToMe: false,
|
||||
overdueOnly: false,
|
||||
page: 1,
|
||||
highlightTaskId: null,
|
||||
};
|
||||
@@ -38,6 +40,7 @@ export function isDefaultTasksView(state: TasksViewState): boolean {
|
||||
state.sortDir === DEFAULT_TASKS_VIEW.sortDir &&
|
||||
state.importantOnly === DEFAULT_TASKS_VIEW.importantOnly &&
|
||||
state.assignedToMe === DEFAULT_TASKS_VIEW.assignedToMe &&
|
||||
state.overdueOnly === DEFAULT_TASKS_VIEW.overdueOnly &&
|
||||
state.page === DEFAULT_TASKS_VIEW.page &&
|
||||
state.highlightTaskId === DEFAULT_TASKS_VIEW.highlightTaskId
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ import { FORM_SELECT_CLASS } from '@/components/shared/formSelectStyles';
|
||||
import { LabCaseAttachmentPreview } from '@/components/ui/lab/LabCaseAttachmentPreview';
|
||||
import { LabCaseAttachmentsDialog } from '@/components/ui/lab/LabCaseAttachmentsDialog';
|
||||
import { labTaskStatusVariant } from '@/components/lab/labTaskStatusDisplay';
|
||||
import { LabCaseDueDateBadge } from '@/components/lab/LabCaseDueDateBadge';
|
||||
import {
|
||||
formatToothList,
|
||||
prosthesisTypeBadgeStyleFromCatalog,
|
||||
@@ -104,9 +105,12 @@ export function CaseDetailPanel({
|
||||
<div className="space-y-4">
|
||||
<header className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between border-b border-border pb-3">
|
||||
<div className="min-w-0 flex-1 space-y-1">
|
||||
<h2 className="text-lg font-semibold text-text-primary">
|
||||
{formatPatientName(labCase.patient)}
|
||||
</h2>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<h2 className="text-lg font-semibold text-text-primary">
|
||||
{formatPatientName(labCase.patient)}
|
||||
</h2>
|
||||
<LabCaseDueDateBadge dueDate={labCase.dueDate} locale={locale} />
|
||||
</div>
|
||||
{!canEditImportant && labCase.isImportant ? (
|
||||
<Badge variant="warning" fixedWidth={false} className="mt-1">
|
||||
{t('importantLabel')}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
formatCaseDateTime,
|
||||
formatPatientName,
|
||||
} from '@/components/lab/caseDetailUtils';
|
||||
import { LabCaseDueDateBadge } from '@/components/lab/LabCaseDueDateBadge';
|
||||
import { casesApi } from '@/lib/api/cases';
|
||||
import { tasksApi } from '@/lib/api/tasks';
|
||||
import { treatmentCatalogApi } from '@/lib/api/treatment-catalog';
|
||||
@@ -376,6 +377,11 @@ export function CasesPage() {
|
||||
<div className="font-medium text-text-primary">
|
||||
{formatPatientName(item.patient)}
|
||||
</div>
|
||||
<LabCaseDueDateBadge
|
||||
dueDate={item.dueDate}
|
||||
locale={locale}
|
||||
className="text-[10px]"
|
||||
/>
|
||||
{item.isImportant ? (
|
||||
<Badge variant="warning" fixedWidth={false}>
|
||||
{t('importantLabel')}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Badge } from '@/components/ui/shared/Badge';
|
||||
import { LabCaseDueDateBadge } from '@/components/lab/LabCaseDueDateBadge';
|
||||
import type { CaseTaskGroup } from '@/components/lab/taskListGrouping';
|
||||
import { countCaseTaskProgress } from '@/components/lab/taskListGrouping';
|
||||
|
||||
@@ -34,6 +35,11 @@ export function TaskCaseGroupHeader({ caseGroup, locale }: TaskCaseGroupHeaderPr
|
||||
{t('fromClinic', { name: caseGroup.clinic.name })} ·{' '}
|
||||
{formatPatientName(caseGroup.patient)}
|
||||
</p>
|
||||
<LabCaseDueDateBadge
|
||||
dueDate={caseGroup.caseDueDate}
|
||||
locale={locale}
|
||||
className="text-[10px]"
|
||||
/>
|
||||
{caseGroup.isImportant ? (
|
||||
<Badge variant="warning" fixedWidth={false} className="text-[10px]">
|
||||
{t('importantBadge')}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
labTaskStatusSelectStyle,
|
||||
labTaskStatusVariant,
|
||||
} from '@/components/lab/labTaskStatusDisplay';
|
||||
import { LabCaseDueDateBadge } from '@/components/lab/LabCaseDueDateBadge';
|
||||
import {
|
||||
formatToothList,
|
||||
prosthesisTypeBadgeStyleFromCatalog,
|
||||
@@ -101,6 +102,13 @@ export function TaskRow({
|
||||
{t('importantBadge')}
|
||||
</Badge>
|
||||
) : null}
|
||||
{flatMode && task.caseDueDate && !exiting ? (
|
||||
<LabCaseDueDateBadge
|
||||
dueDate={task.caseDueDate}
|
||||
locale={locale}
|
||||
className="text-[10px]"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
{flatMode ? (
|
||||
<p className="text-[11px] text-text-secondary truncate">
|
||||
|
||||
@@ -68,6 +68,7 @@ export function TasksPage() {
|
||||
const [stepCompleted, setStepCompleted] = useState(DEFAULT_TASKS_VIEW.stepCompleted);
|
||||
const [importantOnly, setImportantOnly] = useState(DEFAULT_TASKS_VIEW.importantOnly);
|
||||
const [assignedToMe, setAssignedToMe] = useState(DEFAULT_TASKS_VIEW.assignedToMe);
|
||||
const [overdueOnly, setOverdueOnly] = useState(DEFAULT_TASKS_VIEW.overdueOnly);
|
||||
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>(
|
||||
@@ -103,8 +104,9 @@ export function TasksPage() {
|
||||
if (stepCompleted) params.stepCompleted = stepCompleted;
|
||||
if (importantOnly) params.pinImportant = true;
|
||||
if (assignedToMe) params.assignedToMe = true;
|
||||
if (overdueOnly) params.overdue = true;
|
||||
return params;
|
||||
}, [page, search, clinicId, statusFilter, stepCompleted, importantOnly, assignedToMe, sortBy, sortDir]);
|
||||
}, [page, search, clinicId, statusFilter, stepCompleted, importantOnly, assignedToMe, overdueOnly, sortBy, sortDir]);
|
||||
|
||||
const displayModel = useMemo(() => groupTasksForDisplay(tasks, sortBy), [tasks, sortBy]);
|
||||
|
||||
@@ -120,6 +122,7 @@ export function TasksPage() {
|
||||
sortDir,
|
||||
importantOnly,
|
||||
assignedToMe,
|
||||
overdueOnly,
|
||||
page,
|
||||
highlightTaskId,
|
||||
}),
|
||||
@@ -132,6 +135,7 @@ export function TasksPage() {
|
||||
sortDir,
|
||||
importantOnly,
|
||||
assignedToMe,
|
||||
overdueOnly,
|
||||
page,
|
||||
highlightTaskId,
|
||||
],
|
||||
@@ -197,6 +201,7 @@ export function TasksPage() {
|
||||
setStepCompleted(DEFAULT_TASKS_VIEW.stepCompleted);
|
||||
setImportantOnly(DEFAULT_TASKS_VIEW.importantOnly);
|
||||
setAssignedToMe(DEFAULT_TASKS_VIEW.assignedToMe);
|
||||
setOverdueOnly(DEFAULT_TASKS_VIEW.overdueOnly);
|
||||
setSortBy(DEFAULT_TASKS_VIEW.sortBy);
|
||||
setSortDir(DEFAULT_TASKS_VIEW.sortDir);
|
||||
setPage(DEFAULT_TASKS_VIEW.page);
|
||||
@@ -213,6 +218,8 @@ export function TasksPage() {
|
||||
setStatusFilter(DEFAULT_TASKS_VIEW.statusFilter);
|
||||
setStepCompleted(DEFAULT_TASKS_VIEW.stepCompleted);
|
||||
setImportantOnly(DEFAULT_TASKS_VIEW.importantOnly);
|
||||
setAssignedToMe(DEFAULT_TASKS_VIEW.assignedToMe);
|
||||
setOverdueOnly(DEFAULT_TASKS_VIEW.overdueOnly);
|
||||
setSortBy(DEFAULT_TASKS_VIEW.sortBy);
|
||||
setSortDir(DEFAULT_TASKS_VIEW.sortDir);
|
||||
setExpandedCommentsTaskId(null);
|
||||
@@ -289,6 +296,8 @@ export function TasksPage() {
|
||||
return 'groupingOffTaskType';
|
||||
case 'status':
|
||||
return 'groupingOffStatus';
|
||||
case 'dueDate':
|
||||
return 'groupingOffDueDate';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -410,6 +419,7 @@ export function TasksPage() {
|
||||
className={`${filterSelectClass} min-w-0 flex-1`}
|
||||
>
|
||||
<option value="date">{t('sortDate')}</option>
|
||||
<option value="dueDate">{t('sortDueDate')}</option>
|
||||
<option value="clinic">{t('sortClinic')}</option>
|
||||
<option value="patient">{t('sortPatient')}</option>
|
||||
<option value="prosthesis">{t('sortProsthesis')}</option>
|
||||
@@ -440,6 +450,12 @@ export function TasksPage() {
|
||||
label={t('assignedToMe')}
|
||||
className="text-xs [&_span:last-child]:text-xs"
|
||||
/>
|
||||
<Checkbox
|
||||
checked={overdueOnly}
|
||||
onChange={(checked) => applyFilterChange(() => setOverdueOnly(checked))}
|
||||
label={t('overdueOnly')}
|
||||
className="text-xs [&_span:last-child]:text-xs"
|
||||
/>
|
||||
{showReset ? (
|
||||
<Button type="button" variant="ghost" size="sm" onClick={resetView}>
|
||||
{t('resetView')}
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useEffect, useState } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Button } from '@/components/ui/shared/Button';
|
||||
import { Checkbox } from '@/components/ui/shared/Checkbox';
|
||||
import { isDetailReadyForLabDispatch } from '@/components/treatment/treatmentDetailRules';
|
||||
import { isDetailReadyForLabDispatch, isLabCaseCompleted } from '@/components/treatment/treatmentDetailRules';
|
||||
import { toDateInputValue } from '@/components/lab/labCaseDueDateDisplay';
|
||||
import { FORM_SELECT_CLASS } from '@/components/shared/formSelectStyles';
|
||||
import { LinkedOrganizationSearchCombobox } from '@/components/ui/treatment/LinkedOrganizationSearchCombobox';
|
||||
import { CaseSentLabel } from '@/components/ui/treatment/CaseSentLabel';
|
||||
@@ -221,6 +222,73 @@ export function LabCasesDispatchPanel({
|
||||
setApplyAllProsthesis('');
|
||||
}
|
||||
|
||||
const caseFullyComplete = isLabCaseCompleted(activeLabCase?.taskProgress);
|
||||
const canEditDueDate = canEdit && !disabled && (!sent || !caseFullyComplete);
|
||||
|
||||
async function handleSentDueDateBlur(nextValue: string) {
|
||||
if (!activeLabCase?.id || !sent || !canEditDueDate) return;
|
||||
const dueDate = nextValue || null;
|
||||
if (dueDate === (activeLabCase.dueDate?.slice(0, 10) ?? null)) return;
|
||||
try {
|
||||
const response = await treatmentsApi.updateLabCaseDueDate(activeLabCase.id, dueDate);
|
||||
updateActiveLabCase({
|
||||
dueDate: response.data.dueDate,
|
||||
taskProgress: response.data.taskProgress ?? activeLabCase.taskProgress,
|
||||
});
|
||||
} catch (error) {
|
||||
onCommentError?.(error instanceof Error ? error.message : t('dueDateUpdateError'));
|
||||
}
|
||||
}
|
||||
|
||||
function renderShipmentCardHeader() {
|
||||
return (
|
||||
<div className="flex flex-col gap-3 border-b border-border/60 pb-3 sm:flex-row sm:items-start sm:justify-between">
|
||||
<p className="text-xs font-semibold text-text-primary">{t('labShipmentIncludedDetails')}</p>
|
||||
{renderDueDateField()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDueDateField() {
|
||||
if (!activeLabCase) return null;
|
||||
const inputValue = toDateInputValue(activeLabCase.dueDate);
|
||||
|
||||
return (
|
||||
<label className="block shrink-0 sm:max-w-[11rem] sm:text-end">
|
||||
<span className="block text-xs font-medium text-text-secondary sm:text-end">
|
||||
{t('dueDateLabel')}{' '}
|
||||
<span className="font-normal text-text-muted">({t('dueDateOptional')})</span>
|
||||
</span>
|
||||
<input
|
||||
type="date"
|
||||
value={inputValue}
|
||||
disabled={!canEditDueDate}
|
||||
onChange={(e) => {
|
||||
if (!sent) {
|
||||
updateActiveLabCase({ dueDate: e.target.value || null });
|
||||
}
|
||||
}}
|
||||
onBlur={(e) => {
|
||||
if (sent) void handleSentDueDateBlur(e.target.value);
|
||||
}}
|
||||
className={`${FORM_SELECT_CLASS} mt-2 w-full rounded-md px-2 py-1.5 text-sm`}
|
||||
/>
|
||||
{sent && caseFullyComplete && activeLabCase.dueDate ? (
|
||||
<p className="mt-1.5 text-[11px] text-text-muted sm:text-end">{t('dueDateLockedCompleted')}</p>
|
||||
) : null}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function renderIncludedDetailSummary() {
|
||||
if (!activeDetail) return null;
|
||||
return (
|
||||
<p className="text-sm text-text-primary rounded-[var(--radius-sm)] border border-border/50 bg-background-secondary/50 px-3 py-2">
|
||||
{detailSummary(activeDetail)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const activeDetailAttachments = activeDetail.attachmentMetas ?? [];
|
||||
|
||||
return (
|
||||
@@ -248,16 +316,10 @@ export function LabCasesDispatchPanel({
|
||||
<p className="text-xs text-text-muted">{t('labDispatchEmpty')}</p>
|
||||
) : activeLabCase ? (
|
||||
<div className="space-y-4 border border-border/60 rounded-[var(--radius-md)] p-4 bg-background-secondary/30">
|
||||
{renderShipmentCardHeader()}
|
||||
{sent ? (
|
||||
<>
|
||||
<div>
|
||||
<p className="text-xs font-medium text-text-secondary mb-2">
|
||||
{t('labShipmentIncludedDetails')}
|
||||
</p>
|
||||
<p className="text-sm text-text-primary rounded-[var(--radius-sm)] border border-border/50 bg-background-secondary/50 px-3 py-2">
|
||||
{detailSummary(activeDetail)}
|
||||
</p>
|
||||
</div>
|
||||
{renderIncludedDetailSummary()}
|
||||
|
||||
{activeLabCase.id ? (
|
||||
<LabCaseCommentsPanel
|
||||
@@ -296,14 +358,7 @@ export function LabCasesDispatchPanel({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div>
|
||||
<p className="text-xs font-medium text-text-secondary mb-2">
|
||||
{t('labShipmentIncludedDetails')}
|
||||
</p>
|
||||
<p className="text-sm text-text-primary rounded-[var(--radius-sm)] border border-border/50 bg-background-secondary/50 px-3 py-2">
|
||||
{detailSummary(activeDetail)}
|
||||
</p>
|
||||
</div>
|
||||
{renderIncludedDetailSummary()}
|
||||
|
||||
{!sent && activeDetailAttachments.length > 0 ? (
|
||||
<div>
|
||||
|
||||
@@ -203,6 +203,8 @@ function mapLabCaseDraftFromApi(lc: PastLabCase): LabCaseDraft {
|
||||
attachmentIds: (lc.attachments ?? []).map((a) => a.id),
|
||||
sentAt: lc.sentAt ?? null,
|
||||
sends: lc.sends ?? [],
|
||||
dueDate: lc.dueDate ?? null,
|
||||
taskProgress: lc.taskProgress ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -990,6 +992,7 @@ export function TreatmentWorkspace({
|
||||
row !== null,
|
||||
),
|
||||
attachmentIds: lc.attachmentIds,
|
||||
dueDate: lc.dueDate ?? null,
|
||||
};
|
||||
})
|
||||
.filter((row): row is NonNullable<typeof row> => row !== null);
|
||||
|
||||
@@ -83,6 +83,16 @@ export const treatmentsApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
updateLabCaseDueDate: async (
|
||||
labCaseId: string,
|
||||
dueDate: string | null,
|
||||
): Promise<{ success: boolean; data: LabCaseResponse }> => {
|
||||
const response = await apiClient.patch(`/treatments/lab-cases/${labCaseId}/due-date`, {
|
||||
dueDate,
|
||||
});
|
||||
return response.data;
|
||||
},
|
||||
|
||||
listLabCaseComments: async (
|
||||
labCaseId: string,
|
||||
): Promise<{ success: boolean; data: LabCaseComment[] }> => {
|
||||
|
||||
@@ -3,6 +3,8 @@ export type LabTaskStatus = 'IN_PROGRESS' | 'COMPLETED';
|
||||
export interface LabCaseListItem {
|
||||
id: string;
|
||||
sentAt: string | null;
|
||||
dueDate: string | null;
|
||||
isOverdue: boolean;
|
||||
isImportant: boolean;
|
||||
clinic: { id: string; name: string };
|
||||
patient: {
|
||||
@@ -79,6 +81,8 @@ export interface LabCaseAttachmentMeta {
|
||||
export interface LabCaseDetail {
|
||||
id: string;
|
||||
sentAt: string | null;
|
||||
dueDate: string | null;
|
||||
isOverdue: boolean;
|
||||
isImportant: boolean;
|
||||
clinic: { id: string; name: string };
|
||||
patient: {
|
||||
@@ -143,7 +147,8 @@ export type TaskSortField =
|
||||
| 'patient'
|
||||
| 'important'
|
||||
| 'prosthesis'
|
||||
| 'taskType';
|
||||
| 'taskType'
|
||||
| 'dueDate';
|
||||
|
||||
export interface ListLabTasksParams {
|
||||
q?: string;
|
||||
@@ -153,6 +158,7 @@ export interface ListLabTasksParams {
|
||||
important?: boolean;
|
||||
pinImportant?: boolean;
|
||||
assignedToMe?: boolean;
|
||||
overdue?: boolean;
|
||||
sentFrom?: string;
|
||||
sentTo?: string;
|
||||
stepCompleted?: string;
|
||||
@@ -196,6 +202,8 @@ export interface LabTaskListItem {
|
||||
stepLabel: string;
|
||||
status: LabTaskStatus;
|
||||
isImportant: boolean;
|
||||
caseDueDate: string | null;
|
||||
isCaseOverdue: boolean;
|
||||
assignee: LabTaskUser | null;
|
||||
assignedAt: string | null;
|
||||
lastStatusChangedAt: string | null;
|
||||
|
||||
@@ -109,6 +109,8 @@ export interface PastLabCase {
|
||||
prosthesisTypeCode: string;
|
||||
}>;
|
||||
sends?: LabCaseSendInfo[];
|
||||
dueDate?: string | null;
|
||||
taskProgress?: LabCaseTaskProgress | null;
|
||||
attachments?: TreatmentAttachmentMeta[];
|
||||
}
|
||||
|
||||
@@ -155,6 +157,8 @@ export interface LabCaseDraft {
|
||||
attachmentIds: string[];
|
||||
sentAt?: string | null;
|
||||
sends?: LabCaseSendInfo[];
|
||||
dueDate?: string | null;
|
||||
taskProgress?: LabCaseTaskProgress | null;
|
||||
}
|
||||
|
||||
export type SavedTreatmentDetailPayload = {
|
||||
@@ -180,6 +184,7 @@ export interface SaveLabCasePayload {
|
||||
prosthesisTypeCode: string;
|
||||
}>;
|
||||
attachmentIds?: string[];
|
||||
dueDate?: string | null;
|
||||
}
|
||||
|
||||
export interface SaveTreatmentPayload {
|
||||
@@ -201,6 +206,8 @@ export interface LabCaseResponse {
|
||||
teeth: string[];
|
||||
} | null;
|
||||
sends: LabCaseSendInfo[];
|
||||
dueDate: string | null;
|
||||
taskProgress?: LabCaseTaskProgress | null;
|
||||
toothProsthesis?: LabCaseToothProsthesisDraft[];
|
||||
attachments?: TreatmentAttachmentMeta[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user