import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Check, ChevronDown, ChevronRight, Loader2, Pencil, StickyNote, TriangleAlert, Trash2 } from "lucide-react"; import { toast } from "sonner"; import { useAuthServiceContext, useOrgNoteServiceContext } from "../../services"; import type { OrgNote, NoteVisibility, PrivateNoteCreateOutcome } from "../../services/OrgNoteService.tsx"; import { useFhirPractitioners } from "../../hooks"; import { useKeyedRead } from "../../hooks/useKeyedRead.ts"; import { formatFhirHumanName } from "../../utils/fhirHumanName.tsx"; import { formatDate } from "../../utils/dateUtils.ts"; import { formatTimeForDisplay } from "../../lib/utils.ts"; import { Textarea } from "../ui/textarea.tsx"; import { Skeleton } from "../ui/skeleton.tsx"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "../ui/alert-dialog.tsx"; import { newNoteRequirements, describeRequirements } from "./newNoteRequirements"; import NoteVisibilitySelect, { type NoteVisibilityValue } from "./NoteVisibilitySelect.tsx"; import { readDraft, writeDraft, EMPTY_DRAFT } from "./patientNoteDraftStore.ts"; import { usePatientNoteDraftDurability, type PrivateNoteDraftStatus, } from "./usePatientNoteDraftDurability.ts"; import "./PatientNotes.css"; import { useSaveScope } from '../../utils/saveOperation'; import { viewerEpoch } from '../../utils/writeThroughHold'; import { heldPrivateNote, privateNoteRecoveryOwner, reservePrivateNote, releasePrivateNote, onPrivateNoteRecoveryChange, type PrivateNoteReference, type PrivateNoteRecoveryOwner } from './privateNoteCreateRecovery'; /** * What the autosave says about itself, in the CLINICAL NOTE'S OWN WORDS. * * The defect this feature answers is that the two tabs behaved oppositely, so saying the same three * things differently would leave half of it in place. `draft-failed` and `draft-conflict` must never be * quiet: an autosave makes a practitioner stop thinking about saving, and a failure they are not told * about is worse than no autosave at all - they have been taught to trust it, and they will close the tab. * * "Another window", never "someone else". The draft is keyed per author, so this can only ever be the * same practitioner in two places; blaming a colleague would send them chasing a person who is not there. */ const DRAFT_LABEL: Record = { idle: null, loading: null, "draft-saving": Saving draft…, "draft-saved": Draft saved · not posted yet, "draft-failed": Draft NOT saved — this note only exists in this browser. Use Save before leaving., "draft-conflict": This note was changed in another window. Autosave is off — copy your text before leaving., }; interface PatientNotesProps { patientId: string; /** The front desk's Quick View draws the composer, its outcome and the list as its deck does. */ /** The staff Record's opened event draws each note as the deck's tinted note, under its own heading. */ /** In a call the private band above says who sees the notes, so they carry no disclaimer of their own. */ variant?: "desk" | "record" | "meeting"; } const EMPTY_VISIBILITY: NoteVisibilityValue = { mode: "", categoryLabels: [] }; // The picker only ever emits { mode, categoryLabels }; the service owns the label -> roleCode // expansion (Approach 3 in the design doc - the server never sees categories, only expanded codes). const toWireVisibility = ( value: NoteVisibilityValue, expandCategoriesToRoleCodes: (categoryLabels: string[]) => string[], ): NoteVisibility => { // Save is gated on a real choice, so "" never actually reaches the server; default it defensively. const mode = value.mode === "" ? "me-only" : value.mode; return { mode, roleCodes: mode === "roles" ? expandCategoriesToRoleCodes(value.categoryLabels) : [], }; }; const visibilityLabel = ( note: OrgNote, getVisibilityCategoriesForNote: (note: OrgNote) => string[], ): string => { if (note.visibility.mode === "me-only") return "Me only"; if (note.visibility.mode === "all-staff") return "All staff"; const labels = getVisibilityCategoriesForNote(note); return labels.length > 0 ? labels.join(", ") : "Specific role categories"; }; const formatNoteTimestamp = (value?: string): string => { if (!value) return ""; return [formatDate(value), formatTimeForDisplay(value)].filter(Boolean).join(" · "); }; // Org-internal patient notes: add + list + expand-in-place + author edit/delete. Shared by the // receptionist workspace modal and the practitioner chart (Task 11 wires it in - this component // is deliberately standalone, not wired into either surface yet). Mirrors the in-place expand // pattern used elsewhere in the receptionist workspace (WorkspaceAppointments/WorkspaceTaskDetail) // - no nested Sheet/Dialog for the detail view, only the delete confirm uses an overlay // (AlertDialog), same as the established delete-confirm pattern (DeviceUseStatements.tsx). const PatientNotes = ({ patientId, variant }: PatientNotesProps) => { const desk = variant === "desk"; const { fetchNotes, createNote, checkNoteCreate, closeNoteCreate, updateNote, deleteNote, expandCategoriesToRoleCodes, getVisibilityCategoriesForNote, } = useOrgNoteServiceContext(); const { currentUser, currentOrganization } = useAuthServiceContext(); const { practitionersList } = useFhirPractitioners(); // - AN ORG-LESS AUTHOR HAS NO VISIBILITY CONTROL AT ALL, and that is stronger than // "a picker with two entries filtered out". Spencer, 2026-08-07: *"Hide the 'all staff' and 'by // role' options... Shouldn't even be a select option for that at all for orgless practitioners // since it's always just 'me only'."* A filtered picker would still imply those modes exist for // them; the control is ABSENT, and `me-only` is not a default they could change. // // An invited outside collaborator has no organization by construction. `all-staff` and `roles` // both expand to ORGANIZATION role codes, so they are meaningless - not merely unavailable - for // an author with no org, and the server REFUSES them (403) rather than coercing. This is the UI // half of that rule, not the enforcement: hiding a control is never a security boundary. // infra `utils/orgNote.ts` isVisibilityModeAllowedForScope. const hasOrg = Boolean(currentOrganization?.id); const ORG_LESS_VISIBILITY: NoteVisibilityValue = { mode: "me-only", categoryLabels: [] }; const currentUserPractitionerId = currentUser?.fhir?.resourceType === "Practitioner" ? currentUser.fhir.id : undefined; const noteOwner = JSON.stringify([currentUserPractitionerId, currentOrganization?.id ?? '', patientId]); const lifetimeKey = `${noteOwner}:${viewerEpoch()}`; const saveScope = useSaveScope(lifetimeKey); const liveOwner = useRef(noteOwner); liveOwner.current = lifetimeKey; const composerContinuation = useRef({ lifetime: lifetimeKey, generation: 0 }); if (composerContinuation.current.lifetime !== lifetimeKey) { composerContinuation.current = { lifetime: lifetimeKey, generation: composerContinuation.current.generation + 1 }; } useEffect(() => () => { composerContinuation.current.generation += 1; }, []); const createBusy = useRef(false); const activeCreate = useRef<{ owner: PrivateNoteRecoveryOwner; ref?: PrivateNoteReference; sent: boolean }>(); const [recoveryAttempt, setRecoveryAttempt] = useState(0); const [recovery, setRecovery] = useState<{ ready: boolean; owner?: PrivateNoteRecoveryOwner; ref?: PrivateNoteReference; outcome?: PrivateNoteCreateOutcome; notice?: string }>({ ready: false }); useEffect(() => { const op = saveScope.start(10_000); createBusy.current = false; setCreating(false); setRecovery({ ready: false }); void op.step(() => privateNoteRecoveryOwner(currentUserPractitionerId ?? '', currentOrganization?.id ? `organization/${currentOrganization.id}` : `practitioner/${currentUserPractitionerId}`, patientId)) .then((owner) => { op.admit(); const ref = heldPrivateNote(owner); setRecovery({ ready: true, owner, ref, ...(ref && Date.now() - ref.firstAt > 86_400_000 ? { notice: 'An older note save is still unresolved. Check or stop it; age does not prove it was unsaved.' } : {}) }); }) .catch(() => { if (op.belongs()) setRecovery({ ready: false, notice: 'Note recovery storage is unavailable. No new save was sent.' }); }); return () => op.retire(); }, [lifetimeKey, saveScope, recoveryAttempt]); useEffect(() => { if (!recovery.owner) return; const owner = recovery.owner; return onPrivateNoteRecoveryChange(owner, () => { try { const ref = heldPrivateNote(owner); setRecovery(previous => previous.owner !== owner ? previous : { ...previous, ready: true, ref, ...(previous.ref?.operationId !== ref?.operationId ? { outcome: undefined, notice: undefined } : {}) }); } catch { setRecovery(previous => previous.owner !== owner ? previous : { ...previous, ready: false, notice: 'Note recovery storage is unavailable. Your draft and earlier save reference are retained.' }); } }); }, [recovery.owner]); // Notes are read for the patient on screen; an answer or failure for anyone else never shows. const readNotes = useCallback(() => fetchNotes(patientId), [fetchNotes, patientId, lifetimeKey]); const read = useKeyedRead(patientId ? lifetimeKey : undefined, readNotes); // Saves, edits and deletes since that read, stamped with the patient they were made for. const [changes, setChanges] = useState<{ owner: string; apply: ((list: OrgNote[]) => OrgNote[])[] }>(); const changeNotes = (owner: string, apply: (list: OrgNote[]) => OrgNote[]) => setChanges((prev) => ({ owner, apply: [...(prev?.owner === owner ? prev.apply : []), apply] })); const loading = read.status === "idle" || read.status === "pending"; const error = read.status === "failed" ? "Unable to load notes. Please try again." : undefined; const notes = useMemo(() => { if (read.status !== "ready") return []; const mine = changes?.owner === lifetimeKey ? changes.apply : []; return mine.reduce((list, apply) => apply(list), read.value); }, [read, changes, lifetimeKey]); useEffect(() => { if (read.status === "failed") console.error("Failed to load patient notes:", read.error); }, [read]); // - everything the user has TYPED is seeded from, and mirrored back to, an in-memory draft // store keyed by patient, so it survives this component being remounted. floating notes pane // remounts it three ways mid-visit (pop out, dock back, minimise), and before this each one silently // discarded the text. In memory only - note text is PHI and there is none at rest. const [seed] = useState(() => readDraft(noteOwner) ?? EMPTY_DRAFT); // Add area (top). const [newBody, setNewBody] = useState(seed.newBody); const [newVisibility, setNewVisibility] = useState(seed.newVisibility); const [creating, setCreating] = useState(false); // The desk says a save's outcome beside the composer rather than in a toast. const [saveFailed, setSaveFailed] = useState(false); const [savedIds, setSavedIds] = useState([]); // A save answered after the Quick View moved to another patient belongs to nobody on screen. const shownPatient = useRef(patientId); shownPatient.current = patientId; // Add note is collapsed until asked for; collapsing keeps the draft and audience. A recovered // draft opens it, so text somebody wrote is never hidden behind a closed control. const [composerOpen, setComposerOpen] = useState(() => Boolean(seed.newBody.trim())); // Expand-in-place + inline author edit. const [expandedId, setExpandedId] = useState(""); const [editingId, setEditingId] = useState(seed.editingId); const [editBody, setEditBody] = useState(seed.editBody); const [editVisibility, setEditVisibility] = useState(seed.editVisibility); const [savingEdit, setSavingEdit] = useState(false); // SWAP THE DRAFT DURING RENDER WHEN THE PATIENT CHANGES, NOT IN AN EFFECT. // This component is reused across patients without unmounting (the chart route swaps the prop), and an // effect runs AFTER the commit - so for one painted frame the previous patient's note text would sit in // the editor under the new patient's heading. That is the same defect class as Critical, // where stale state outlived a prop change. Adjusting state during render is React's sanctioned pattern // for exactly this: it re-renders before committing, so the wrong frame is never shown. const [draftOwner, setDraftOwner] = useState(lifetimeKey); if (draftOwner !== lifetimeKey) { const next = readDraft(noteOwner) ?? EMPTY_DRAFT; setDraftOwner(lifetimeKey); setRecovery({ ready: false }); createBusy.current = false; setNewBody(next.newBody); setNewVisibility(next.newVisibility); setComposerOpen(Boolean(next.newBody.trim())); setEditingId(next.editingId); setEditBody(next.editBody); setEditVisibility(next.editVisibility); setCreating(false); setSaveFailed(false); setSavedIds([]); setChanges(undefined); } useEffect(() => { // No owner guard here, deliberately. The render-time swap above runs BEFORE this commits, so // `patientId` and the five values below always belong to the same patient by the time this fires - // which is precisely what makes a cross-patient write impossible. A guard was tried and removed: // mutation testing showed no test could reach it, because the swap makes it unreachable by // construction. If anyone ever moves that swap into an effect, this write becomes unsafe and no // guard here would save it - the swap is the invariant, not a check on top of it. writeDraft(noteOwner, { newBody, newVisibility, editingId, editBody, editVisibility }); }, [noteOwner, patientId, newBody, newVisibility, editingId, editBody, editVisibility]); // AND THE SAME DRAFT, DURABLY. The in-memory mirror above solves the remount; this survives a // refresh, a crash and a different workstation - which is what the clinical note one tab across has // always done, and the reason a practitioner who learned that tab lost work in this one. const durability = usePatientNoteDraftDurability( patientId, { newBody, newVisibility, editingId, editBody, editVisibility }, { key: lifetimeKey, organizationId: currentOrganization?.id, applicable: hasOrg, admit: () => { if (liveOwner.current !== lifetimeKey) throw new Error('The draft owner changed.'); } }, ); /* * A STORED DRAFT IS ADOPTED ONLY OVER AN EMPTY EDITOR, and this is the rule that keeps the feature * from causing the loss it prevents. Something typed since the mount is newer than anything the * server held, and overwriting it would be this component destroying work by restoring work. * * The visibility comes back WITH the body. A resumed draft that restored the text and defaulted the * visibility would bring a note set to `me only` back as `all staff`, and nobody re-checks a field * they already answered. */ const [adoptedFor, setAdoptedFor] = useState(); useEffect(() => { if (!durability.restored || adoptedFor === lifetimeKey) return; setAdoptedFor(lifetimeKey); if (newBody.trim() !== "" || editBody.trim() !== "") return; setNewBody(durability.restored.newBody); setNewVisibility(durability.restored.newVisibility); if (durability.restored.newBody.trim()) setComposerOpen(true); setEditingId(durability.restored.editingId); setEditBody(durability.restored.editBody); setEditVisibility(durability.restored.editVisibility); }, [durability.restored, lifetimeKey, adoptedFor, newBody, editBody]); // Confirm-before-delete (house rule: never delete on first click). const [deleteTarget, setDeleteTarget] = useState(null); const [deleting, setDeleting] = useState(false); const sortedNotes = useMemo( () => [...notes].sort((a, b) => { const aTime = a.createdAt ? new Date(a.createdAt).getTime() : 0; const bTime = b.createdAt ? new Date(b.createdAt).getTime() : 0; return bTime - aTime; }), [notes], ); // Practitioner id -> display name. Staff names are not PHI, so this is fine even in the // receptionist (PHI-limited) view. Falls back to a neutral label when the author can't be // resolved (directory miss, or a note with no author on record). const resolveAuthorName = useCallback( (authorId?: string): string => { if (!authorId) return "Staff member"; const match = practitionersList.find((entry) => entry.practitioner.id === authorId); if (!match) return "Staff member"; return formatFhirHumanName(match.practitioner.name) || "Staff member"; }, [practitionersList], ); // ONE LIST DRIVES THE BUTTON AND THE SENTENCE BENEATH IT. Walking C9 on 2026-09-07 found Save // disabled with no title, no message and no marker on the control it was waiting for: an author // could type a note, look at a greyed button, and have nothing on screen say that choosing an // audience was the missing step. The rule was right and only its statement was missing. // Deriving `canSaveNewNote` from the list rather than restating it is what keeps the two from // drifting - a second copy of the rule is how the message and the button start disagreeing. const newNoteMissing = newNoteRequirements(newBody, newVisibility, hasOrg); const canSaveNewNote = newNoteMissing.length === 0; const newRevision = JSON.stringify([newBody, newVisibility]); const currentRevision = useRef({ value: newRevision, generation: 0 }); if (currentRevision.current.value !== newRevision) currentRevision.current = { value: newRevision, generation: currentRevision.current.generation + 1 }; const applyCreateOutcome = async (outcome: PrivateNoteCreateOutcome, reference: PrivateNoteReference, owner: PrivateNoteRecoveryOwner, submittedRevision: number | undefined, op: ReturnType) => { op.admit(); setRecovery({ ready: true, owner, ref: reference, outcome }); if (outcome.state === 'closed') { await releasePrivateNote(owner, reference.operationId); op.admit(); setRecovery({ ready: true, owner, notice: 'The earlier save was stopped. Your current draft is retained.' }); return; } // The envelope can acknowledge a deleted note without supplying stale prose for the list. if (outcome.note) changeNotes(lifetimeKey, (list) => [outcome.note!, ...list.filter((n) => n.id !== outcome.noteId)]); setSavedIds((prev) => [...new Set([outcome.noteId, ...prev])]); if (outcome.evidencePending) { setRecovery({ ready: true, owner, ref: reference, outcome, notice: 'Note saved. Clinical evidence is pending. Check its outcome before starting another note.' }); return; } const sameLocal = submittedRevision !== undefined && currentRevision.current.generation === submittedRevision; let cleaned = !hasOrg; if (reference.cleanup) { const result = await durability.cleanupSubmitted(reference.cleanup); op.admit(); cleaned = result === 'removed' || result === 'already-absent' || result === 'preserved'; } const mayClear = sameLocal && (!hasOrg || (reference.cleanup && durability.isSubmittedCurrent(reference.cleanup))); if (mayClear) { setNewBody(''); setNewVisibility(EMPTY_VISIBILITY); setComposerOpen(false); } const retainedText = (JSON.parse(currentRevision.current.value)[0] as string).trim().length > 0; if (cleaned && (mayClear || !retainedText)) { await releasePrivateNote(owner, reference.operationId); op.admit(); setRecovery({ ready: true, owner, notice: outcome.current === 'deleted' ? 'The earlier note was saved and later deleted.' : 'Note saved.' }); } else { setRecovery({ ready: true, owner, ref: reference, outcome, notice: 'Note saved. Draft cleanup is pending; retained text may already be submitted. Check before starting a new note.' }); } }; const handleCreateNote = async () => { if (!canSaveNewNote || createBusy.current || !recovery.ready || !recovery.owner || recovery.ref) return; createBusy.current = true; const owner = recovery.owner; const op = saveScope.start(10_000); const submittedRevision = currentRevision.current.generation; let reference: PrivateNoteReference | undefined; let sent = false; let known: PrivateNoteCreateOutcome | undefined; const active = { owner, sent: false } as NonNullable; activeCreate.current = active; setCreating(true); setSaveFailed(false); try { const frozen = await op.step(() => durability.freezeSubmitted({ newBody, newVisibility, editingId, editBody, editVisibility })); op.admit(); reference = await reservePrivateNote(owner, frozen.state === 'acknowledged' ? frozen.cleanup : undefined, op.admit); op.admit(); active.ref = reference; setRecovery({ ready: true, owner, ref: reference }); const outcome = await op.step(() => createNote({ operationId: reference!.operationId, patientId, body: newBody.trim(), visibility: toWireVisibility(hasOrg ? newVisibility : ORG_LESS_VISIBILITY, expandCategoriesToRoleCodes), }, { ...op.transport, authorId: currentUserPractitionerId ?? '', organizationId: currentOrganization?.id, bypassOrganization: !hasOrg, onSend: () => { op.admit(); if (heldPrivateNote(owner)?.operationId !== reference!.operationId) throw new Error('The note recovery reference changed before dispatch.'); sent = true; active.sent = true; op.transport.onSend?.(); } }), true); known = outcome; await applyCreateOutcome(outcome, reference, owner, submittedRevision, op); } catch { if (reference && !sent) { try { await releasePrivateNote(owner, reference.operationId); } catch { /* Keep unreadable storage blocked. */ } } if (op.belongs()) { setSaveFailed(!sent); try { setRecovery({ ready: true, owner, ref: sent ? reference : heldPrivateNote(owner), ...(known ? { outcome: known } : {}), notice: known?.state === 'committed' ? 'Note saved. Draft cleanup is pending; review retained text before starting a new note.' : sent ? 'The note save could not be confirmed. Check its outcome or stop this save before starting another.' : 'No note save was sent. Your draft is retained.' }); } catch { setRecovery({ ready: false, notice: 'Note recovery storage is unavailable. Your draft is retained.' }); } } } finally { durability.resume(); if (activeCreate.current === active) activeCreate.current = undefined; if (op.belongs()) { createBusy.current = false; setCreating(false); } } }; const recoverCreate = async (action: 'check' | 'close') => { if (createBusy.current || !recovery.owner || !recovery.ref) return; const { owner, ref: reference } = recovery; createBusy.current = true; const op = saveScope.start(10_000); setCreating(true); let known: PrivateNoteCreateOutcome | undefined; try { const transport = { ...op.transport, authorId: currentUserPractitionerId ?? '', organizationId: currentOrganization?.id, bypassOrganization: !hasOrg }; const outcome = await op.step(() => action === 'check' ? checkNoteCreate(patientId, reference.operationId, transport) : closeNoteCreate(patientId, reference.operationId, transport), action === 'close'); known = outcome; await applyCreateOutcome(outcome, reference, owner, undefined, op); } catch { const retained = known ?? recovery.outcome; if (op.belongs()) setRecovery({ ready: true, owner, ref: reference, ...(retained ? { outcome: retained } : {}), notice: retained?.state === 'committed' && retained.evidencePending ? 'Note saved. Clinical evidence is pending. Check its outcome before starting another note.' : retained?.state === 'committed' ? 'Note saved. Draft cleanup is pending; review retained text before starting a new note.' : retained?.state === 'closed' ? 'The earlier save was stopped. Recovery reference cleanup is pending.' : 'The earlier save is still unresolved. Your draft and its recovery reference are retained.' }); } finally { if (op.belongs()) { createBusy.current = false; setCreating(false); } } }; const acknowledgeNewIntent = async () => { if (recovery.outcome?.state !== 'committed' || recovery.outcome.evidencePending || !recovery.owner || !recovery.ref) return; const op = saveScope.start(10_000); try { await op.step(() => releasePrivateNote(recovery.owner!, recovery.ref!.operationId)); op.admit(); setRecovery({ ready: true, owner: recovery.owner, notice: 'Earlier completion reviewed. Saving this draft will create a distinct new note.' }); } catch { if (op.belongs()) setRecovery((r) => ({ ...r, notice: 'The earlier completion could not be released from recovery storage.' })); } }; const toggleComposer = async () => { const generation = ++composerContinuation.current.generation; const belongs = () => liveOwner.current === lifetimeKey && composerContinuation.current.generation === generation; if (creating) { saveScope.renew(); const active = activeCreate.current; if (active && !active.sent) { try { if (active.ref) await releasePrivateNote(active.owner, active.ref.operationId); if (!belongs()) return; setRecovery({ ready: true, owner: active.owner }); } catch { if (!belongs()) return; setRecovery({ ready: false, notice: 'Note recovery storage is unavailable. Your draft is retained.' }); } } } if (!belongs()) return; createBusy.current = false; setCreating(false); setComposerOpen((open) => !open); }; const toggleShown = (noteId: string) => { setExpandedId((current) => (current === noteId ? "" : noteId)); }; const startEdit = (note: OrgNote) => { setEditingId(note.id); setEditBody(note.body); setEditVisibility({ mode: note.visibility.mode, categoryLabels: getVisibilityCategoriesForNote(note), }); }; // The same list as the composer above, for the same reason. An existing note already has a mode, // so the audience arm never fires here - but "Specific role categories" with none named does, and // it is the more confusing of the two: the author changes the audience to a narrower one, names // nobody, and the button dies without a word. const editMissing = newNoteRequirements(editBody, editVisibility, hasOrg); const canSaveEdit = editMissing.length === 0; const handleSaveEdit = async (note: OrgNote) => { if (!canSaveEdit || savingEdit) return; const owner = lifetimeKey; setSavingEdit(true); try { const updated = await updateNote({ ...note, body: editBody.trim(), // - the same rule on EDIT. The server enforces it on PUT too, because a rule // applied only at creation is one edit away from being escaped. visibility: toWireVisibility( hasOrg ? editVisibility : ORG_LESS_VISIBILITY, expandCategoriesToRoleCodes, ), }); // Optimistic replace (see create) - avoid the eventually-consistent refetch. changeNotes(owner, (list) => list.map((existing) => (existing.id === updated.id ? updated : existing))); setEditingId(undefined); } catch (err) { console.error("Failed to update note:", err); toast.error("Could not update the note. Please try again."); } finally { setSavingEdit(false); } }; const handleConfirmDelete = async () => { if (!deleteTarget || deleting) return; const owner = lifetimeKey; setDeleting(true); try { await deleteNote(deleteTarget); const deletedId = deleteTarget.id; // Optimistic remove (see create) - avoid the eventually-consistent refetch. changeNotes(owner, (list) => list.filter((existing) => existing.id !== deletedId)); setDeleteTarget(null); if (expandedId === deletedId) setExpandedId(""); if (editingId === deletedId) setEditingId(undefined); } catch (err) { console.error("Failed to delete note:", err); toast.error("Could not delete the note. Please try again."); } finally { setDeleting(false); } }; // The desk shows the latest note and those saved this visit; the rest sit behind View all notes. const visibleNotes = desk ? sortedNotes.slice(0, 1 + savedIds.length) : sortedNotes; const olderNotes = desk ? sortedNotes.slice(1 + savedIds.length) : []; const audienceSummary = (value: NoteVisibilityValue): string => { const practice = currentOrganization?.name ? ` in ${currentOrganization.name}` : ""; if (value.mode === "all-staff") return `All staff${practice}`; if (value.mode === "me-only") return "Only you"; if (value.mode === "roles") return `${value.categoryLabels.join(", ")}${practice}`; return ""; }; const noteItem = (note: OrgNote) => { const isAuthor = Boolean(currentUserPractitionerId) && note.authorId === currentUserPractitionerId; const isEditing = editingId === note.id; const isShown = expandedId === note.id; const itemClass = desk ? "ahr-patient-notes__item ahr-list-card" : "ahr-patient-notes__item"; if (isEditing) { return (