Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 62d5d94121 | |||
| 880809fdbe | |||
| e52701dba3 |
@@ -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`.
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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`.
|
||||
|
||||
|
||||
@@ -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';
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "اعلان",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,6 +13,7 @@ const TYPE_I18N: Record<UserNotificationType, string> = {
|
||||
CASE_IMPORTANT: 'typeCaseImportant',
|
||||
TASK_COMPLETED: 'typeTaskCompleted',
|
||||
TASK_ASSIGNED: 'typeTaskAssigned',
|
||||
CASE_COMPLETED: 'typeCaseCompleted',
|
||||
CONNECTION_REQUEST: 'typeConnectionRequest',
|
||||
STAFF_INVITE: 'typeStaffInvite',
|
||||
};
|
||||
@@ -74,6 +75,17 @@ export function notificationContextLine(
|
||||
if (clinicName) parts.push(clinicName);
|
||||
if (prosthesisLabel) parts.push(prosthesisLabel);
|
||||
break;
|
||||
case 'CASE_COMPLETED':
|
||||
if (patientName) parts.push(patientName);
|
||||
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':
|
||||
if (patientName) parts.push(patientName);
|
||||
if (labName) parts.push(labName);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ export type LabCaseActivityType =
|
||||
| 'CASE_IMPORTANT'
|
||||
| 'CASE_AMENDED'
|
||||
| 'TASK_COMPLETED'
|
||||
| 'TASK_ASSIGNED';
|
||||
| 'TASK_ASSIGNED'
|
||||
| 'CASE_COMPLETED';
|
||||
|
||||
export interface LabCaseActivityItem {
|
||||
id: string;
|
||||
|
||||
@@ -6,6 +6,7 @@ export type UserNotificationType =
|
||||
| 'CASE_IMPORTANT'
|
||||
| 'TASK_COMPLETED'
|
||||
| 'TASK_ASSIGNED'
|
||||
| 'CASE_COMPLETED'
|
||||
| 'CONNECTION_REQUEST'
|
||||
| 'STAFF_INVITE';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user