--- name: dyolink-tab-badges description: Sidebar tab badge counts (Cases/Tasks/Treatment) from LabCaseActivity + read cursors. Use when changing tab-counts API, activity emit, read state, or Sidebar badges — distinct from the header inbox bell. --- # Tab badges (Cases / Tasks / Treatment) Sidebar unread pills for **lab Cases**, **lab Tasks**, and **clinic Treatment**. Activity is stored as `LabCaseActivity` (shared clinic↔lab case events); badges are org-type-specific. Backend: [`backend/src/modules/notifications/`](backend/src/modules/notifications/) Activity types: [`backend/src/common/lab-case-activity.ts`](backend/src/common/lab-case-activity.ts) Frontend hook: [`frontend/src/lib/hooks/useTabBadgeCounts.ts`](frontend/src/lib/hooks/useTabBadgeCounts.ts) ## Models - **`LabCaseActivity`** — append-only events: `CASE_SENT`, `CLINIC_COMMENT`, `LAB_COMMENT`, `CASE_IMPORTANT`, `CASE_AMENDED` (stub for Step 7), `TASK_COMPLETED` - **`LabCaseUserTabReadState`** — per user/org/tab cursor (`TASKS`) for sidebar badge clearing on tab visit. - **`LabCaseUserReadState`** — per user/org/labCase cursor; drives Cases tab count and `hasUnread` on case list cards ## Tab badge buckets (Option B) | Org | Tab | Activity types | |-----|-----|----------------| | LAB | Cases | `CASE_SENT`, `CLINIC_COMMENT`, `CASE_IMPORTANT` | | LAB | Tasks | `TASK_COMPLETED`, `LAB_COMMENT` | | CLINIC | Treatment | `LAB_COMMENT` (only `visibleToClinic`), `TASK_COMPLETED` — **only lab cases for treatments the user provided** | Counts exclude events where `actorUserId === current user`. Clinic `LAB_COMMENT` counts only when `payload.visibleToClinic === true`. ## APIs - `GET /notifications/tab-counts` → `{ cases?, tasks?, treatment? }` — **Cases** count = number of cases with unread Cases-bucket activity (per-case read cursor) - `GET /notifications/lab-cases/:labCaseId/activities` — activity feed for a case (clinic-safe lab comments) - `POST /notifications/mark-tab-read` `{ tab }` — Tasks only (Cases/Treatment skip tab-level clear) - `POST /notifications/mark-case-read` `{ labCaseId }` — opening a case clears that case’s unread dot and updates Cases tab count - `GET /treatments/patients/:patientId/lab-cases` — patient shipment summaries for Treatment rail + tracker cards - `GET /treatments/lab-cases/unread` — org-wide unread shipment summaries for Treatment “All updates” scope ## Emit activity from | Event | Service | |-------|---------| | First send | `treatments.service` `sendLabCase` → `CASE_SENT` | | Comment | `lab-case-comments.service` → `CLINIC_COMMENT` / `LAB_COMMENT` | | Mark important | `cases.service` `updateImportant` (only when set true) → `CASE_IMPORTANT` | | Task completed | `tasks.service` `updateStatus` → `TASK_COMPLETED` | After mutations, frontend calls `notifyTabBadgesChanged()` (window event). **Live path:** inbox Socket.IO `notification.created` also dispatches that event so sidebar counts refresh without navigation. **Only currently mounted** feature pages soft-reload list data on the same event (silent; Treatment form/draft untouched). Unmounted tabs do not fetch list data until the user opens them. See `.cursor/skills/notifications-inbox/SKILL.md`. ## Frontend pattern (same as org connections) - `useTabBadgeCounts()` — always mounted in dashboard Sidebar; fetch on pathname change + `tab-badges-changed` - `useMarkTabReadOnVisit()` — Tasks page only (Cases/Treatment badges clear when opening unread cases) - `NavBadgePill` in [`Sidebar.tsx`](frontend/src/components/ui/shared/Sidebar.tsx) - **Organizations** pending connections still use `usePendingConnectionsCount` (separate pending-state API) - **Live soft refresh (mounted page only):** - Cases → silent list + selected detail - Tasks → silent task list - Treatment (`TreatmentWorkspace`) → silent patient lab cases + unread rail - Orgs → silent list on `pending-connections-changed` only ## Out of scope (later steps) - Push / email - Prefetching unmounted tab list data on every socket event - Pushing full page remounts / wiping Treatment draft state on live events - `CASE_AMENDED` emit (Step 7) ## Related: header inbox Permission-free bell + `UserNotification` fan-out + Socket.IO — see `.cursor/skills/notifications-inbox/SKILL.md`. Independent of tab badge cursors.