From 880809fdbe06b0ed51f1678273c064f467753742 Mon Sep 17 00:00:00 2001 From: rameen Date: Mon, 7 Sep 2026 22:36:33 +0330 Subject: [PATCH 1/3] improvement: users get notified whenever a task is done completely, not per step. --- .cursor/rules/tab-badges.mdc | 2 +- .cursor/skills/notifications-inbox/SKILL.md | 2 +- .cursor/skills/tab-badges/SKILL.md | 9 +++-- .../migration.sql | 8 ++++ backend/prisma/schema.prisma | 2 + backend/src/common/lab-case-activity.ts | 8 ++-- backend/src/modules/tasks/tasks.service.ts | 38 ++++++++++++++----- frontend/messages/en.json | 2 + frontend/messages/fa.json | 2 + frontend/messages/nl.json | 2 + frontend/src/app/[locale]/layout.tsx | 3 +- .../ui/notifications/NotificationCard.tsx | 7 ++++ frontend/src/lib/labCaseActivityLabels.ts | 2 + frontend/src/lib/theme.ts | 4 +- frontend/src/styles/globals.css | 13 ++++--- frontend/src/types/lab-case-activity.ts | 3 +- frontend/src/types/notifications.ts | 1 + 17 files changed, 79 insertions(+), 29 deletions(-) create mode 100644 backend/prisma/migrations/20260907200000_case_completed_notifications/migration.sql diff --git a/.cursor/rules/tab-badges.mdc b/.cursor/rules/tab-badges.mdc index a7c77e7..ca90aa8 100644 --- a/.cursor/rules/tab-badges.mdc +++ b/.cursor/rules/tab-badges.mdc @@ -6,7 +6,7 @@ alwaysApply: false # Tab badges (Cases / Tasks / Treatment) -- **Split counts (Option B):** Lab Cases = sent + clinic comments + important; Lab Tasks = completions + lab comments + assignments (assignee-only); Clinic Treatment = visible lab comments + completions. +- **Split counts (Option B):** Lab Cases = sent + clinic comments + important; Lab Tasks = case fully completed + lab comments + assignments (assignee-only); Clinic Treatment = visible lab comments + case fully completed. Per-step `TASK_COMPLETED` is timeline-only (not badges or inbox). - **API:** `GET /notifications/tab-counts`; **Cases + Treatment** use per-case read + `hasUnread` on list cards; Tasks marks read on tab visit. Treatment rail uses `TreatmentLabCasesPanel` + `LabCaseTrackerCard` + activity feed. - **Pattern:** `useTabBadgeCounts` + `notifyTabBadgesChanged()` — same shape as `usePendingConnectionsCount`. - **Live:** inbox Socket.IO `notification.created` → `notifyTabBadgesChanged()` (and org pending event when relevant). **Mounted** Cases/Tasks/Treatment/Orgs pages soft-refetch lists; unmounted tabs do not. Sidebar badge counts always refetch (hook is always mounted). diff --git a/.cursor/skills/notifications-inbox/SKILL.md b/.cursor/skills/notifications-inbox/SKILL.md index 8330f6c..ca86be1 100644 --- a/.cursor/skills/notifications-inbox/SKILL.md +++ b/.cursor/skills/notifications-inbox/SKILL.md @@ -43,7 +43,7 @@ Full tab-badge map: `.cursor/skills/tab-badges/SKILL.md`. ## Emit sites (parallel to LabCaseActivity) -CASE_SENT, CLINIC_COMMENT, LAB_COMMENT (+ LAB_COMMENT_CLINIC), CASE_IMPORTANT, TASK_COMPLETED, TASK_ASSIGNED (assignee only), CONNECTION_REQUEST, STAFF_INVITE — see service call sites. +CASE_SENT, CLINIC_COMMENT, LAB_COMMENT (+ LAB_COMMENT_CLINIC), CASE_IMPORTANT, CASE_COMPLETED (all tasks in the case done — not each step), TASK_ASSIGNED (assignee only), CONNECTION_REQUEST, STAFF_INVITE — see service call sites. **Inbox card context** is denormalized inside `UserNotificationService.notify()` (`enrichInboxPayload`) from ids already on the payload (`labCaseId`, `taskId`, `fromOrganizationId`). Emit sites stay thin (`{ labCaseId }`, etc.). Inbox list/read does **not** join related tables. Older rows may lack these fields until new events are emitted. diff --git a/.cursor/skills/tab-badges/SKILL.md b/.cursor/skills/tab-badges/SKILL.md index 5ddf8f0..64320d5 100644 --- a/.cursor/skills/tab-badges/SKILL.md +++ b/.cursor/skills/tab-badges/SKILL.md @@ -13,7 +13,7 @@ Frontend hook: [`frontend/src/lib/hooks/useTabBadgeCounts.ts`](frontend/src/lib/ ## Models -- **`LabCaseActivity`** — append-only events: `CASE_SENT`, `CLINIC_COMMENT`, `LAB_COMMENT`, `CASE_IMPORTANT`, `CASE_AMENDED` (stub for Step 7), `TASK_COMPLETED`, `TASK_ASSIGNED` +- **`LabCaseActivity`** — append-only events: `CASE_SENT`, `CLINIC_COMMENT`, `LAB_COMMENT`, `CASE_IMPORTANT`, `CASE_AMENDED` (stub for Step 7), `TASK_COMPLETED` (timeline only), `TASK_ASSIGNED`, `CASE_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 @@ -22,8 +22,8 @@ Frontend hook: [`frontend/src/lib/hooks/useTabBadgeCounts.ts`](frontend/src/lib/ | Org | Tab | Activity types | |-----|-----|----------------| | LAB | Cases | `CASE_SENT`, `CLINIC_COMMENT`, `CASE_IMPORTANT` | -| LAB | Tasks | `TASK_COMPLETED`, `LAB_COMMENT`, `TASK_ASSIGNED` (assignee only) | -| CLINIC | Treatment | `LAB_COMMENT` (only `visibleToClinic`), `TASK_COMPLETED` — **only lab cases for treatments the user provided** | +| LAB | Tasks | `CASE_COMPLETED`, `LAB_COMMENT`, `TASK_ASSIGNED` (assignee only) | +| CLINIC | Treatment | `LAB_COMMENT` (only `visibleToClinic`), `CASE_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`. @@ -43,7 +43,8 @@ Counts exclude events where `actorUserId === current user`. Clinic `LAB_COMMENT` | 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` | +| Task completed (step) | `tasks.service` `updateStatus` → `TASK_COMPLETED` **activity only** (case timeline; not inbox or tab badges) | +| Case fully completed | `tasks.service` `updateStatus` when no in-progress tasks remain → `CASE_COMPLETED` activity + inbox | | Task assigned | `cases.service` `assignTask` → `TASK_ASSIGNED` (inbox + Tasks badge for **assignee**, including self-assign) | After mutations, frontend calls `notifyTabBadgesChanged()` (window event). diff --git a/backend/prisma/migrations/20260907200000_case_completed_notifications/migration.sql b/backend/prisma/migrations/20260907200000_case_completed_notifications/migration.sql new file mode 100644 index 0000000..647af28 --- /dev/null +++ b/backend/prisma/migrations/20260907200000_case_completed_notifications/migration.sql @@ -0,0 +1,8 @@ +-- AlterEnum +ALTER TYPE "LabCaseActivityType" ADD VALUE 'CASE_COMPLETED'; + +-- AlterEnum +ALTER TYPE "UserNotificationType" ADD VALUE 'CASE_COMPLETED'; + +-- Step-level TASK_COMPLETED inbox rows were spam; case-complete uses CASE_COMPLETED going forward. +DELETE FROM "UserNotification" WHERE type = 'TASK_COMPLETED'; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index f80558f..35b5551 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -492,6 +492,7 @@ enum LabCaseActivityType { CASE_AMENDED TASK_COMPLETED TASK_ASSIGNED + CASE_COMPLETED } enum LabCaseTabReadTarget { @@ -508,6 +509,7 @@ enum UserNotificationType { CASE_IMPORTANT TASK_COMPLETED TASK_ASSIGNED + CASE_COMPLETED CONNECTION_REQUEST STAFF_INVITE } diff --git a/backend/src/common/lab-case-activity.ts b/backend/src/common/lab-case-activity.ts index 9b11fd0..b55190e 100644 --- a/backend/src/common/lab-case-activity.ts +++ b/backend/src/common/lab-case-activity.ts @@ -7,15 +7,15 @@ export const LAB_CASES_TAB_ACTIVITY_TYPES: LabCaseActivityType[] = [ LabCaseActivityType.CASE_IMPORTANT, ]; -/** Lab Tasks tab — task completions, lab-side comments, and assignments (assignee-scoped in counts). */ +/** Lab Tasks tab — case fully completed, lab-side comments, and assignments (assignee-scoped in counts). */ export const LAB_TASKS_TAB_ACTIVITY_TYPES: LabCaseActivityType[] = [ - LabCaseActivityType.TASK_COMPLETED, + LabCaseActivityType.CASE_COMPLETED, LabCaseActivityType.LAB_COMMENT, LabCaseActivityType.TASK_ASSIGNED, ]; -/** Clinic Treatment tab — visible lab comments and task progress. */ +/** Clinic Treatment tab — visible lab comments and case fully completed. */ export const CLINIC_TREATMENT_TAB_ACTIVITY_TYPES: LabCaseActivityType[] = [ LabCaseActivityType.LAB_COMMENT, - LabCaseActivityType.TASK_COMPLETED, + LabCaseActivityType.CASE_COMPLETED, ]; diff --git a/backend/src/modules/tasks/tasks.service.ts b/backend/src/modules/tasks/tasks.service.ts index 233eacc..d43457d 100644 --- a/backend/src/modules/tasks/tasks.service.ts +++ b/backend/src/modules/tasks/tasks.service.ts @@ -303,6 +303,7 @@ export class TasksService { } } + let caseCompleted = false; if (dto.status === LabTaskStatus.COMPLETED && task.status !== LabTaskStatus.COMPLETED) { await this.labCaseActivity.record( { @@ -321,18 +322,37 @@ export class TasksService { }, tx, ); + + const remainingIncomplete = await tx.labCaseTask.count({ + where: { + labCaseId: task.labCaseId, + status: { not: LabTaskStatus.COMPLETED }, + }, + }); + caseCompleted = remainingIncomplete === 0; + if (caseCompleted) { + await this.labCaseActivity.record( + { + labCaseId: task.labCaseId, + type: LabCaseActivityType.CASE_COMPLETED, + actorUserId, + payload: { labCaseId: task.labCaseId }, + }, + tx, + ); + } } - return result; + return { result, caseCompleted }; }); - if (dto.status === LabTaskStatus.COMPLETED && task.status !== LabTaskStatus.COMPLETED) { + if (updated.caseCompleted) { void this.userNotifications.notify({ organizationId: labOrganizationId, - type: UserNotificationType.TASK_COMPLETED, - href: `/tasks?taskId=${encodeURIComponent(taskId)}&labCaseId=${encodeURIComponent(task.labCaseId)}`, + type: UserNotificationType.CASE_COMPLETED, + href: `/cases?caseId=${encodeURIComponent(task.labCaseId)}`, actorUserId, - payload: { labCaseId: task.labCaseId, taskId }, + payload: { labCaseId: task.labCaseId }, requiredPermission: 'TAB_TASKS_READ', }); @@ -340,10 +360,10 @@ export class TasksService { if (clinicOrgId) { void this.userNotifications.notify({ organizationId: clinicOrgId, - type: UserNotificationType.TASK_COMPLETED, + type: UserNotificationType.CASE_COMPLETED, href: `/treatment?labCaseId=${encodeURIComponent(task.labCaseId)}`, actorUserId, - payload: { labCaseId: task.labCaseId, taskId }, + payload: { labCaseId: task.labCaseId }, requiredPermission: 'TAB_TREATMENT_READ', labCaseIdForProviderScope: task.labCaseId, }); @@ -353,11 +373,11 @@ export class TasksService { const locale = normalizeCatalogLocale(localeInput); const prosthesisLabels = await this.catalogLabels.resolveLabels( CatalogEntityKind.PROSTHESIS_TYPE, - atomicProsthesisCodes([updated.prosthesisTypeCode]), + atomicProsthesisCodes([updated.result.prosthesisTypeCode]), locale, ); - return { success: true, data: this.mapTaskListItem(updated, prosthesisLabels) }; + return { success: true, data: this.mapTaskListItem(updated.result, prosthesisLabels) }; } async listFilterOptions( diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 961fcbb..ca06680 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -846,6 +846,7 @@ "activityTaskCompleted": "{step} completed by {actor} · {date}", "activityTaskAssigned": "{step} assigned by {actor} · {date}", "activityCaseImportant": "Marked important by {actor} · {date}", + "activityCaseCompleted": "All lab work completed by {actor} · {date}", "activityCaseAmended": "Case updated by {actor} · {date}", "activityGeneric": "Update · {date}", "loadingHistory": "Loading history…", @@ -1105,6 +1106,7 @@ "typeCaseImportant": "Case marked as important", "typeTaskCompleted": "Lab task completed", "typeTaskAssigned": "A task was assigned to you", + "typeCaseCompleted": "Lab case completed", "typeConnectionRequest": "New organization connection request", "typeStaffInvite": "Staff invitation created", "typeUnknown": "Notification", diff --git a/frontend/messages/fa.json b/frontend/messages/fa.json index 9023f7b..da31abf 100644 --- a/frontend/messages/fa.json +++ b/frontend/messages/fa.json @@ -847,6 +847,7 @@ "activityTaskCompleted": "{step} توسط {actor} تکمیل شد · {date}", "activityTaskAssigned": "{step} توسط {actor} اختصاص داده شد · {date}", "activityCaseImportant": "مهم علامت‌گذاری شد توسط {actor} · {date}", + "activityCaseCompleted": "تمام کارهای لابراتوار توسط {actor} تکمیل شد · {date}", "activityCaseAmended": "پرونده به‌روزرسانی شد توسط {actor} · {date}", "activityGeneric": "به‌روزرسانی · {date}", "loadingHistory": "در حال بارگذاری تاریخچه...", @@ -1106,6 +1107,7 @@ "typeCaseImportant": "پرونده به‌عنوان مهم علامت خورد", "typeTaskCompleted": "وظیفه لابراتوار تکمیل شد", "typeTaskAssigned": "یک وظیفه به شما اختصاص داده شد", + "typeCaseCompleted": "پرونده لابراتوار تکمیل شد", "typeConnectionRequest": "درخواست اتصال سازمان جدید", "typeStaffInvite": "دعوتنامه کارکنان ایجاد شد", "typeUnknown": "اعلان", diff --git a/frontend/messages/nl.json b/frontend/messages/nl.json index f3cc81e..bb4bfae 100644 --- a/frontend/messages/nl.json +++ b/frontend/messages/nl.json @@ -846,6 +846,7 @@ "activityTaskCompleted": "{step} voltooid door {actor} · {date}", "activityTaskAssigned": "{step} toegewezen door {actor} · {date}", "activityCaseImportant": "Als belangrijk gemarkeerd door {actor} · {date}", + "activityCaseCompleted": "Al het labwerk voltooid door {actor} · {date}", "activityCaseAmended": "Case bijgewerkt door {actor} · {date}", "activityGeneric": "Update · {date}", "loadingHistory": "Geschiedenis laden...", @@ -1105,6 +1106,7 @@ "typeCaseImportant": "Case gemarkeerd als belangrijk", "typeTaskCompleted": "Labtaak voltooid", "typeTaskAssigned": "Er is een taak aan u toegewezen", + "typeCaseCompleted": "Labcase voltooid", "typeConnectionRequest": "Nieuw organisatieverzoek", "typeStaffInvite": "Personeelsuitnodiging aangemaakt", "typeUnknown": "Melding", diff --git a/frontend/src/app/[locale]/layout.tsx b/frontend/src/app/[locale]/layout.tsx index 9969ef5..0dfe5da 100644 --- a/frontend/src/app/[locale]/layout.tsx +++ b/frontend/src/app/[locale]/layout.tsx @@ -54,7 +54,7 @@ export default async function LocaleLayout({ setRequestLocale(locale); const messages = await getMessages(); - const themeInit = `(function(){try{var k=${JSON.stringify(THEME_STORAGE_KEY)};var t=localStorage.getItem(k);document.documentElement.setAttribute('data-theme',t==='light'||t==='dark'?t:'dark');}catch(e){document.documentElement.setAttribute('data-theme','dark');}})();`; + const themeInit = `(function(){try{var k=${JSON.stringify(THEME_STORAGE_KEY)};var t=localStorage.getItem(k);document.documentElement.setAttribute('data-theme',t==='light'||t==='dark'?t:'light');}catch(e){document.documentElement.setAttribute('data-theme','light');}})();`; const dir = isRtlLocale(locale) ? 'rtl' : 'ltr'; const fontSans = isRtlLocale(locale) ? 'var(--font-vazirmatn), var(--font-noto-sans-arabic), system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif' @@ -65,6 +65,7 @@ export default async function LocaleLayout({ lang={localeHtmlLang(locale)} dir={dir} data-locale={locale} + data-theme="light" className={`${vazirmatn.variable} ${notoSansArabic.variable}`} style={{ ['--font-sans' as never]: fontSans }} suppressHydrationWarning diff --git a/frontend/src/components/ui/notifications/NotificationCard.tsx b/frontend/src/components/ui/notifications/NotificationCard.tsx index 165c9ea..c046ab6 100644 --- a/frontend/src/components/ui/notifications/NotificationCard.tsx +++ b/frontend/src/components/ui/notifications/NotificationCard.tsx @@ -13,6 +13,7 @@ const TYPE_I18N: Record = { CASE_IMPORTANT: 'typeCaseImportant', TASK_COMPLETED: 'typeTaskCompleted', TASK_ASSIGNED: 'typeTaskAssigned', + CASE_COMPLETED: 'typeCaseCompleted', CONNECTION_REQUEST: 'typeConnectionRequest', STAFF_INVITE: 'typeStaffInvite', }; @@ -74,6 +75,12 @@ export function notificationContextLine( if (clinicName) parts.push(clinicName); if (prosthesisLabel) parts.push(prosthesisLabel); break; + case 'CASE_COMPLETED': + if (patientName) parts.push(patientName); + if (clinicName) parts.push(clinicName); + else if (labName) parts.push(labName); + if (prosthesisLabel) parts.push(prosthesisLabel); + break; case 'LAB_COMMENT_CLINIC': if (patientName) parts.push(patientName); if (labName) parts.push(labName); diff --git a/frontend/src/lib/labCaseActivityLabels.ts b/frontend/src/lib/labCaseActivityLabels.ts index 117f2de..27a3cb8 100644 --- a/frontend/src/lib/labCaseActivityLabels.ts +++ b/frontend/src/lib/labCaseActivityLabels.ts @@ -43,6 +43,8 @@ export function formatLabCaseActivityLine( }); case 'CASE_IMPORTANT': return t('activityCaseImportant', { actor, date }); + case 'CASE_COMPLETED': + return t('activityCaseCompleted', { actor, date }); case 'CASE_AMENDED': return t('activityCaseAmended', { actor, date }); default: diff --git a/frontend/src/lib/theme.ts b/frontend/src/lib/theme.ts index 8c2cc97..2933db2 100644 --- a/frontend/src/lib/theme.ts +++ b/frontend/src/lib/theme.ts @@ -3,14 +3,14 @@ export const THEME_STORAGE_KEY = 'dyolink-theme'; export type ThemeMode = 'light' | 'dark'; export function getStoredTheme(): ThemeMode { - if (typeof window === 'undefined') return 'dark'; + if (typeof window === 'undefined') return 'light'; try { const v = localStorage.getItem(THEME_STORAGE_KEY); if (v === 'light' || v === 'dark') return v; } catch { /* ignore */ } - return 'dark'; + return 'light'; } export function applyTheme(mode: ThemeMode) { diff --git a/frontend/src/styles/globals.css b/frontend/src/styles/globals.css index cda40ac..dd2a8ec 100644 --- a/frontend/src/styles/globals.css +++ b/frontend/src/styles/globals.css @@ -262,13 +262,17 @@ body { } html { - color-scheme: dark; + color-scheme: light; } html[data-theme='light'] { color-scheme: light; } +html[data-theme='dark'] { + color-scheme: dark; +} + /* Minimal RTL layer — refine incrementally. */ html[dir='rtl'] body { direction: rtl; @@ -338,9 +342,7 @@ select option { } :root[data-theme='dark'] select.form-select, -:root[data-theme='dark'] select, -:root:not([data-theme='light']) select.form-select, -:root:not([data-theme='light']) select { +:root[data-theme='dark'] select { color-scheme: dark; } @@ -358,8 +360,7 @@ select option { } } -:root[data-theme='dark'] .surface-card, -:root:not([data-theme='light']) .surface-card { +:root[data-theme='dark'] .surface-card { background: color-mix(in srgb, var(--color-card-background) 82%, var(--color-background-primary)); } diff --git a/frontend/src/types/lab-case-activity.ts b/frontend/src/types/lab-case-activity.ts index fed69c9..4f83c5d 100644 --- a/frontend/src/types/lab-case-activity.ts +++ b/frontend/src/types/lab-case-activity.ts @@ -5,7 +5,8 @@ export type LabCaseActivityType = | 'CASE_IMPORTANT' | 'CASE_AMENDED' | 'TASK_COMPLETED' - | 'TASK_ASSIGNED'; + | 'TASK_ASSIGNED' + | 'CASE_COMPLETED'; export interface LabCaseActivityItem { id: string; diff --git a/frontend/src/types/notifications.ts b/frontend/src/types/notifications.ts index 390390a..8d4daf0 100644 --- a/frontend/src/types/notifications.ts +++ b/frontend/src/types/notifications.ts @@ -6,6 +6,7 @@ export type UserNotificationType = | 'CASE_IMPORTANT' | 'TASK_COMPLETED' | 'TASK_ASSIGNED' + | 'CASE_COMPLETED' | 'CONNECTION_REQUEST' | 'STAFF_INVITE'; -- 2.53.0.windows.1 From 62d5d941212c5dc66dc665eb0feff2acd7bac578 Mon Sep 17 00:00:00 2001 From: rameen Date: Mon, 7 Sep 2026 23:18:00 +0330 Subject: [PATCH 2/3] review notification logic and fix minor issue. --- .cursor/rules/dyolink-overview.mdc | 2 +- .cursor/rules/lab-tasks.mdc | 1 + .cursor/skills/lab-tasks/SKILL.md | 2 +- AGENTS.md | 4 ++-- .../src/components/ui/notifications/NotificationCard.tsx | 9 +++++++-- 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.cursor/rules/dyolink-overview.mdc b/.cursor/rules/dyolink-overview.mdc index a573b36..77e9b9b 100644 --- a/.cursor/rules/dyolink-overview.mdc +++ b/.cursor/rules/dyolink-overview.mdc @@ -46,4 +46,4 @@ Logical failures: `AppException(ErrorCode.X)` → `errors.X` in en/fa/nl. UI: `g ## Notifications (inbox + live tabs) -Header bell: `UserNotification` + Socket.IO. Same `notification.created` also drives sidebar tab badges and soft list refresh on **currently open** Cases/Tasks/Treatment/Orgs pages. Skills: `.cursor/skills/notifications-inbox/SKILL.md`, `.cursor/skills/tab-badges/SKILL.md`. +Header bell: `UserNotification` + Socket.IO. Same `notification.created` also drives sidebar tab badges and soft list refresh on **currently open** Cases/Tasks/Treatment/Orgs pages. Inbox + Tasks/Treatment badges use **`CASE_COMPLETED`** (every task in the case done); per-step `TASK_COMPLETED` is timeline-only. Skills: `.cursor/skills/notifications-inbox/SKILL.md`, `.cursor/skills/tab-badges/SKILL.md`. diff --git a/.cursor/rules/lab-tasks.mdc b/.cursor/rules/lab-tasks.mdc index a28eeb5..ef61ffe 100644 --- a/.cursor/rules/lab-tasks.mdc +++ b/.cursor/rules/lab-tasks.mdc @@ -17,5 +17,6 @@ alwaysApply: false - **Mobile UX:** `LAB_TASK_STATUS_SELECT_CLASS` (44px tap target on small screens); `TaskCaseGroupHeader` sticky while scrolling grouped tasks; filter selects use same touch sizing on Tasks. - **Show in case:** `GET /tasks/locate-page` finds page in full list; highlight + scroll. - **Today deep links:** `parseTasksSearchParams` + `prosthesisTypeCode` / `unassignedOnly` / `overdueOnly` query params on Tasks. +- **Inbox / tab badges:** completing a step writes timeline `TASK_COMPLETED` only; inbox + Tasks/Treatment badges fire on `CASE_COMPLETED` when no in-progress tasks remain. 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 1298848..c5064bf 100644 --- a/.cursor/skills/lab-tasks/SKILL.md +++ b/.cursor/skills/lab-tasks/SKILL.md @@ -95,7 +95,7 @@ Keep changes minimal — match existing `sm:` breakpoint patterns elsewhere in t ## Tab badges + live soft refresh -See `.cursor/skills/tab-badges/SKILL.md` — split lab Cases/Tasks counts, clinic Treatment; `useTabBadgeCounts` + `notifyTabBadgesChanged`. +See `.cursor/skills/tab-badges/SKILL.md` — split lab Cases/Tasks counts, clinic Treatment; `useTabBadgeCounts` + `notifyTabBadgesChanged`. Inbox and Tasks/Treatment badges fire on `CASE_COMPLETED` (all tasks done), not each workflow step. The case timeline still records `TASK_COMPLETED` per step. 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`. diff --git a/AGENTS.md b/AGENTS.md index 61e54cb..7aac795 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,7 +58,7 @@ frontend/src/ - Detail treatment type need **not** match appointment purpose — purpose only seeds the **first** line of an empty **appointment** draft (first open, and **Add detail** when the plan is `[]`). Further **Add detail** starts with an empty type. Unscheduled / New treatment still seeds a blank first line. - **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 shipments rail**: unified list with scope toggle **This patient** vs **All updates** (unread across org for **this clinician's cases only**, includes patient name). Opening a case from the rail **opens the lab dispatch view** (same slot as the chart). -- **Unread semantics**: Treatment tab badge = count of unread cases **for the user's own treatment plans** (per-case read cursor) and clears when a case is opened/marked read (not on tab visit). +- **Unread semantics**: Treatment tab badge = count of unread cases **for the user's own treatment plans** (per-case read cursor) and clears when a case is opened/marked read (not on tab visit). Completions count only when **every task in the case is done** (`CASE_COMPLETED`) — not each workflow step. - **Live lab rail**: `notification.created` → `notifyTabBadgesChanged()` silently refreshes patient lab cases + unread rail (does **not** clear draft/form state). - **Lab shipment progress + comments**: shown in **Lab dispatch panel** for the active shipment; expanding activity / opening comments marks that case read. Shared UI: `LabCaseCommentsPanel` — newest first; sent = start / received = end (`text-start`/`justify-start`, RTL-safe); pass `viewerSide`. @@ -68,7 +68,7 @@ frontend/src/ **Staff (quick ref):** Owner / `TAB_STAFF_EDIT` can **remove** a password (`POST /staff/members/:id/clear-password`) and copy a setup link — never set one for someone else. Login page unchanged (`passwordHash: null` cannot sign in). `/accept-invite` `password_setup` is password-only. See `.cursor/rules/staff.mdc`. -**Lab Tasks tab:** Newest case first; steps ordered 1→N; case grouping when sorted by date; `stepCompleted` filter; filter by case source (`origin`: received vs generated); prosthesis colors from catalog; job titles show the picker path to the leaf (`prosthesisJobPath.ts`); 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; completing **`intraoral_scan`** completes every scan task in that case (case-scoped; catalog first step for all prosthesis types); **mobile:** larger task status controls, sticky case header when grouped; **tab badges:** `LabCaseActivity` + `GET /notifications/tab-counts` (lab Cases/Tasks split, clinic Treatment) — live via inbox Socket.IO → `notifyTabBadgesChanged()` + soft list refresh — see `.cursor/skills/lab-tasks/SKILL.md`, `.cursor/skills/tab-badges/SKILL.md`, `.cursor/skills/notifications-inbox/SKILL.md`. +**Lab Tasks tab:** Newest case first; steps ordered 1→N; case grouping when sorted by date; `stepCompleted` filter; filter by case source (`origin`: received vs generated); prosthesis colors from catalog; job titles show the picker path to the leaf (`prosthesisJobPath.ts`); 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; completing **`intraoral_scan`** completes every scan task in that case (case-scoped; catalog first step for all prosthesis types); **mobile:** larger task status controls, sticky case header when grouped; **tab badges / inbox:** Tasks + Treatment badges and the header bell increment on **`CASE_COMPLETED`** (all tasks in the case done), not each step — the case timeline still lists `TASK_COMPLETED` per step; live via inbox Socket.IO → `notifyTabBadgesChanged()` + soft list refresh — see `.cursor/skills/lab-tasks/SKILL.md`, `.cursor/skills/tab-badges/SKILL.md`, `.cursor/skills/notifications-inbox/SKILL.md`. **Lab Cases tab:** Filter by **prosthesis type** (not treatment type); auto-select newest case on open; list **10 per page**; left rail list fills column height (`flex-1 overflow-y-auto`); list cards use `LabCaseProsthesisGroupsList` (colored type + teeth, shared with Treatment rail) plus **Received** / **Generated** origin badges. Job titles show the picker path to the **leaf** (`prosthesisJobPath.ts` — e.g. Crowns · PFM Crown, not “Crowns”; indirect is Inlay · Layered ceramic, not the long Veneer/Inlay/Onlay/Overlay parent). Deep link: `?caseId=`, `?clinicOrganizationId=`. **Share link:** QR + URL on sent cases (attachment left, QR right); opens `/lab-case/[token]` focus page. **Case Sheet PDF:** client A4 (`jspdf`/`html2canvas`); hex-only print layout; optional `externalCode` replaces order number. Lab-origin Start has no clinic send; comments and mark-read still work (no clinic-visibility toggle). Unstarted generated drafts can be deleted (`DELETE /cases/:id`). **Live:** inbox Socket.IO → `notifyTabBadgesChanged()` soft-refreshes list + selected detail (no remount). See `.cursor/skills/lab-cases/SKILL.md` and `.cursor/skills/lab-case-share-link/SKILL.md`. diff --git a/frontend/src/components/ui/notifications/NotificationCard.tsx b/frontend/src/components/ui/notifications/NotificationCard.tsx index c046ab6..8790665 100644 --- a/frontend/src/components/ui/notifications/NotificationCard.tsx +++ b/frontend/src/components/ui/notifications/NotificationCard.tsx @@ -77,8 +77,13 @@ export function notificationContextLine( break; case 'CASE_COMPLETED': if (patientName) parts.push(patientName); - if (clinicName) parts.push(clinicName); - else if (labName) parts.push(labName); + if (item.href.startsWith('/treatment')) { + if (labName) parts.push(labName); + } else if (clinicName) { + parts.push(clinicName); + } else if (labName) { + parts.push(labName); + } if (prosthesisLabel) parts.push(prosthesisLabel); break; case 'LAB_COMMENT_CLINIC': -- 2.53.0.windows.1 From e4f175903556bf4f09284e57fb46e91ac9231dba Mon Sep 17 00:00:00 2001 From: rameen Date: Tue, 8 Sep 2026 10:42:22 +0330 Subject: [PATCH 3/3] fix: use mapped table name in case_completed migration Prisma migrate failed on prod (P3009) because DELETE targeted "UserNotification" instead of user_notifications. Co-authored-by: Cursor --- .../20260907200000_case_completed_notifications/migration.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/prisma/migrations/20260907200000_case_completed_notifications/migration.sql b/backend/prisma/migrations/20260907200000_case_completed_notifications/migration.sql index 647af28..c2e895d 100644 --- a/backend/prisma/migrations/20260907200000_case_completed_notifications/migration.sql +++ b/backend/prisma/migrations/20260907200000_case_completed_notifications/migration.sql @@ -5,4 +5,4 @@ ALTER TYPE "LabCaseActivityType" ADD VALUE 'CASE_COMPLETED'; ALTER TYPE "UserNotificationType" ADD VALUE 'CASE_COMPLETED'; -- Step-level TASK_COMPLETED inbox rows were spam; case-complete uses CASE_COMPLETED going forward. -DELETE FROM "UserNotification" WHERE type = 'TASK_COMPLETED'; +DELETE FROM "user_notifications" WHERE type = 'TASK_COMPLETED'; -- 2.53.0.windows.1