From caf50a1b625e2c7ce6695dd5c52f51f49ac51372 Mon Sep 17 00:00:00 2001 From: Brian Smith Date: Mon, 7 Sep 2026 10:59:58 -0700 Subject: [PATCH] perf(charting): add Cmd+S instant sign hotkey, debounced localStorage draft auto-save, and rapid clinical macro chips --- src/components/charting/SoapChartEditor.tsx | 192 +++++++++++++++++++- 1 file changed, 185 insertions(+), 7 deletions(-) diff --git a/src/components/charting/SoapChartEditor.tsx b/src/components/charting/SoapChartEditor.tsx index 6a5fafa..da0afa3 100644 --- a/src/components/charting/SoapChartEditor.tsx +++ b/src/components/charting/SoapChartEditor.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useState } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import { SoapNote, Patient, @@ -88,6 +88,62 @@ export const SoapChartEditor: React.FC = ({ const [vasScore, setVasScore] = useState(initialSoapNote?.vasScore || 3); const [isSigned, setIsSigned] = useState(initialSoapNote?.status === 'signed'); const [signedTimestamp, setSignedTimestamp] = useState(initialSoapNote?.signedAt); + const [lastAutoSaved, setLastAutoSaved] = useState(null); + + // Restore draft from local storage if note is un-signed + useEffect(() => { + if (!initialSoapNote && typeof window !== 'undefined') { + try { + const key = `mediusa_draft_soap_${patient.id}`; + const raw = localStorage.getItem(key); + if (raw) { + const draft = JSON.parse(raw); + if (draft.subjective) setSubjective(draft.subjective); + if (draft.objective) setObjective(draft.objective); + if (draft.assessment) setAssessment(draft.assessment); + if (draft.plan) setPlan(draft.plan); + if (draft.vasScore !== undefined) setVasScore(draft.vasScore); + if (draft.adjustments && Array.isArray(draft.adjustments)) setAdjustments(draft.adjustments); + setLastAutoSaved(new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })); + } + } catch {} + } + }, [patient.id, initialSoapNote]); + + // Debounced auto-save draft to local storage + useEffect(() => { + if (isSigned) return; + const timer = setTimeout(() => { + if (typeof window !== 'undefined') { + try { + const key = `mediusa_draft_soap_${patient.id}`; + localStorage.setItem( + key, + JSON.stringify({ + subjective, + objective, + assessment, + plan, + vasScore, + adjustments, + timestamp: Date.now(), + }) + ); + setLastAutoSaved(new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })); + } catch {} + } + }, 800); + return () => clearTimeout(timer); + }, [patient.id, isSigned, subjective, objective, assessment, plan, vasScore, adjustments]); + + // Fast macro appender helper + const handleAppendMacro = (setter: React.Dispatch>, phrase: string) => { + clinicalAudio.playClick(); + setter((prev) => { + const trimmed = prev.trim(); + return trimmed ? `${trimmed} ${phrase}` : phrase; + }); + }; const handleApplyExtractedSoap = (data: { vasScore: number; @@ -154,12 +210,18 @@ export const SoapChartEditor: React.FC = ({ setPlan('1. Diversified adjustment delivered to indicated segments.\n2. Manual myofascial release 10 minutes.\n3. Continue active home rehabilitation.'); }; - const handleSignChart = () => { + const handleSignChart = useCallback(() => { clinicalAudio.playSuccess(); const now = new Date().toISOString(); setIsSigned(true); setSignedTimestamp(now); + if (typeof window !== 'undefined') { + try { + localStorage.removeItem(`mediusa_draft_soap_${patient.id}`); + } catch {} + } + const savedNote: SoapNote = { id: initialSoapNote?.id || `soap-${Date.now()}`, tenantId: patient.tenantId, @@ -183,7 +245,35 @@ export const SoapChartEditor: React.FC = ({ }; onSaveSoapNote(savedNote); - }; + }, [ + initialSoapNote, + patient, + provider, + discipline, + vasScore, + subjective, + objective, + assessment, + plan, + adjustments, + selectedIcd10, + selectedCpt, + onSaveSoapNote, + ]); + + // Global Cmd+S / Ctrl+S hotkey to instant-sign + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 's') { + e.preventDefault(); + if (!isSigned) { + handleSignChart(); + } + } + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [isSigned, handleSignChart]); const handleCreateSuperbillClick = () => { clinicalAudio.playSuccess(); @@ -244,9 +334,16 @@ export const SoapChartEditor: React.FC = ({ Signed & Locked ) : ( - - In Progress (Draft) - +
+ + In Progress (Draft) + + {lastAutoSaved && ( + + ✓ Auto-saved {lastAutoSaved} + + )} +
)} @@ -338,6 +435,7 @@ export const SoapChartEditor: React.FC = ({ type="button" onClick={handleSignChart} disabled={isSigned} + title={isSigned ? 'Chart Digitally Sealed' : 'Sign & Complete Note (Cmd+S / Ctrl+S)'} className={`px-4 py-2 rounded-lg text-xs font-bold flex items-center gap-1.5 transition shadow-xs ${ isSigned ? 'bg-emerald-50 text-emerald-700 border border-emerald-200 cursor-default' @@ -345,7 +443,7 @@ export const SoapChartEditor: React.FC = ({ }`} > - {isSigned ? 'Signed & Locked' : 'Sign & Complete Note'} + {isSigned ? 'Signed & Locked' : 'Sign & Complete (⌘S)'} + ))} + + )}
Auto-populated from Intake and Ambient Scribe. @@ -449,6 +567,26 @@ export const SoapChartEditor: React.FC = ({ onChange={(e) => setObjective(e.target.value)} className="w-full bg-slate-50 border border-slate-200 rounded-lg p-3 text-xs text-slate-800 focus:bg-white focus:outline-none focus:border-sky-500 resize-none" /> + {!isSigned && ( +
+ Quick: + {[ + 'Cervical active ROM restricted', + 'Lumbar paraspinal hypertonicity', + 'Negative Kemp test bilaterally', + 'Negative Straight Leg Raise', + ].map((chip) => ( + + ))} +
+ )}
Vertebrae selected in Spine Map sync automatically. @@ -473,6 +611,26 @@ export const SoapChartEditor: React.FC = ({ onChange={(e) => setAssessment(e.target.value)} className="w-full bg-slate-50 border border-slate-200 rounded-lg p-3 text-xs text-slate-800 focus:bg-white focus:outline-none focus:border-sky-500 resize-none" /> + {!isSigned && ( +
+ Quick: + {[ + 'Meeting care plan milestones', + 'Subluxation complex stable', + 'Antalgic lean resolved', + 'Spinal biomechanics progressing', + ].map((chip) => ( + + ))} +
+ )}
Mapped to ICD-10 diagnostic codes below. @@ -497,6 +655,26 @@ export const SoapChartEditor: React.FC = ({ onChange={(e) => setPlan(e.target.value)} className="w-full bg-slate-50 border border-slate-200 rounded-lg p-3 text-xs text-slate-800 focus:bg-white focus:outline-none focus:border-sky-500 resize-none" /> + {!isSigned && ( +
+ Quick: + {[ + 'Diversified adjustments delivered', + 'Home postural stabilization exercises', + 'Cryotherapy 15min post-treatment', + 'Return in 3 days for visit 4/12', + ].map((chip) => ( + + ))} +
+ )}
Populates CPT procedure line items on Superbill.