diff --git a/.cursor/rules/treatment-workspace.mdc b/.cursor/rules/treatment-workspace.mdc index 9f0c3a5..495d033 100644 --- a/.cursor/rules/treatment-workspace.mdc +++ b/.cursor/rules/treatment-workspace.mdc @@ -8,8 +8,10 @@ alwaysApply: false - **Browse mode** (`selectedPreviewId` set): preview only; banner + **Load into workspace**; form unchanged until load. - **Current draft** preview: heading “Current draft”; no load button while editing live. -- **Entry wizard:** Teeth → Content → Lab via `WizardStepper` (not detail chips). Lab step only for lab-dependent (prosthesis) active detail. Appointment purpose does not gate type or Next. +- **Entry wizard:** Teeth → Content → Lab via `WizardStepper` (not detail chips). Lab step only for lab-dependent (prosthesis) active detail; entering Lab auto-opens shipment draft. Add detail ungated. Appointment purpose does not gate type or Next. - **Tooth groups:** Shift+same-arch range = connected (linked dots); Ctrl/⌘+same-arch range = singles (one lab row each); click in connected span → collapse to that single only. `toothSelectionGroups.ts`; prune lab `toothProsthesis` on change. +- **Lab dispatch UI:** due date end-aligned beside title (`sm:flex-row` + `justify-between`; stacks on mobile). Prosthesis type: stacked below `md`, 50/50 same-row from `md`. Content clinical field = **Notes** (not case comments). +- **Lab comments:** shared `LabCaseCommentsPanel` (newest-first; sent=`justify-start`, received=`justify-end`; `viewerSide`) across Treatment / Cases / Tasks / share. Use logical `text-start`/`text-end`, not left/right. - **Detail chrome:** chips + Add at top; **Remove** = trash on chip (unsent, >1 detail; disabled when day-locked / no edit / uploading). No delete in Content step. - **Lab dispatch attention:** `LabDispatchAttentionPanel` — unsent lab-dependent details; quick jump to dispatch. - **History API:** patient-scoped; non-owners filtered by provider on treatment or appointment; org owners see all. diff --git a/.cursor/skills/lab-case-share-link/SKILL.md b/.cursor/skills/lab-case-share-link/SKILL.md index 9be6bbc..fe237c2 100644 --- a/.cursor/skills/lab-case-share-link/SKILL.md +++ b/.cursor/skills/lab-case-share-link/SKILL.md @@ -46,7 +46,7 @@ Task status updates use **`PATCH /tasks/:id`** (not token routes) — same assig **Cases detail header:** attachment preview **left**, QR thumb **right**, same row (`w-24 sm:w-32`). QR opens dialog (large QR + URL + copy); no inline copy on panel. Only when `shareUrl` present (sent case). -**Share focus page:** grouped tasks (reuse `TaskRow`, `TaskCaseGroupHeader`); comments section via `LabCaseCommentsPanel` + token API adapters. Access denied → inline message (`asApiError` for `LAB_CASE_ACCESS_DENIED`). +**Share focus page:** grouped tasks (reuse `TaskRow`, `TaskCaseGroupHeader`); comments via `LabCaseCommentsPanel` + token API (`viewerSide` from `session.accessMode`). Newest-first; sent/received use logical start/end. Access denied → inline message (`asApiError` for `LAB_CASE_ACCESS_DENIED`). ## Auth redirect (logged out → login → back) diff --git a/.cursor/skills/lab-cases/SKILL.md b/.cursor/skills/lab-cases/SKILL.md index 586f6e6..fe06c2d 100644 --- a/.cursor/skills/lab-cases/SKILL.md +++ b/.cursor/skills/lab-cases/SKILL.md @@ -34,7 +34,7 @@ List item shape: `prosthesisGroups: { prosthesisTypeCode, teeth[] }[]` from task ## Detail panel - Task assignment: `PATCH /cases/:caseId/tasks/:taskId/assign` (`TAB_CASES_EDIT`) -- Comments: shared `LabCaseCommentsPanel` + `tasksApi` comment routes +- Comments: shared `LabCaseCommentsPanel` + `tasksApi` comment routes (`viewerSide="LAB"`). Newest-first; sent/received use logical start/end alignment (RTL-safe). Compact `h-9` composer with primary send + visibility controls. - Mark read: `POST /notifications/mark-case-read` on select (Cases tab badge) - **Share link (sent cases):** `shareUrl` on detail; attachment preview left + QR thumb right; `LabCaseShareQrDialog` (`react-qr-code`). Full flow: `.cursor/skills/lab-case-share-link/SKILL.md`. diff --git a/.cursor/skills/lab-tasks/SKILL.md b/.cursor/skills/lab-tasks/SKILL.md index 425d0ba..3f0b73e 100644 --- a/.cursor/skills/lab-tasks/SKILL.md +++ b/.cursor/skills/lab-tasks/SKILL.md @@ -26,7 +26,7 @@ Other sorts use flat list on the frontend; `stepOrder asc` is still a tiebreaker 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}`). +**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). ## Prosthesis colors diff --git a/.cursor/skills/treatment-workspace/SKILL.md b/.cursor/skills/treatment-workspace/SKILL.md index ca981ea..5080a77 100644 --- a/.cursor/skills/treatment-workspace/SKILL.md +++ b/.cursor/skills/treatment-workspace/SKILL.md @@ -41,8 +41,8 @@ Right-column entry uses `WizardStepper` (`components/ui/shared/WizardStepper.tsx | Step | UI | Notes | |------|-----|--------| | **Teeth** | `FdiToothChart` | Shift+click connected range; Ctrl/⌘+click single range; linked dots | -| **Content** | `TreatmentDetailsEditor` fields only (`showChrome={false}`) | Type / notes / attachments — **no delete button** | -| **Lab** | `LabCasesDispatchPanel` | Shown in stepper **only** when active detail type is lab-dependent (`labDependentCodes`) | +| **Content** | `TreatmentDetailsEditor` fields only (`showChrome={false}`) | Type / **Notes** / attachments — **no delete button** | +| **Lab** | `LabCasesDispatchPanel` | Shown in stepper **only** when active detail type is lab-dependent (`labDependentCodes`). Entering Lab **auto-ensures** a shipment draft (no “Add lab shipment” click). Add detail stays ungated. Due date beside title (RTL via logical end). Prosthesis dropdown same-row from `md`. | - Detail type may differ from appointment purpose (purpose only defaults new details). - Next/Back navigate visible steps; leaving prosthesis while on Lab returns to Content. @@ -61,6 +61,7 @@ Helpers: `frontend/src/components/treatment/toothSelectionGroups.ts`. Persisted - Prevent browser selection artifacts (`select-none`, modifier `preventDefault` on mousedown). - On group change, prune/remap `labCase.toothProsthesis` via `pruneToothProsthesisForGroups`. - Connected UI label: `ConnectedSelectionBadge` (shared `Badge` + primary tint) in dispatch + lab case lists. +- Lab case comments: shared `LabCaseCommentsPanel` — newest-first; **sent** = `justify-start` / `text-start`, **received** = `justify-end` / `text-end` (RTL-safe); require `viewerSide: 'LAB' | 'CLINIC'`. Used in Cases, Tasks, share focus, and Treatment (`DetailLabCaseCommentsSection`). Composer: `h-9` input + primary send / visibility buttons (white icons; Send mirrored in RTL). @@ -123,12 +124,11 @@ Saved lab-dependent detail with **no teeth** can autosave but **cannot** create ## Lab case comments on details +Comments for a shipment live in the Lab dispatch panel (and Cases/Tasks/share), not on Content notes. - -Below each detail in the editor when the linked lab case is **in progress** (not all tasks completed): - -- `canCommentOnDetailLabCase(detail)` — requires `sentAt`, `labCaseId`, and `!isLabCaseCompleted(taskProgress)`. -- UI: `DetailLabCaseCommentsSection` → existing `LabCaseCommentsPanel` + `treatmentsApi` comment endpoints. +- Unsent: deferred composer in `DetailLabCaseCommentsSection` (posts with Send to lab). +- Sent / in progress: live composer until tasks complete (`canPostComments`). +- UI: `DetailLabCaseCommentsSection` → `LabCaseCommentsPanel` + `treatmentsApi` comment endpoints (`viewerSide="CLINIC"`). - Backend includes `tasks: { select: { id, status } }` on lab cases; `mapDetail` exposes `taskProgress: { completed, total }`. diff --git a/AGENTS.md b/AGENTS.md index a05d991..493d590 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,8 +45,9 @@ frontend/src/ **Treatment edit / details (quick ref):** - Day/mode gate: editable only for live draft on today/future (`canEditTreatmentForDay`). Past day / historical load → read-only form. - Sent-to-lab detail locks that line; **Add detail** still OK same day; **Remove detail** = trash icon on each detail chip (not in the wizard Content step) — only unsent and not the last line. Attachment upload blocked when sent (`TREATMENT_DETAIL_SENT`). -- **Entry wizard:** `WizardStepper` — Teeth → Content → Lab; Lab step only when active detail type is lab-dependent (prosthesis). Detail chips ≠ wizard chrome (chips stay bordered tabs; stepper is numbered rail). +- **Entry wizard:** `WizardStepper` — Teeth → Content → Lab; Lab step only when active detail type is lab-dependent (prosthesis); entering Lab auto-opens shipment draft (no Add-shipment CTA). Content notes field label is **Notes** (clinical). Detail chips ≠ wizard chrome. - **Tooth selection:** Shift+click same-arch range = connected group (linked dots); Ctrl/⌘+click same range = individual singles (one lab row each); plain click on a connected tooth collapses the whole span to that single tooth (no peel). Helpers: `toothSelectionGroups.ts`. Connected label: `ConnectedSelectionBadge` (shared `Badge` + primary tint). +- **Lab dispatch layout:** due date beside title (`justify-between`, logical start/end for RTL); prosthesis type on same row as teeth from `md:` up (stacked on mobile). **Treatment lab rules (quick ref):** - Lab-dependent details (e.g. prosthesis) **without teeth** can save but **cannot ship** — show `LabShipmentBlockedNotice` + inline banner; toast on dispatch add. @@ -54,7 +55,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 shipments rail**: unified list with scope toggle **This patient** vs **All updates** (unread across org for **this clinician's cases only**, includes patient name). - **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). -- **Lab shipment progress + comments**: shown in **Lab dispatch panel** for the active shipment; expanding activity / opening comments marks that case read. +- **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`. **Appointments (quick ref):** Do not delete (or change patient) when `hasTreatment`; codes `APPOINTMENT_HAS_TREATMENT` / `APPOINTMENT_PATIENT_LOCKED`. Past days: no new bookings; edit/delete OK without treatment; with treatment → toast. Appointment delete does not cascade-delete treatments. See `.cursor/rules/appointments.mdc`. diff --git a/frontend/messages/en.json b/frontend/messages/en.json index 950994c..9aa533e 100644 --- a/frontend/messages/en.json +++ b/frontend/messages/en.json @@ -669,8 +669,8 @@ "detailsSaveHint": "Lab dispatch is configured separately below.", "addCase": "Add case", "caseLabel": "Case {n}", - "comments": "Comments", - "commentsPlaceholder": "Write clinical notes for this case…", + "comments": "Notes", + "commentsPlaceholder": "Write clinical notes for this detail…", "treatmentType": "Treatment type", "treatmentTypePlaceholder": "Select treatment type…", "treatmentTypeNotSelected": "Type not selected", diff --git a/frontend/messages/fa.json b/frontend/messages/fa.json index 9c19539..6245a2d 100644 --- a/frontend/messages/fa.json +++ b/frontend/messages/fa.json @@ -670,8 +670,8 @@ "detailsSaveHint": "ارسال لاب در بخش جداگانه زیر پیکربندی می‌شود.", "addCase": "افزودن پرونده", "caseLabel": "پرونده {n}", - "comments": "نظرات", - "commentsPlaceholder": "یادداشت‌های بالینی این پرونده را بنویسید...", + "comments": "یادداشت‌ها", + "commentsPlaceholder": "یادداشت‌های بالینی این جزئیات را بنویسید…", "treatmentType": "نوع درمان", "treatmentTypePlaceholder": "نوع درمان را انتخاب کنید…", "treatmentTypeNotSelected": "نوع انتخاب نشده", diff --git a/frontend/messages/nl.json b/frontend/messages/nl.json index 7e8308b..c2b58fe 100644 --- a/frontend/messages/nl.json +++ b/frontend/messages/nl.json @@ -669,8 +669,8 @@ "detailsSaveHint": "Lab-dispatch wordt hieronder apart geconfigureerd.", "addCase": "Case toevoegen", "caseLabel": "Case {n}", - "comments": "Opmerkingen", - "commentsPlaceholder": "Schrijf klinische notities voor deze case...", + "comments": "Notities", + "commentsPlaceholder": "Schrijf klinische notities voor dit detail…", "treatmentType": "Behandeltype", "treatmentTypePlaceholder": "Selecteer behandeltype…", "treatmentTypeNotSelected": "Type niet geselecteerd", diff --git a/frontend/src/components/ui/lab/CaseTasksFocusView.tsx b/frontend/src/components/ui/lab/CaseTasksFocusView.tsx index 87931fc..9e656e3 100644 --- a/frontend/src/components/ui/lab/CaseTasksFocusView.tsx +++ b/frontend/src/components/ui/lab/CaseTasksFocusView.tsx @@ -182,6 +182,7 @@ export function CaseTasksFocusView({ token }: CaseTasksFocusViewProps) {
{ diff --git a/frontend/src/components/ui/lab/CasesPage.tsx b/frontend/src/components/ui/lab/CasesPage.tsx index c05fef4..2e3c124 100644 --- a/frontend/src/components/ui/lab/CasesPage.tsx +++ b/frontend/src/components/ui/lab/CasesPage.tsx @@ -535,6 +535,7 @@ export function CasesPage() {
{ diff --git a/frontend/src/components/ui/lab/LabCaseCommentsPanel.tsx b/frontend/src/components/ui/lab/LabCaseCommentsPanel.tsx index 8a50f62..679fb6d 100644 --- a/frontend/src/components/ui/lab/LabCaseCommentsPanel.tsx +++ b/frontend/src/components/ui/lab/LabCaseCommentsPanel.tsx @@ -1,14 +1,17 @@ 'use client'; -import { useCallback, useEffect, useState, type KeyboardEvent } from 'react'; +import { useCallback, useEffect, useMemo, useState, type KeyboardEvent } from 'react'; import { useTranslations } from 'next-intl'; import { Eye, EyeOff, Send } from 'lucide-react'; import { getUserFacingError } from '@/components/shared/formatApiError'; import { useAsyncActionById } from '@/lib/hooks/useAsyncAction'; import type { LabCaseComment } from '@/types/cases'; +export type LabCaseCommentViewerSide = 'LAB' | 'CLINIC'; + interface LabCaseCommentsPanelProps { caseId: string; + viewerSide: LabCaseCommentViewerSide; canPost: boolean; canToggleVisibility: boolean; loadComments: () => Promise; @@ -24,8 +27,15 @@ interface LabCaseCommentsPanelProps { onComposerValueChange?: (value: string) => void; } +function sortNewestFirst(items: LabCaseComment[]): LabCaseComment[] { + return [...items].sort( + (a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(), + ); +} + export function LabCaseCommentsPanel({ caseId, + viewerSide, canPost, canToggleVisibility, loadComments, @@ -45,17 +55,19 @@ export function LabCaseCommentsPanel({ const [visibleToClinic, setVisibleToClinic] = useState(false); const toggleBusy = useAsyncActionById(); + const orderedComments = useMemo(() => sortNewestFirst(comments), [comments]); + const refresh = useCallback(async () => { setLoading(true); try { const items = await loadComments(); - setComments(items); + setComments(sortNewestFirst(items)); } catch (error: unknown) { onError?.(getUserFacingError(error, tErrors, t('errorLoad'))); } finally { setLoading(false); } - }, [loadComments, onError, t]); + }, [loadComments, onError, t, tErrors]); useEffect(() => { void refresh(); @@ -67,7 +79,7 @@ export function LabCaseCommentsPanel({ setPosting(true); try { const created = await onPost(trimmed, visibleToClinic); - setComments((prev) => [...prev, created]); + setComments((prev) => sortNewestFirst([created, ...prev])); setBody(''); setVisibleToClinic(false); } catch (error: unknown) { @@ -96,8 +108,11 @@ export function LabCaseCommentsPanel({ }); } + const composerInputClass = + 'min-w-0 flex-1 h-9 rounded-md border border-border bg-surface px-3 py-1.5 text-sm leading-tight resize-none'; + return ( -
+

{t('title')}

{loading ? ( @@ -105,15 +120,22 @@ export function LabCaseCommentsPanel({ ) : comments.length === 0 ? (

{t('empty')}

) : ( -
    - {comments.map((comment) => ( -
  • -
    -
    -
    +
    + {orderedComments.map((comment) => { + const mine = comment.authorSide === viewerSide; + return ( +
    +
    +
    {comment.authorSide === 'LAB' ? t('labAuthor') : t('clinicAuthor')} {comment.authorName ? ` · ${comment.authorName}` : ''} @@ -125,32 +147,32 @@ export function LabCaseCommentsPanel({ {t('hiddenFromClinic')} ) ) : null} + {canToggleVisibility && comment.canToggleVisibility && onToggleVisibility ? ( + + ) : null}
    -

    {comment.body}

    +

    {comment.body}

    - {canToggleVisibility && comment.canToggleVisibility && onToggleVisibility ? ( - - ) : null}
    -
  • - ))} -
+ ); + })} +
)} {canPost && deferSubmit ? ( @@ -159,46 +181,48 @@ export function LabCaseCommentsPanel({ value={composerValue ?? ''} onChange={(e) => onComposerValueChange?.(e.target.value)} placeholder={t('placeholder')} - rows={2} - className="w-full rounded-md border border-border bg-surface px-3 py-2 text-sm resize-none" + rows={1} + className="w-full h-9 rounded-md border border-border bg-surface px-3 py-1.5 text-sm leading-tight resize-none" />
) : canPost ? ( -
+