---
name: dyolink-lab-tasks
description: Lab Tasks tab — list, sort, filters, case grouping, prosthesis colors, step-completed filter. Use when changing TasksPage, tasks API, or lab task list UX.
---

# Lab Tasks

Main UI: [`frontend/src/components/ui/lab/TasksPage.tsx`](frontend/src/components/ui/lab/TasksPage.tsx)  
Backend: [`backend/src/modules/tasks/`](backend/src/modules/tasks/)

## Default sort (backend)

`sortBy=date` + `sortDir=desc`:

1. `labCase.sentAt` desc (newest case first)
2. `labCaseId`, `sourceKey`, `tooth`, `prosthesisTypeCode` asc (one pipeline per unconnected tooth; connected same-type teeth share a pipeline; `partial_denture` is one pipeline for all selected teeth)
3. `stepOrder` asc (steps 1→N within that tooth)
4. `id` asc

Other sorts use flat list on the frontend; `stepOrder asc` is still a tiebreaker.

## Case grouping (frontend)

- **`sortBy === 'date'`** → grouped view via [`taskListGrouping.ts`](frontend/src/components/lab/taskListGrouping.ts): case header → prosthesis sub-header (one per pipeline: unconnected tooth, or connected same-type bridge) → task rows (`treatmentDetailId:teeth:prosthesisTypeCode`).
- **Other sorts** → flat list; show muted hint (`groupingOff*` i18n keys). Each row keeps clinic/patient/teeth context.

Components: `TaskCaseGroupHeader`, `TaskProsthesisGroupHeader`, `TaskRow`.

**Grouped comments (`sortBy=date`):** single comments button on `TaskCaseGroupHeader`; panel expands below header (`expandedCommentsCaseId`). Per-task comments button only in **flat** sort (`showCommentsButton={flatMode}`). Shared `LabCaseCommentsPanel` with `viewerSide="LAB"` (newest-first, logical start/end chat align). Generated cases (`LAB_INTERNAL`) hide clinic-visibility controls.

## Prosthesis colors

- Map: [`catalog-type-colors.ts`](frontend/src/components/shared/catalog-type-colors.ts) → `PROSTHESIS_TYPE_COLORS` (one hex per catalog code).
- Resolve with [`prosthesisTypeDisplay.ts`](frontend/src/components/treatment/prosthesisTypeDisplay.ts) — use `prosthesisTypeColorFromCatalog(code, catalog)` as **text color** (no type pills), **not** list row index.
- Job titles: picker path to the last node ([`prosthesisJobPath.ts`](frontend/src/components/treatment/prosthesisJobPath.ts) / `ProsthesisStackedTypeLabel`) so technicians see e.g. Implants · Screw Retained, not “Implants”. Indirect skips the long parent: Inlay · Layered ceramic. Join with `PROSTHESIS_JOB_PATH_SEP` (`\u00a0·\u00a0`) so the gap around · does not collapse.
- Load catalog via `prosthesisCatalogApi.list()` on Tasks/Cases/Today dashboard.

## Filters

| Param | API | UI |
|-------|-----|-----|
| `q`, `clinicOrganizationId`, `status` | `GET /tasks` | Search, clinic, status |
| `origin` | `GET /tasks` | `CLINIC_DISPATCH` (received) / `LAB_INTERNAL` (generated) |
| `stepCompleted` | `GET /tasks` | Workflow step dropdown |
| `pinImportant` | `GET /tasks` | Important first (sort pin) |
| `assignedToMe` | `GET /tasks` | Only tasks assigned to current user |
| `prosthesisTypeCode` | `GET /tasks` | From Today prosthesis chart deep link |
| `unassignedOnly` | `GET /tasks` | Tasks with no assignee |
| `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.

**Tasks visibility & status edit:** All tasks remain visible to every user with task access (no hiding assigned tasks). **Unassigned** tasks or tasks **assigned to you** → status dropdown when `TAB_TASKS_EDIT`. **Assigned to someone else** → read-only “Assigned to {name}” badge instead of the dropdown (backend rejects status PATCH). Same rules on **lab case share link** page (`CaseTasksFocusView` + `canEditLabTaskStatus`). Managers assign/monitor in Cases.

**Step completed filter:** Restricts to prosthesis groups `(labCaseId, treatmentDetailId, prosthesisTypeCode)` where that `workflowStepCode` task is `COMPLETED`. Combined with `status=IN_PROGRESS`, returns only in-progress tasks in those groups (completed step row hidden).

- **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`.
- **URL state:** `parseTasksSearchParams` applies Today deep-link query params on mount (`importantOnly`, `overdueOnly`, `unassignedOnly`, `prosthesisTypeCode`, `status`, sort).
- **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.

## APIs

- `GET /tasks` — paginated flat task list (grouping is client-side when `sortBy=date`)
- `PATCH /tasks/:taskId` — update status (only assignee or unassigned task). Completing **`intraoral_scan`** also completes every other `intraoral_scan` task in the same case (case-scoped; catalog first step for all prosthesis types).
- `GET /tasks/filter-options` — clinics + workflow steps (localized)
- `GET /tasks/locate-page` — page number for a task in the sorted filtered list
- `GET /cases/assignable-staff` — staff eligible for task assignment
- `PATCH /cases/:caseId/tasks/:taskId/assign` — assign or unassign (`assigneeUserId` nullable)

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`, `LabCaseDueDateBadge`.

## Mobile UX (Tasks + Treatment dispatch)

Keep changes minimal — match existing `sm:` breakpoint patterns elsewhere in the app.

- **Task status control:** `LAB_TASK_STATUS_SELECT_CLASS` in [`formSelectStyles.ts`](frontend/src/components/shared/formSelectStyles.ts) — full-width, `min-h-[44px]`, `text-base` on mobile; compact on `sm+`. Read-only status / assignee badges match height on mobile.
- **Sticky case header:** `TaskCaseGroupHeader` uses `sticky top-0 z-10` + translucent background when `sortBy=date` (grouped view). Sticks within dashboard `<main>` scroll.
- **Tasks filters:** filter `<select>`s use the same 44px mobile height as other form controls.
- **Treatment lab dispatch:** shipment card `p-3 sm:p-4`; **Send to lab** is `w-full sm:w-auto`.

## Tab badges + live soft refresh

See `.cursor/skills/tab-badges/SKILL.md` — split lab Cases/Tasks counts, clinic Treatment; `useTabBadgeCounts` + `notifyTabBadgesChanged`.

Inbox Socket.IO `notification.created` → `notifyTabBadgesChanged()` → silent `loadTasks({ silent: true })` on an open Tasks page (filters preserved; no remount). Details: `.cursor/skills/notifications-inbox/SKILL.md`.

## Permissions

`TAB_TASKS_READ` / `TAB_TASKS_EDIT`; `LabOrgGuard` on all task routes.
