diff --git a/.cursor/rules/lab-tasks.mdc b/.cursor/rules/lab-tasks.mdc
index feb420b..af5e113 100644
--- a/.cursor/rules/lab-tasks.mdc
+++ b/.cursor/rules/lab-tasks.mdc
@@ -1,6 +1,6 @@
---
description: Lab Tasks tab — sort, grouping, filters, prosthesis colors
-globs: frontend/src/components/ui/lab/TasksPage.tsx,frontend/src/components/ui/lab/Task*.tsx,frontend/src/components/lab/taskListGrouping.ts,frontend/src/components/treatment/prosthesisTypeDisplay.ts,frontend/src/components/shared/catalog-type-colors.ts,backend/src/modules/tasks/**
+globs: frontend/src/components/ui/lab/TasksPage.tsx,frontend/src/components/ui/lab/Task*.tsx,frontend/src/components/ui/lab/Case*.tsx,frontend/src/components/ui/treatment/LabCasesDispatchPanel.tsx,frontend/src/components/lab/taskListGrouping.ts,frontend/src/components/lab/labCaseDueDateDisplay.ts,frontend/src/components/treatment/prosthesisTypeDisplay.ts,frontend/src/components/shared/catalog-type-colors.ts,backend/src/modules/tasks/**,backend/src/common/lab-case-due-date.ts,backend/src/modules/treatments/**
alwaysApply: false
---
@@ -11,6 +11,7 @@ alwaysApply: false
- **Prosthesis colors:** `PROSTHESIS_TYPE_COLORS` + `prosthesisTypeBadgeStyleFromCatalog` — never row index.
- **Important first:** `pinImportant=true` (sort pin, not filter).
- **Task assignment:** assign in Cases (`TAB_CASES_EDIT`); status edit on Tasks only for assignee or unassigned tasks; others see “Assigned to {name}”.
+- **Case due dates:** clinic sets in Treatment lab dispatch; lab sees on Cases/Tasks; `overdue` filter + `sortBy=dueDate` on Tasks.
- **Show in case:** `GET /tasks/locate-page` finds page in full list; highlight + scroll.
Full map: `.cursor/skills/lab-tasks/SKILL.md`
diff --git a/.cursor/skills/lab-tasks/SKILL.md b/.cursor/skills/lab-tasks/SKILL.md
index 9cbd83f..7908bb3 100644
--- a/.cursor/skills/lab-tasks/SKILL.md
+++ b/.cursor/skills/lab-tasks/SKILL.md
@@ -40,6 +40,8 @@ Components: `TaskCaseGroupHeader`, `TaskProsthesisGroupHeader`, `TaskRow`.
| `stepCompleted` | `GET /tasks` | Workflow step dropdown |
| `pinImportant` | `GET /tasks` | Important first (sort pin) |
| `assignedToMe` | `GET /tasks` | Only tasks assigned to current user |
+| `overdue` | `GET /tasks` | Cases with due date before today and at least one in-progress task |
+| `sortBy=dueDate` | `GET /tasks` | Sort by `LabCase.dueDate` (flat list; grouping off) |
| Clinics + steps options | `GET /tasks/filter-options` | Populates dropdowns (not from current page) |
**Task assignment:** Managed in **Cases** (`TAB_CASES_EDIT`), not on Tasks tab. `PATCH /cases/:caseId/tasks/:taskId/assign`; assignable staff via `GET /cases/assignable-staff` (members with `TAB_TASKS_EDIT`, including participating owner). Case detail task row: step label, status badge, assign dropdown, and last-updated line on one compact row.
@@ -50,6 +52,8 @@ Components: `TaskCaseGroupHeader`, `TaskProsthesisGroupHeader`, `TaskRow`.
- **Important first:** `pinImportant=true` prepends important cases in sort order.
- **Assigned to me:** `assignedToMe=true` filters to current user's assigned tasks only.
+- **Overdue cases:** `overdue=true` — `LabCase.dueDate` before start of UTC day **and** at least one task still `IN_PROGRESS`. Shown with error badge on Cases list/detail and task case headers.
+- **Sort by due date:** `sortBy=dueDate` — flat list (grouping off); tiebreakers match other non-date sorts.
- **Reset view:** `resetView` restores `DEFAULT_TASKS_VIEW` from `tasksViewDefaults.ts`.
- **Show in case:** flat-sort rows only; resets filters/sort, calls `GET /tasks/locate-page` to find the correct page in the full default-sorted list, then highlights + scrolls to the task.
- **Complete animation:** when marking done under in-progress filter, row plays exit animation + success toast before refetch.
@@ -63,7 +67,15 @@ Components: `TaskCaseGroupHeader`, `TaskProsthesisGroupHeader`, `TaskRow`.
- `GET /cases/assignable-staff` — staff eligible for task assignment
- `PATCH /cases/:caseId/tasks/:taskId/assign` — assign or unassign (`assigneeUserId` nullable)
-List items include `caseSentAt`, `assignee`, `assignedAt` for case headers / flat rows.
+List items include `caseSentAt`, `caseDueDate`, `isCaseOverdue`, `assignee`, `assignedAt` for case headers / flat rows.
+
+## Case due dates (clinic → lab)
+
+- **Schema:** `LabCase.dueDate` (optional `DateTime`).
+- **Clinic set:** Treatment lab dispatch panel — date input on unsent shipment (saved with draft/send); on sent cases, blur saves via `PATCH /treatments/lab-cases/:labCaseId/due-date`.
+- **Edit lock:** Clinic cannot change due date after **all** tasks are completed (`taskProgress.completed === taskProgress.total`).
+- **Lab display:** Cases list + detail; Tasks case group header when grouped by date.
+- **Utils:** `backend/src/common/lab-case-due-date.ts`, `frontend/src/components/lab/labCaseDueDateDisplay.ts`.
## Permissions
diff --git a/AGENTS.md b/AGENTS.md
index b800cb8..d96b183 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -45,7 +45,7 @@ frontend/src/
- **History filters** are client-side only (`treatmentHistoryFilters.ts`): “Not shipped to lab” + single date on already-fetched patient history; includes live current draft when filtering.
- **Lab case comments** on a detail when sent and lab case tasks are not all `COMPLETED` (`taskProgress` from API).
-**Lab Tasks tab:** Newest case first; steps ordered 1→N; case grouping when sorted by date; `stepCompleted` filter; prosthesis colors from `PROSTHESIS_TYPE_COLORS` via catalog; task assignment in **Cases** (compact row: status + assignee + last update); on **Tasks**, all staff see every task but only assignee (or unassigned pool) can change status — others see “Assigned to {name}” instead of the status dropdown — see `.cursor/skills/lab-tasks/SKILL.md`.
+**Lab Tasks tab:** Newest case first; steps ordered 1→N; case grouping when sorted by date; `stepCompleted` filter; prosthesis colors from `PROSTHESIS_TYPE_COLORS` via catalog; task assignment in **Cases** (compact row: status + assignee + last update); on **Tasks**, all staff see every task but only assignee (or unassigned pool) can change status — others see “Assigned to {name}” instead of the status dropdown; **case due dates** set/edited in clinic Treatment lab dispatch, shown on lab Cases/Tasks with overdue filter + sort — see `.cursor/skills/lab-tasks/SKILL.md`.
## Backend layout
diff --git a/backend/prisma/migrations/20260713140000_lab_case_due_date/migration.sql b/backend/prisma/migrations/20260713140000_lab_case_due_date/migration.sql
new file mode 100644
index 0000000..7e7a24b
--- /dev/null
+++ b/backend/prisma/migrations/20260713140000_lab_case_due_date/migration.sql
@@ -0,0 +1,5 @@
+-- Expected due date for lab cases (set by clinic at send / editable until complete)
+
+ALTER TABLE "lab_cases" ADD COLUMN "dueDate" TIMESTAMP(3);
+
+CREATE INDEX "lab_cases_dueDate_idx" ON "lab_cases"("dueDate");
diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma
index e503da2..6191431 100644
--- a/backend/prisma/schema.prisma
+++ b/backend/prisma/schema.prisma
@@ -214,6 +214,7 @@ model LabCase {
sortOrder Int
destinationOrganizationId String?
sentAt DateTime?
+ dueDate DateTime?
isImportant Boolean @default(false)
treatment Treatment @relation(fields: [treatmentId], references: [id], onDelete: Cascade)
diff --git a/backend/src/common/lab-case-due-date.ts b/backend/src/common/lab-case-due-date.ts
new file mode 100644
index 0000000..5400b16
--- /dev/null
+++ b/backend/src/common/lab-case-due-date.ts
@@ -0,0 +1,42 @@
+import { LabTaskStatus } from '@prisma/client';
+
+/** Parse YYYY-MM-DD (or ISO) into UTC midnight for that calendar day. */
+export function parseDueDateInput(value?: string | null): Date | null {
+ if (value === undefined || value === null || value === '') {
+ return null;
+ }
+ const trimmed = value.trim();
+ if (!trimmed) return null;
+
+ const dateOnly = /^\d{4}-\d{2}-\d{2}$/.test(trimmed);
+ const parsed = dateOnly ? new Date(`${trimmed}T00:00:00.000Z`) : new Date(trimmed);
+ if (Number.isNaN(parsed.getTime())) {
+ throw new Error('Invalid due date');
+ }
+ if (dateOnly) {
+ return parsed;
+ }
+ const normalized = new Date(parsed);
+ normalized.setUTCHours(0, 0, 0, 0);
+ return normalized;
+}
+
+export function startOfUtcDay(date = new Date()): Date {
+ const d = new Date(date);
+ d.setUTCHours(0, 0, 0, 0);
+ return d;
+}
+
+export function isLabCaseOverdue(
+ dueDate: Date | null | undefined,
+ tasks: Array<{ status: LabTaskStatus }>,
+): boolean {
+ if (!dueDate) return false;
+ const hasInProgress = tasks.some((task) => task.status === LabTaskStatus.IN_PROGRESS);
+ if (!hasInProgress) return false;
+ return dueDate < startOfUtcDay();
+}
+
+export function isLabCaseFullyCompleted(tasks: Array<{ status: LabTaskStatus }>): boolean {
+ return tasks.length > 0 && tasks.every((task) => task.status === LabTaskStatus.COMPLETED);
+}
diff --git a/backend/src/modules/cases/cases.service.ts b/backend/src/modules/cases/cases.service.ts
index 072c97a..4a1325a 100644
--- a/backend/src/modules/cases/cases.service.ts
+++ b/backend/src/modules/cases/cases.service.ts
@@ -15,6 +15,9 @@ import {
import { TreatmentCatalogService } from '../treatment-catalog/treatment-catalog.service';
import { normalizeTeeth } from '../treatments/treatment.utils';
import { ListLabCasesDto, UpdateLabCaseImportantDto, AssignLabCaseTaskDto } from './dto/cases.dto';
+import {
+ isLabCaseOverdue,
+} from '../../common/lab-case-due-date';
import { normalizeTaskTeeth } from './lab-case-task.util';
import { hasEffectivePermission } from '../../common/membership-permissions';
@@ -534,6 +537,7 @@ export class CasesService {
private mapLabCaseListItem(lc: {
id: string;
sentAt: Date | null;
+ dueDate: Date | null;
isImportant: boolean;
treatment: {
organization: { id: string; name: string };
@@ -548,6 +552,8 @@ export class CasesService {
return {
id: lc.id,
sentAt: lc.sentAt?.toISOString() ?? null,
+ dueDate: lc.dueDate?.toISOString() ?? null,
+ isOverdue: isLabCaseOverdue(lc.dueDate, lc.tasks),
isImportant: lc.isImportant,
clinic: lc.treatment.organization,
patient: {
@@ -582,6 +588,8 @@ export class CasesService {
return {
id: lc.id,
sentAt: lc.sentAt?.toISOString() ?? null,
+ dueDate: lc.dueDate?.toISOString() ?? null,
+ isOverdue: isLabCaseOverdue(lc.dueDate, lc.tasks),
isImportant: lc.isImportant,
clinic: lc.treatment.organization,
patient: lc.treatment.patient,
diff --git a/backend/src/modules/tasks/dto/tasks.dto.ts b/backend/src/modules/tasks/dto/tasks.dto.ts
index dcffd93..1ce3e0e 100644
--- a/backend/src/modules/tasks/dto/tasks.dto.ts
+++ b/backend/src/modules/tasks/dto/tasks.dto.ts
@@ -32,7 +32,8 @@ export type TaskSortField =
| 'patient'
| 'important'
| 'prosthesis'
- | 'taskType';
+ | 'taskType'
+ | 'dueDate';
export class ListLabTasksDto {
@IsOptional()
@@ -87,8 +88,14 @@ export class ListLabTasksDto {
@IsBoolean()
assignedToMe?: boolean;
+ /** Past due date with at least one in-progress task on the case. */
@IsOptional()
- @IsIn(['date', 'status', 'clinic', 'patient', 'important', 'prosthesis', 'taskType'])
+ @Transform(toBoolean)
+ @IsBoolean()
+ overdue?: boolean;
+
+ @IsOptional()
+ @IsIn(['date', 'status', 'clinic', 'patient', 'important', 'prosthesis', 'taskType', 'dueDate'])
sortBy?: TaskSortField;
@IsOptional()
@@ -159,8 +166,14 @@ export class LocateTaskPageDto {
@IsBoolean()
assignedToMe?: boolean;
+ /** Past due date with at least one in-progress task on the case. */
@IsOptional()
- @IsIn(['date', 'status', 'clinic', 'patient', 'important', 'prosthesis', 'taskType'])
+ @Transform(toBoolean)
+ @IsBoolean()
+ overdue?: boolean;
+
+ @IsOptional()
+ @IsIn(['date', 'status', 'clinic', 'patient', 'important', 'prosthesis', 'taskType', 'dueDate'])
sortBy?: TaskSortField;
@IsOptional()
diff --git a/backend/src/modules/tasks/tasks.service.ts b/backend/src/modules/tasks/tasks.service.ts
index 0460720..454ea08 100644
--- a/backend/src/modules/tasks/tasks.service.ts
+++ b/backend/src/modules/tasks/tasks.service.ts
@@ -12,6 +12,7 @@ import {
normalizeCatalogLocale,
} from '../catalog/catalog-label.service';
import { normalizeTaskTeeth } from '../cases/lab-case-task.util';
+import { isLabCaseOverdue, startOfUtcDay } from '../../common/lab-case-due-date';
import { ListLabTasksDto, LocateTaskPageDto, UpdateLabTaskDto } from './dto/tasks.dto';
import { hasEffectivePermission } from '../../common/membership-permissions';
@@ -20,6 +21,7 @@ const taskListInclude = {
assignee: { select: { id: true, name: true } },
labCase: {
include: {
+ tasks: { select: { status: true } },
treatment: {
include: {
organization: { select: { id: true, name: true } },
@@ -310,7 +312,15 @@ export class TasksService {
const base: Prisma.LabCaseTaskWhereInput = {
...(query.labCaseId ? { labCaseId: query.labCaseId } : {}),
- labCase: labCaseScope,
+ labCase: {
+ ...labCaseScope,
+ ...(query.overdue
+ ? {
+ dueDate: { not: null, lt: startOfUtcDay() },
+ tasks: { some: { status: LabTaskStatus.IN_PROGRESS } },
+ }
+ : {}),
+ },
...(status !== undefined ? { status } : {}),
...(query.assignedToMe ? { assigneeUserId: actorUserId } : {}),
};
@@ -360,6 +370,7 @@ export class TasksService {
sentTo: query.sentTo,
stepCompleted: query.stepCompleted,
assignedToMe: query.assignedToMe,
+ overdue: query.overdue,
sortBy: query.sortBy,
sortDir: query.sortDir,
limit: query.limit,
@@ -501,6 +512,17 @@ export class TasksService {
...stepTiebreakers,
];
break;
+ case 'dueDate':
+ orderBy = [
+ { labCase: { dueDate: dir } },
+ { labCase: { sentAt: 'desc' } },
+ { labCaseId: 'asc' },
+ { treatmentDetailId: 'asc' },
+ { prosthesisTypeCode: 'asc' },
+ { stepOrder: 'asc' },
+ { id: 'asc' },
+ ];
+ break;
case 'date':
default:
orderBy = [
@@ -539,6 +561,8 @@ export class TasksService {
stepLabel: task.stepLabel,
status: task.status,
isImportant: task.labCase.isImportant,
+ caseDueDate: task.labCase.dueDate?.toISOString() ?? null,
+ isCaseOverdue: isLabCaseOverdue(task.labCase.dueDate, task.labCase.tasks),
assignee: task.assignee
? { id: task.assignee.id, name: task.assignee.name }
: null,
diff --git a/backend/src/modules/treatments/dto/treatment.dto.ts b/backend/src/modules/treatments/dto/treatment.dto.ts
index a2fa750..f8a0e97 100644
--- a/backend/src/modules/treatments/dto/treatment.dto.ts
+++ b/backend/src/modules/treatments/dto/treatment.dto.ts
@@ -1,6 +1,7 @@
import {
ArrayMinSize,
IsArray,
+ IsDateString,
IsOptional,
IsString,
IsUUID,
@@ -84,6 +85,16 @@ export class SaveLabCaseDto {
@IsArray()
@IsUUID(undefined, { each: true })
attachmentIds?: string[];
+
+ @IsOptional()
+ @IsDateString()
+ dueDate?: string | null;
+}
+
+export class UpdateLabCaseDueDateDto {
+ @IsOptional()
+ @IsDateString()
+ dueDate?: string | null;
}
export class SaveTreatmentLabCasesDto {
diff --git a/backend/src/modules/treatments/treatments.controller.ts b/backend/src/modules/treatments/treatments.controller.ts
index 59c7e54..991ca1c 100644
--- a/backend/src/modules/treatments/treatments.controller.ts
+++ b/backend/src/modules/treatments/treatments.controller.ts
@@ -4,6 +4,7 @@ import {
Get,
Param,
ParseIntPipe,
+ Patch,
Post,
Put,
Query,
@@ -22,6 +23,7 @@ import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import {
SaveTreatmentDraftDto,
SaveTreatmentLabCasesDto,
+ UpdateLabCaseDueDateDto,
} from './dto/treatment.dto';
import { CreateLabCaseCommentDto } from '../lab-case-comments/dto/lab-case-comment.dto';
import { LabCaseCommentsService } from '../lab-case-comments/lab-case-comments.service';
@@ -200,6 +202,22 @@ export class TreatmentsController {
);
}
+ @Patch('lab-cases/:labCaseId/due-date')
+ @ApiOperation({ summary: 'Update expected due date for a sent lab case' })
+ updateLabCaseDueDate(
+ @Param('labCaseId') labCaseId: string,
+ @Body() dto: UpdateLabCaseDueDateDto,
+ @Req() req: { user: { id: string; organizationId?: string } },
+ ) {
+ const organizationId = this.treatmentsService.getOrganizationIdFromUser(req.user);
+ return this.treatmentsService.updateLabCaseDueDate(
+ labCaseId,
+ dto,
+ organizationId,
+ req.user.id,
+ );
+ }
+
@Get('lab-cases/:labCaseId/comments')
@ApiOperation({ summary: 'List comments for a lab case during treatment dispatch' })
listLabCaseComments(
diff --git a/backend/src/modules/treatments/treatments.service.ts b/backend/src/modules/treatments/treatments.service.ts
index 8f11505..14372c0 100644
--- a/backend/src/modules/treatments/treatments.service.ts
+++ b/backend/src/modules/treatments/treatments.service.ts
@@ -4,7 +4,7 @@ import {
Injectable,
NotFoundException,
} from '@nestjs/common';
-import { LabTaskStatus, LinkStatus } from '@prisma/client';
+import { LabTaskStatus, LinkStatus, Prisma } from '@prisma/client';
import { createReadStream, existsSync, mkdirSync } from 'fs';
import { join } from 'path';
import { randomUUID } from 'crypto';
@@ -15,7 +15,12 @@ import { TreatmentCatalogService } from '../treatment-catalog/treatment-catalog.
import {
SaveTreatmentDraftDto,
SaveTreatmentLabCasesDto,
+ UpdateLabCaseDueDateDto,
} from './dto/treatment.dto';
+import {
+ isLabCaseFullyCompleted,
+ parseDueDateInput,
+} from '../../common/lab-case-due-date';
import {
generateTreatmentTitle,
normalizeTeeth,
@@ -408,9 +413,15 @@ export class TreatmentsService {
for (const [index, lc] of dto.labCases.entries()) {
if (lc.id && sentLabCaseIds.has(lc.id)) {
+ if (lc.dueDate !== undefined) {
+ await this.updateLabCaseDueDateInTx(tx, lc.id, organizationId, lc.dueDate);
+ }
continue;
}
+ const dueDate =
+ lc.dueDate !== undefined ? parseDueDateInput(lc.dueDate) : undefined;
+
const row = lc.id
? await tx.labCase.update({
where: { id: lc.id },
@@ -418,6 +429,7 @@ export class TreatmentsService {
clientKey: lc.clientId,
sortOrder: index,
destinationOrganizationId: lc.destinationOrganizationId ?? null,
+ ...(dueDate !== undefined ? { dueDate } : {}),
},
})
: await tx.labCase.create({
@@ -426,6 +438,7 @@ export class TreatmentsService {
clientKey: lc.clientId,
sortOrder: index,
destinationOrganizationId: lc.destinationOrganizationId ?? null,
+ ...(dueDate !== undefined ? { dueDate } : {}),
},
});
@@ -586,6 +599,88 @@ export class TreatmentsService {
return { success: true, data: this.mapLabCase(refreshed) };
}
+ async updateLabCaseDueDate(
+ labCaseId: string,
+ dto: UpdateLabCaseDueDateDto,
+ organizationId: string,
+ actorUserId: string,
+ ) {
+ await this.assertCanEditTreatment(actorUserId, organizationId);
+
+ const updated = await this.prisma.$transaction(async (tx) => {
+ await this.updateLabCaseDueDateInTx(tx, labCaseId, organizationId, dto.dueDate ?? null);
+ return tx.labCase.findFirstOrThrow({
+ where: { id: labCaseId },
+ include: {
+ details: {
+ include: {
+ detail: {
+ select: { id: true, clientKey: true, treatmentType: true, teeth: true },
+ },
+ },
+ },
+ sends: {
+ orderBy: [{ sentAt: 'asc' }],
+ include: { organization: { select: { id: true, name: true } } },
+ },
+ toothProsthesis: true,
+ tasks: { select: { id: true, status: true } },
+ attachments: {
+ include: {
+ attachment: {
+ select: {
+ id: true,
+ fileName: true,
+ mimeType: true,
+ sizeBytes: true,
+ createdAt: true,
+ },
+ },
+ },
+ },
+ },
+ });
+ });
+
+ return { success: true, data: this.mapLabCase(updated) };
+ }
+
+ private async updateLabCaseDueDateInTx(
+ tx: Prisma.TransactionClient,
+ labCaseId: string,
+ organizationId: string,
+ dueDateInput?: string | null,
+ ) {
+ const labCase = await tx.labCase.findFirst({
+ where: {
+ id: labCaseId,
+ treatment: { organizationId },
+ sentAt: { not: null },
+ },
+ include: { tasks: { select: { status: true } } },
+ });
+
+ if (!labCase) {
+ throw new NotFoundException('Lab case not found');
+ }
+
+ if (isLabCaseFullyCompleted(labCase.tasks)) {
+ throw new BadRequestException('Due date cannot be changed after all tasks are completed');
+ }
+
+ let dueDate: Date | null;
+ try {
+ dueDate = parseDueDateInput(dueDateInput ?? null);
+ } catch {
+ throw new BadRequestException('Invalid due date');
+ }
+
+ await tx.labCase.update({
+ where: { id: labCaseId },
+ data: { dueDate },
+ });
+ }
+
async uploadDetailAttachments(
appointmentId: string,
detailClientKey: string,
@@ -804,6 +899,7 @@ export class TreatmentsService {
sortOrder?: number;
destinationOrganizationId?: string | null;
sentAt?: Date | null;
+ dueDate?: Date | null;
details?: Array<{
treatmentDetailId: string;
detail?: { id: string; clientKey: string | null; treatmentType: string; teeth: unknown };
@@ -827,12 +923,16 @@ export class TreatmentsService {
createdAt: Date;
};
}>;
+ tasks?: Array<{ id: string; status: LabTaskStatus }>;
}) {
+ const taskProgress = this.mapTaskProgress(lc.tasks ?? []);
return {
id: lc.id,
clientId: lc.clientKey ?? lc.id,
destinationOrganizationId: lc.destinationOrganizationId ?? null,
sentAt: lc.sentAt?.toISOString() ?? null,
+ dueDate: lc.dueDate?.toISOString() ?? null,
+ taskProgress,
treatmentDetailId: lc.details?.[0]?.treatmentDetailId ?? null,
detail: lc.details?.[0]
? {
diff --git a/frontend/messages/en.json b/frontend/messages/en.json
index 2012dd1..d7fa3b9 100644
--- a/frontend/messages/en.json
+++ b/frontend/messages/en.json
@@ -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.",
diff --git a/frontend/messages/fa.json b/frontend/messages/fa.json
index d41236e..82448f4 100644
--- a/frontend/messages/fa.json
+++ b/frontend/messages/fa.json
@@ -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": "محمولههای لاب ذخیره شد.",
diff --git a/frontend/messages/nl.json b/frontend/messages/nl.json
index 7b26ce4..315298f 100644
--- a/frontend/messages/nl.json
+++ b/frontend/messages/nl.json
@@ -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.",
diff --git a/frontend/src/components/lab/LabCaseDueDateBadge.tsx b/frontend/src/components/lab/LabCaseDueDateBadge.tsx
new file mode 100644
index 0000000..100f6ad
--- /dev/null
+++ b/frontend/src/components/lab/LabCaseDueDateBadge.tsx
@@ -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 (
+
diff --git a/frontend/src/components/ui/lab/TasksPage.tsx b/frontend/src/components/ui/lab/TasksPage.tsx
index 2d41a28..eba00f4 100644
--- a/frontend/src/components/ui/lab/TasksPage.tsx
+++ b/frontend/src/components/ui/lab/TasksPage.tsx
@@ -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