feat: polish UX +5% with Cmd+K Command Palette, Web Audio tactile feedback, Patient Clinical Vitals HUD, Fast Macros & Toasts

This commit is contained in:
2026-09-05 15:44:58 -07:00
parent dd5057d4e3
commit bfe527fb33
9 changed files with 1191 additions and 21 deletions
+155 -5
View File
@@ -35,6 +35,9 @@ import { RetailInventoryView } from '@/components/pos/RetailInventoryView';
import { MembershipsView } from '@/components/memberships/MembershipsView';
import { TelehealthRoom } from '@/components/telehealth/TelehealthRoom';
import { WaitlistModal } from '@/components/waitlist/WaitlistModal';
import { CommandPalette } from '@/components/ui/CommandPalette';
import { ClinicalToastContainer, ToastMessage } from '@/components/ui/ClinicalToast';
import { clinicalAudio } from '@/lib/clinical-audio';
import {
Calendar,
FileText,
@@ -51,6 +54,10 @@ import {
Shield,
ShieldAlert,
UserCheck,
Search,
Volume2,
VolumeX,
Sparkles,
} from 'lucide-react';
export default function Home() {
@@ -78,6 +85,11 @@ export default function Home() {
const [waitlist, setWaitlist] = useState<WaitlistEntry[]>(INITIAL_WAITLIST);
const [isWaitlistOpen, setIsWaitlistOpen] = useState(false);
// Polish state: Command Palette, Toasts & Audio
const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false);
const [toasts, setToasts] = useState<ToastMessage[]>([]);
const [audioEnabled, setAudioEnabled] = useState(true);
const [activePatient, setActivePatient] = useState<Patient>(INITIAL_PATIENTS[0]);
const [activeSoapNote, setActiveSoapNote] = useState<SoapNote | undefined>(INITIAL_SOAP_NOTES[0]);
const [activeSuperbill, setActiveSuperbill] = useState<Superbill>(INITIAL_SUPERBILLS[0]);
@@ -86,7 +98,52 @@ export default function Home() {
const activeProviders = INITIAL_PROVIDERS.filter((p) => p.tenantId === activeTenant.id);
const activeProvider = activeProviders[0] || INITIAL_PROVIDERS[0];
// Toast notification helper
const addToast = (type: 'success' | 'alert' | 'info', title: string, description?: string) => {
const id = `toast-${Date.now()}-${Math.random().toString(36).substring(2, 6)}`;
const newToast: ToastMessage = {
id,
type,
title,
description,
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
};
setToasts((prev) => [...prev, newToast]);
setTimeout(() => {
setToasts((prev) => prev.filter((t) => t.id !== id));
}, 4500);
};
const handleDismissToast = (id: string) => {
setToasts((prev) => prev.filter((t) => t.id !== id));
};
// Keyboard shortcut listener for Cmd+K / Ctrl+K
React.useEffect(() => {
const handleGlobalKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault();
setIsCommandPaletteOpen((prev) => !prev);
}
};
window.addEventListener('keydown', handleGlobalKeyDown);
return () => window.removeEventListener('keydown', handleGlobalKeyDown);
}, []);
const handleToggleAudio = () => {
const next = !audioEnabled;
setAudioEnabled(next);
clinicalAudio.setEnabled(next);
if (next) {
clinicalAudio.playSuccess();
addToast('info', 'Tactile Audio Active', 'Synthesized Web Audio clicks & chimes enabled');
} else {
addToast('info', 'Tactile Audio Muted', 'Sound effects silenced');
}
};
const handleSelectAppointment = (apt: Appointment) => {
clinicalAudio.playClick();
const patientMatch = patients.find((p) => p.id === apt.patientId) || patients[0];
setActivePatient(patientMatch);
@@ -101,12 +158,15 @@ export default function Home() {
};
const handleUpdateAppointmentStatus = (aptId: string, status: Appointment['status']) => {
clinicalAudio.playClick();
setAppointments((prev) =>
prev.map((a) => (a.id === aptId ? { ...a, status } : a))
);
addToast('info', 'Encounter Status Updated', `Appointment transitioned to ${status}`);
};
const handleSaveSoapNote = (note: SoapNote) => {
clinicalAudio.playSuccess();
setSoapNotes((prev) => {
const idx = prev.findIndex((s) => s.id === note.id);
if (idx >= 0) {
@@ -117,9 +177,11 @@ export default function Home() {
return [...prev, note];
});
setActiveSoapNote(note);
addToast('success', 'Chart Synced to HIPAA Vault', `${note.patientName}${note.status === 'signed' ? 'Signed & Locked' : 'Draft Saved'}`);
};
const handleGenerateSuperbillFromNote = (note: SoapNote) => {
clinicalAudio.playSuccess();
const newSb: Superbill = {
id: `sb-${Date.now()}`,
tenantId: activeTenant.id,
@@ -153,9 +215,11 @@ export default function Home() {
setSuperbills((prev) => [newSb, ...prev]);
setActiveSuperbill(newSb);
setClinicTab('billing');
addToast('success', 'Superbill Claim Created', `${newSb.invoiceNumber}$${newSb.totalAmount.toFixed(2)} Fee Schedule`);
};
const handleMarkPaid = (superbillId: string, method: Superbill['paymentMethod']) => {
clinicalAudio.playSuccess();
setSuperbills((prev) =>
prev.map((sb) =>
sb.id === superbillId
@@ -171,9 +235,11 @@ export default function Home() {
paymentMethod: method,
}));
}
addToast('success', 'Stripe Payment Captured', `Receipt processed via ${method}`);
};
const handlePatientBookingComplete = (newAptData: any) => {
clinicalAudio.playSuccess();
const newApt: Appointment = {
id: `apt-${Date.now()}`,
tenantId: activeTenant.id,
@@ -181,34 +247,44 @@ export default function Home() {
...newAptData,
};
setAppointments((prev) => [newApt, ...prev]);
addToast('success', 'Intake & Booking Confirmed', `${newApt.patientName} scheduled for ${newApt.time}`);
};
const handleAddTenant = (newTenant: ClinicTenant) => {
setTenants((prev) => [...prev, newTenant]);
addToast('success', 'New Clinic Deployed', `${newTenant.name} onboarded to Mediusa OS`);
};
const handleSwitchTenant = (tenantId: string) => {
clinicalAudio.playClick();
setActiveTenantId(tenantId);
setPortalMode('clinic');
setClinicTab('calendar');
};
const handleUpdateProductStock = (productId: string, newStock: number) => {
clinicalAudio.playClick();
setProducts((prev) =>
prev.map((p) => (p.id === productId ? { ...p, stockQty: newStock } : p))
);
addToast('info', 'POS Inventory Updated', `Stock quantity adjusted to ${newStock}`);
};
const handleEnrollPatientInMembership = (patientId: string, planName: string) => {
clinicalAudio.playSuccess();
setPatients((prev) =>
prev.map((p) => (p.id === patientId ? { ...p, activeMembership: planName } : p))
);
addToast('success', 'Membership Enrolled', `Patient subscribed to ${planName}`);
};
const handleAutoFillWaitlistPatient = (entryId: string) => {
clinicalAudio.playPing();
const entry = waitlist.find((w) => w.id === entryId);
setWaitlist((prev) =>
prev.map((w) => (w.id === entryId ? { ...w, status: 'notified' } : w))
);
addToast('alert', 'Waitlist SMS Dispatched', `Slot opening sent to ${entry?.patientName || 'patient'}`);
};
return (
@@ -226,7 +302,7 @@ export default function Home() {
<div className="text-xs font-black tracking-tight text-slate-900 flex items-center gap-1.5">
<span>MEDIUSA CLINIC OS</span>
<span className="text-[10px] px-2 py-0.5 rounded-full bg-sky-100 text-sky-800 font-bold border border-sky-200">
CLINICAL v2.4
CLINICAL v2.5
</span>
</div>
<div className="text-xs text-slate-500 font-mono flex items-center gap-1 mt-0.5">
@@ -236,6 +312,36 @@ export default function Home() {
</div>
</div>
{/* Quick Command Palette Trigger (Cmd+K) */}
<button
type="button"
onClick={() => {
clinicalAudio.playClick();
setIsCommandPaletteOpen(true);
}}
className="hidden xl:flex items-center gap-2 px-3 py-1.5 bg-slate-50 hover:bg-slate-100 border border-slate-200 rounded-lg text-slate-500 text-xs transition shadow-2xs"
>
<Search className="w-3.5 h-3.5 text-slate-400" />
<span>Search patients or actions...</span>
<kbd className="px-1.5 py-0.5 text-[9px] font-mono bg-white border border-slate-200 rounded text-slate-500 font-bold">
K
</kbd>
</button>
{/* Audio Toggle Button */}
<button
type="button"
onClick={handleToggleAudio}
title={audioEnabled ? 'Tactile Audio Active (Click to Mute)' : 'Tactile Audio Muted (Click to Enable)'}
className={`p-1.5 rounded-lg border text-xs transition flex items-center gap-1 shadow-2xs ${
audioEnabled
? 'bg-sky-50 border-sky-200 text-sky-700 hover:bg-sky-100'
: 'bg-slate-100 border-slate-200 text-slate-400 hover:bg-slate-200'
}`}
>
{audioEnabled ? <Volume2 className="w-4 h-4" /> : <VolumeX className="w-4 h-4" />}
</button>
{/* Clinic Dropdown */}
<div className="relative">
<select
@@ -257,7 +363,11 @@ export default function Home() {
<span className="text-[10px] uppercase font-bold text-slate-500 px-1.5">Role:</span>
<button
type="button"
onClick={() => setStaffRole('doctor')}
onClick={() => {
clinicalAudio.playClick();
setStaffRole('doctor');
addToast('info', 'Role: Attending Doctor (D.C.)', 'Full clinical SOAP, 2D spine & telehealth unlocked');
}}
className={`px-2 py-1 rounded text-xs font-bold transition ${
staffRole === 'doctor'
? 'bg-white text-sky-800 shadow-2xs border border-slate-200'
@@ -270,10 +380,12 @@ export default function Home() {
<button
type="button"
onClick={() => {
clinicalAudio.playClick();
setStaffRole('front_desk');
if (clinicTab === 'charting' || clinicTab === 'telehealth') {
setClinicTab('calendar');
}
addToast('alert', 'Role: Front Desk (HIPAA Safe)', 'Protected medical notes locked for privacy');
}}
className={`px-2 py-1 rounded text-xs font-bold transition ${
staffRole === 'front_desk'
@@ -286,7 +398,11 @@ export default function Home() {
</button>
<button
type="button"
onClick={() => setStaffRole('billing_admin')}
onClick={() => {
clinicalAudio.playClick();
setStaffRole('billing_admin');
addToast('info', 'Role: Billing Administrator', 'Superbill claims and memberships prioritized');
}}
className={`px-2 py-1 rounded text-xs font-bold transition ${
staffRole === 'billing_admin'
? 'bg-white text-emerald-800 shadow-2xs border border-slate-200'
@@ -303,7 +419,10 @@ export default function Home() {
<div className="flex items-center gap-1 bg-slate-100 p-1 rounded-lg border border-slate-200 text-xs font-semibold">
<button
type="button"
onClick={() => setPortalMode('clinic')}
onClick={() => {
clinicalAudio.playClick();
setPortalMode('clinic');
}}
className={`px-3 py-1.5 rounded-md flex items-center gap-1.5 transition ${
portalMode === 'clinic'
? 'bg-sky-700 text-white font-bold shadow-xs'
@@ -316,7 +435,10 @@ export default function Home() {
<button
type="button"
onClick={() => setPortalMode('patient')}
onClick={() => {
clinicalAudio.playClick();
setPortalMode('patient');
}}
className={`px-3 py-1.5 rounded-md flex items-center gap-1.5 transition ${
portalMode === 'patient'
? 'bg-sky-700 text-white font-bold shadow-xs'
@@ -555,6 +677,34 @@ export default function Home() {
onAutoFillPatient={handleAutoFillWaitlistPatient}
/>
{/* Global Command Palette (Cmd+K) */}
<CommandPalette
isOpen={isCommandPaletteOpen}
onClose={() => setIsCommandPaletteOpen(false)}
patients={patients}
onSelectPatient={(p) => {
setActivePatient(p);
const existing = soapNotes.find((s) => s.patientId === p.id);
setActiveSoapNote(existing);
}}
onNavigateTab={(tab) => setClinicTab(tab)}
onOpenWaitlist={() => setIsWaitlistOpen(true)}
onSetStaffRole={(role) => {
setStaffRole(role);
if (role === 'front_desk' && (clinicTab === 'charting' || clinicTab === 'telehealth')) {
setClinicTab('calendar');
}
}}
onToggleAudio={handleToggleAudio}
audioEnabled={audioEnabled}
/>
{/* Hospital Clinical Toast Notification System */}
<ClinicalToastContainer
toasts={toasts}
onDismiss={handleDismissToast}
/>
{/* Hospital Footer */}
<footer className="bg-white border-t border-slate-200 py-4 px-6 text-center text-xs text-slate-500">
<div className="flex flex-col sm:flex-row items-center justify-between max-w-7xl mx-auto gap-2">
+18 -10
View File
@@ -12,6 +12,8 @@ import {
} from '@/types/clinical';
import { SpineVisualizer } from '@/components/ui/SpineVisualizer';
import { AmbientAudioRecorder } from '@/components/ui/AmbientAudioRecorder';
import { PatientClinicalHud } from '@/components/ui/PatientClinicalHud';
import { clinicalAudio } from '@/lib/clinical-audio';
import { STANDARD_CPT_CODES, STANDARD_ICD10_CODES, DISCIPLINE_PRESETS } from '@/lib/mock-data';
import {
CheckCircle,
@@ -22,6 +24,7 @@ import {
ShieldCheck,
Video,
Layers,
Lock,
} from 'lucide-react';
interface SoapChartEditorProps {
@@ -109,6 +112,7 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
};
const handleSwitchDiscipline = (newDiscipline: ClinicalDiscipline) => {
clinicalAudio.playClick();
setDiscipline(newDiscipline);
const preset = DISCIPLINE_PRESETS[newDiscipline];
if (preset) {
@@ -126,6 +130,7 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
};
const handleToggleAdjustment = (entry: SpinalAdjustmentEntry) => {
clinicalAudio.playClick();
setAdjustments((prev) => {
const exists = prev.some((a) => a.vertebra === entry.vertebra);
if (exists) {
@@ -137,6 +142,7 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
};
const handleCloneLastNote = () => {
clinicalAudio.playClick();
setSubjective('Patient reports continued symptom improvement following last spinal adjustment. Morning stiffness resolved within 10 minutes. Current pain rated 3/10.');
setObjective('Palpation reveals decreased tone in lumbar paraspinals. Persistent fixation noted at L4-L5 with right sacral torsion.');
setAssessment('Care plan compliance high. Significant restoration of active range of motion noted.');
@@ -144,6 +150,7 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
};
const handleSignChart = () => {
clinicalAudio.playSuccess();
const now = new Date().toISOString();
setIsSigned(true);
setSignedTimestamp(now);
@@ -174,6 +181,7 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
};
const handleCreateSuperbillClick = () => {
clinicalAudio.playSuccess();
const savedNote: SoapNote = {
id: initialSoapNote?.id || `soap-${Date.now()}`,
tenantId: patient.tenantId,
@@ -193,14 +201,18 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
icd10Codes: selectedIcd10,
cptCodes: selectedCpt,
signedAt: signedTimestamp,
signedBy: `${provider.name}, ${provider.credentials}`,
signedBy: isSigned ? `${provider.name}, ${provider.credentials}` : undefined,
};
onSaveSoapNote(savedNote);
onGenerateSuperbill(savedNote);
};
return (
<div className="space-y-6">
<div className="space-y-4">
{/* Patient Clinical HUD Ribbon */}
<PatientClinicalHud patient={patient} />
{/* Top Patient Header Bar */}
<div className="bg-white border border-slate-200 rounded-xl p-5 shadow-xs flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-center gap-3">
@@ -218,14 +230,10 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
<h3 className="text-lg font-bold text-slate-900">
{patient.firstName} {patient.lastName}
</h3>
<span className="text-xs px-2.5 py-0.5 rounded-full bg-sky-50 text-sky-800 border border-sky-200 font-semibold">
Care Plan: Visit {patient.carePlan.completedVisits + 1} of {patient.carePlan.totalVisits}
</span>
{patient.activeMembership && (
<span className="text-xs px-2 py-0.5 rounded-full bg-emerald-50 text-emerald-800 border border-emerald-200 font-bold">
{patient.activeMembership}
</span>
)}
<div className="flex items-center gap-1.5 px-2.5 py-0.5 rounded-full bg-emerald-50 border border-emerald-200 text-emerald-800 text-[10px] font-semibold">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
<span>HIPAA Vault Sync</span>
</div>
{isSigned ? (
<span className="text-xs px-2.5 py-0.5 rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200 font-semibold flex items-center gap-1">
<ShieldCheck className="w-3.5 h-3.5" /> Signed &amp; Locked
+59
View File
@@ -0,0 +1,59 @@
'use client';
import React from 'react';
import { CheckCircle2, AlertTriangle, Info, X } from 'lucide-react';
export interface ToastMessage {
id: string;
type: 'success' | 'alert' | 'info';
title: string;
description?: string;
timestamp: string;
}
interface ClinicalToastProps {
toasts: ToastMessage[];
onDismiss: (id: string) => void;
}
export const ClinicalToastContainer: React.FC<ClinicalToastProps> = ({ toasts, onDismiss }) => {
if (toasts.length === 0) return null;
return (
<aside aria-label="Clinical System Notifications" className="fixed bottom-6 right-6 z-50 flex flex-col space-y-2 max-w-sm w-full pointer-events-none">
{toasts.map((t) => (
<div
key={t.id}
className={`pointer-events-auto flex items-start gap-3 p-3.5 rounded-xl border shadow-lg backdrop-blur-md transition-all duration-300 transform translate-y-0 ${
t.type === 'success'
? 'bg-white/95 border-emerald-200 text-slate-800'
: t.type === 'alert'
? 'bg-white/95 border-amber-200 text-slate-800'
: 'bg-white/95 border-sky-200 text-slate-800'
}`}
>
<div className="mt-0.5 shrink-0">
{t.type === 'success' && <CheckCircle2 className="w-5 h-5 text-emerald-600" />}
{t.type === 'alert' && <AlertTriangle className="w-5 h-5 text-amber-600" />}
{t.type === 'info' && <Info className="w-5 h-5 text-sky-600" />}
</div>
<div className="flex-1 min-w-0">
<p className="text-xs font-semibold text-slate-900 leading-tight">{t.title}</p>
{t.description && (
<p className="text-[11px] text-slate-600 mt-0.5 leading-snug">{t.description}</p>
)}
<span className="text-[10px] text-slate-400 mt-1 block">{t.timestamp}</span>
</div>
<button
onClick={() => onDismiss(t.id)}
className="text-slate-400 hover:text-slate-600 p-1 rounded-md transition-colors"
>
<X className="w-3.5 h-3.5" />
</button>
</div>
))}
</aside>
);
};
+375
View File
@@ -0,0 +1,375 @@
'use client';
import React, { useState, useEffect, useRef } from 'react';
import {
Search,
Calendar,
FileText,
Video,
ShoppingBag,
Repeat,
DollarSign,
Users,
Activity,
User,
Shield,
Volume2,
VolumeX,
X,
Sparkles,
Command,
} from 'lucide-react';
import { Patient, StaffRole } from '@/types/clinical';
import { clinicalAudio } from '@/lib/clinical-audio';
export interface CommandItem {
id: string;
category: 'Patients' | 'Navigation' | 'Actions' | 'Staff Role' | 'Settings';
title: string;
subtitle?: string;
icon: React.ReactNode;
badge?: string;
action: () => void;
}
interface CommandPaletteProps {
isOpen: boolean;
onClose: () => void;
patients: Patient[];
onSelectPatient: (p: Patient) => void;
onNavigateTab: (tab: 'calendar' | 'charting' | 'telehealth' | 'retail' | 'memberships' | 'billing' | 'retention') => void;
onOpenWaitlist: () => void;
onSetStaffRole: (role: StaffRole) => void;
onToggleAudio: () => void;
audioEnabled: boolean;
}
export const CommandPalette: React.FC<CommandPaletteProps> = ({
isOpen,
onClose,
patients,
onSelectPatient,
onNavigateTab,
onOpenWaitlist,
onSetStaffRole,
onToggleAudio,
audioEnabled,
}) => {
const [query, setQuery] = useState('');
const [selectedIndex, setSelectedIndex] = useState(0);
const inputRef = useRef<HTMLInputElement>(null);
const listRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isOpen) {
setTimeout(() => inputRef.current?.focus(), 50);
setQuery('');
setSelectedIndex(0);
clinicalAudio.playClick();
}
}, [isOpen]);
// Generate commands list
const baseCommands: CommandItem[] = [
// Patients
...patients.map((p) => ({
id: `patient-${p.id}`,
category: 'Patients' as const,
title: `${p.firstName} ${p.lastName}`,
subtitle: `${p.carePlan.targetCondition} • Visit ${p.carePlan.completedVisits}/${p.carePlan.totalVisits}`,
badge: p.status === 'dropout_risk' ? 'Dropout Risk' : 'Active Patient',
icon: <User className="w-4 h-4 text-sky-600" />,
action: () => {
onSelectPatient(p);
onNavigateTab('charting');
onClose();
},
})),
// Navigation
{
id: 'nav-calendar',
category: 'Navigation',
title: 'Schedule & Operatory Calendar',
subtitle: 'View live provider schedules and patient arrival states',
icon: <Calendar className="w-4 h-4 text-sky-600" />,
action: () => {
onNavigateTab('calendar');
onClose();
},
},
{
id: 'nav-charting',
category: 'Navigation',
title: 'Clinical SOAP Charting & Spine Visualizer',
subtitle: '2D anatomical subluxation clicks and ambient AI scribe',
icon: <FileText className="w-4 h-4 text-emerald-600" />,
action: () => {
onNavigateTab('charting');
onClose();
},
},
{
id: 'nav-telehealth',
category: 'Navigation',
title: 'Virtual Telehealth Consultation Suite',
subtitle: 'HIPAA-compliant video consult with live side-by-side notes',
icon: <Video className="w-4 h-4 text-blue-600" />,
action: () => {
onNavigateTab('telehealth');
onClose();
},
},
{
id: 'nav-retail',
category: 'Navigation',
title: 'Front-Desk Supplement & Retail POS',
subtitle: 'Stock inventory, barcode search, Stripe terminal checkout',
icon: <ShoppingBag className="w-4 h-4 text-amber-600" />,
action: () => {
onNavigateTab('retail');
onClose();
},
},
{
id: 'nav-memberships',
category: 'Navigation',
title: 'Recurring Memberships & Visit Packages',
subtitle: '$89/mo monthly wellness club and pre-paid visit blocks',
icon: <Repeat className="w-4 h-4 text-emerald-600" />,
action: () => {
onNavigateTab('memberships');
onClose();
},
},
{
id: 'nav-billing',
category: 'Navigation',
title: 'Medical Superbill & CMS-1500 Billing',
subtitle: 'Generate reimbursement claim PDF and Stripe copay checkout',
icon: <DollarSign className="w-4 h-4 text-slate-700" />,
action: () => {
onNavigateTab('billing');
onClose();
},
},
{
id: 'nav-retention',
category: 'Navigation',
title: 'Care Plan Retention & Dropout Radar',
subtitle: 'Autonomous recovery SMS dispatch for lagging patients',
icon: <Users className="w-4 h-4 text-rose-600" />,
action: () => {
onNavigateTab('retention');
onClose();
},
},
// Fast Actions
{
id: 'action-waitlist',
category: 'Actions',
title: 'Cancellation Waitlist Queue',
subtitle: 'Dispatch SMS alerts to queued patients for open slots',
badge: 'Waitlist',
icon: <Activity className="w-4 h-4 text-amber-600" />,
action: () => {
onOpenWaitlist();
onClose();
},
},
// Staff Roles
{
id: 'role-doctor',
category: 'Staff Role',
title: 'Switch Role: Doctor (D.C.)',
subtitle: 'Full access to clinical SOAP, 2D spine, AI scribe & telehealth',
icon: <Shield className="w-4 h-4 text-sky-600" />,
action: () => {
onSetStaffRole('doctor');
onClose();
},
},
{
id: 'role-front-desk',
category: 'Staff Role',
title: 'Switch Role: Front Desk (HIPAA-Safe Mode)',
subtitle: 'Locks medical charts; grants schedule, POS & waitlist access',
icon: <Shield className="w-4 h-4 text-emerald-600" />,
action: () => {
onSetStaffRole('front_desk');
onClose();
},
},
{
id: 'role-billing-admin',
category: 'Staff Role',
title: 'Switch Role: Billing Administrator',
subtitle: 'Direct focus on superbills, claims, packages & recurring clubs',
icon: <Shield className="w-4 h-4 text-indigo-600" />,
action: () => {
onSetStaffRole('billing_admin');
onClose();
},
},
// Settings
{
id: 'setting-audio',
category: 'Settings',
title: audioEnabled ? 'Mute Tactile Clinical Audio' : 'Enable Tactile Clinical Audio',
subtitle: 'Web Audio synthesizer feedback for spine clicks and payments',
icon: audioEnabled ? <VolumeX className="w-4 h-4 text-slate-600" /> : <Volume2 className="w-4 h-4 text-emerald-600" />,
action: () => {
onToggleAudio();
onClose();
},
},
];
const filtered = baseCommands.filter((cmd) => {
const q = query.toLowerCase();
return (
cmd.title.toLowerCase().includes(q) ||
(cmd.subtitle && cmd.subtitle.toLowerCase().includes(q)) ||
cmd.category.toLowerCase().includes(q)
);
});
// Handle keyboard navigation inside the list
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex((prev) => (prev + 1) % (filtered.length || 1));
clinicalAudio.playClick();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex((prev) => (prev - 1 + filtered.length) % (filtered.length || 1));
clinicalAudio.playClick();
} else if (e.key === 'Enter') {
e.preventDefault();
if (filtered[selectedIndex]) {
clinicalAudio.playSuccess();
filtered[selectedIndex].action();
}
} else if (e.key === 'Escape') {
e.preventDefault();
onClose();
}
};
if (!isOpen) return null;
return (
<div
role="dialog"
aria-modal="true"
aria-label="Command Palette"
className="fixed inset-0 z-50 flex items-start justify-center pt-20 px-4 bg-slate-900/40 backdrop-blur-sm transition-all"
onClick={(e) => {
if (e.target === e.currentTarget) onClose();
}}
>
<div
className="w-full max-w-xl bg-white rounded-2xl shadow-2xl border border-slate-200 overflow-hidden transform transition-all flex flex-col max-h-[80vh]"
onKeyDown={handleKeyDown}
>
{/* Search Header */}
<div className="flex items-center px-4 py-3.5 border-b border-slate-100 bg-slate-50/50">
<Search className="w-5 h-5 text-slate-400 mr-3 shrink-0" />
<input
ref={inputRef}
type="text"
value={query}
onChange={(e) => {
setQuery(e.target.value);
setSelectedIndex(0);
}}
placeholder="Type a patient name, clinical command, or view... (Cmd+K)"
className="w-full text-sm text-slate-800 placeholder-slate-400 bg-transparent border-none focus:outline-none focus:ring-0"
/>
<kbd className="hidden sm:inline-flex items-center gap-1 px-2 py-0.5 text-[10px] font-mono font-medium text-slate-400 bg-slate-100 border border-slate-200 rounded">
ESC
</kbd>
</div>
{/* Results List */}
<div ref={listRef} className="overflow-y-auto p-2 divide-y divide-slate-50 space-y-1">
{filtered.length === 0 ? (
<div className="py-12 text-center text-slate-400 text-xs">
No matching patients or clinical commands found for &ldquo;{query}&rdquo;
</div>
) : (
filtered.map((item, idx) => {
const isSelected = idx === selectedIndex;
return (
<button
key={item.id}
type="button"
onClick={() => {
clinicalAudio.playSuccess();
item.action();
}}
onMouseEnter={() => setSelectedIndex(idx)}
className={`w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-left transition-all ${
isSelected ? 'bg-sky-50/80 border border-sky-200' : 'hover:bg-slate-50 border border-transparent'
}`}
>
<div className="flex items-center gap-3 min-w-0">
<div
className={`p-2 rounded-lg shrink-0 ${
isSelected ? 'bg-white shadow-xs' : 'bg-slate-100'
}`}
>
{item.icon}
</div>
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="text-xs font-semibold text-slate-800 truncate">
{item.title}
</span>
{item.badge && (
<span className="text-[10px] px-1.5 py-0.5 rounded-full font-medium bg-slate-100 text-slate-600">
{item.badge}
</span>
)}
</div>
{item.subtitle && (
<p className="text-[11px] text-slate-500 truncate mt-0.5">
{item.subtitle}
</p>
)}
</div>
</div>
<span className="text-[10px] font-medium text-slate-400 uppercase tracking-wider shrink-0 ml-2">
{item.category}
</span>
</button>
);
})
)}
</div>
{/* Footer shortcuts helper */}
<div className="px-4 py-2.5 bg-slate-50 border-t border-slate-100 flex items-center justify-between text-[11px] text-slate-500">
<div className="flex items-center gap-3">
<span className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-white border border-slate-200 rounded font-mono text-[9px]"></kbd>
<kbd className="px-1.5 py-0.5 bg-white border border-slate-200 rounded font-mono text-[9px]"></kbd>
Navigate
</span>
<span className="flex items-center gap-1">
<kbd className="px-1.5 py-0.5 bg-white border border-slate-200 rounded font-mono text-[9px]"></kbd>
Select
</span>
</div>
<div className="flex items-center gap-1 text-[10px] text-sky-700 font-medium">
<Sparkles className="w-3 h-3" /> Mediusa Quick Command Hub
</div>
</div>
</div>
</div>
);
};
+344
View File
@@ -0,0 +1,344 @@
'use client';
import React, { useState } from 'react';
import { Patient, PatientVitals } from '@/types/clinical';
import {
Activity,
Heart,
Wind,
Flame,
AlertTriangle,
ShieldCheck,
CreditCard,
Edit2,
Check,
X,
ChevronUp,
ChevronDown,
} from 'lucide-react';
import { clinicalAudio } from '@/lib/clinical-audio';
interface PatientClinicalHudProps {
patient: Patient;
onUpdateVitals?: (patientId: string, newVitals: PatientVitals) => void;
}
export const PatientClinicalHud: React.FC<PatientClinicalHudProps> = ({
patient,
onUpdateVitals,
}) => {
const [isExpanded, setIsExpanded] = useState(true);
const [isEditingVitals, setIsEditingVitals] = useState(false);
const vitals = patient.vitals || {
bloodPressure: '120/80',
heartRate: 72,
temperature: '98.6 °F',
oxygenSat: 99,
painLevel: 4,
bmi: '23.5',
allergies: [],
contraindications: [],
};
const [editBp, setEditBp] = useState(vitals.bloodPressure);
const [editHr, setEditHr] = useState(vitals.heartRate);
const [editO2, setEditO2] = useState(vitals.oxygenSat);
const [editPain, setEditPain] = useState(vitals.painLevel);
const handleSaveVitals = () => {
clinicalAudio.playSuccess();
if (onUpdateVitals) {
onUpdateVitals(patient.id, {
...vitals,
bloodPressure: editBp,
heartRate: Number(editHr),
oxygenSat: Number(editO2),
painLevel: Number(editPain),
});
}
setIsEditingVitals(false);
};
const carePlanProgress = Math.round(
(patient.carePlan.completedVisits / patient.carePlan.totalVisits) * 100
);
return (
<section aria-label="Patient Clinical HUD" className="mb-4 bg-white border border-slate-200 rounded-2xl shadow-xs overflow-hidden">
{/* Top Banner Row */}
<div className="px-4 py-2.5 bg-slate-50/80 border-b border-slate-200 flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-sky-100 text-sky-700 font-bold text-xs flex items-center justify-center border border-sky-200">
{patient.firstName[0]}
{patient.lastName[0]}
</div>
<div>
<div className="flex items-center gap-2">
<h2 className="text-xs font-bold text-slate-900">
{patient.firstName} {patient.lastName}
</h2>
<span className="text-[10px] text-slate-500 font-mono bg-slate-200/70 px-1.5 py-0.5 rounded">
MRN-{patient.id.toUpperCase()}
</span>
<span className="text-[10px] text-slate-500">
DOB: {patient.dob} ({patient.gender})
</span>
</div>
<p className="text-[11px] text-slate-600 truncate max-w-md">
<span className="font-semibold text-slate-700">Chief Complaint:</span>{' '}
{patient.chiefComplaint}
</p>
</div>
</div>
<div className="flex items-center gap-2">
{/* Active Membership Badge */}
{patient.activeMembership && (
<div className="hidden sm:flex items-center gap-1.5 px-2.5 py-1 rounded-full bg-emerald-50 border border-emerald-200 text-emerald-700 text-[10px] font-semibold">
<CreditCard className="w-3 h-3" />
<span>{patient.activeMembership}</span>
{patient.packageCreditsRemaining !== undefined && (
<span className="bg-emerald-200/80 px-1 rounded-full text-[9px]">
{patient.packageCreditsRemaining} left
</span>
)}
</div>
)}
{/* Insurance Pill */}
<div className="hidden md:flex items-center gap-1 px-2.5 py-1 rounded-full bg-sky-50 border border-sky-200 text-sky-800 text-[10px] font-medium">
<ShieldCheck className="w-3 h-3 text-sky-600" />
<span>{patient.insuranceName}</span>
</div>
<button
onClick={() => {
clinicalAudio.playClick();
setIsExpanded(!isExpanded);
}}
className="p-1 rounded-lg hover:bg-slate-200/60 text-slate-500 text-xs flex items-center gap-1 font-medium transition-colors"
>
{isExpanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
</button>
</div>
</div>
{/* Expanded Clinical Data Row */}
{isExpanded && (
<div className="p-3.5 bg-white grid grid-cols-1 lg:grid-cols-12 gap-3 text-xs">
{/* Left: Vitals Gauges (Col 5) */}
<div className="lg:col-span-5 flex flex-wrap items-center gap-2">
{/* BP */}
<div className="flex-1 min-w-[100px] p-2 rounded-xl bg-slate-50 border border-slate-200 flex items-center gap-2.5">
<Activity className="w-4 h-4 text-sky-600 shrink-0" />
<div>
<span className="text-[10px] text-slate-500 uppercase tracking-wider block font-medium">
BP
</span>
<span className="font-bold text-slate-800 text-xs">
{vitals.bloodPressure}{' '}
<span className="text-[9px] font-normal text-slate-400">mmHg</span>
</span>
</div>
</div>
{/* Heart Rate */}
<div className="flex-1 min-w-[90px] p-2 rounded-xl bg-slate-50 border border-slate-200 flex items-center gap-2.5">
<Heart className="w-4 h-4 text-rose-500 shrink-0" />
<div>
<span className="text-[10px] text-slate-500 uppercase tracking-wider block font-medium">
HR
</span>
<span className="font-bold text-slate-800 text-xs">
{vitals.heartRate}{' '}
<span className="text-[9px] font-normal text-slate-400">bpm</span>
</span>
</div>
</div>
{/* SpO2 */}
<div className="flex-1 min-w-[85px] p-2 rounded-xl bg-slate-50 border border-slate-200 flex items-center gap-2.5">
<Wind className="w-4 h-4 text-teal-600 shrink-0" />
<div>
<span className="text-[10px] text-slate-500 uppercase tracking-wider block font-medium">
SpO
</span>
<span className="font-bold text-slate-800 text-xs">
{vitals.oxygenSat}%
</span>
</div>
</div>
{/* Pain Scale */}
<div className="flex-1 min-w-[90px] p-2 rounded-xl bg-slate-50 border border-slate-200 flex items-center gap-2.5">
<Flame className="w-4 h-4 text-amber-500 shrink-0" />
<div>
<span className="text-[10px] text-slate-500 uppercase tracking-wider block font-medium">
Pain VAS
</span>
<span className="font-bold text-amber-700 text-xs">
{vitals.painLevel}/10
</span>
</div>
</div>
<button
onClick={() => {
clinicalAudio.playClick();
setIsEditingVitals(true);
}}
title="Edit Patient Vitals"
className="p-2 rounded-xl bg-slate-100 hover:bg-slate-200 text-slate-600 transition-colors shrink-0"
>
<Edit2 className="w-3.5 h-3.5" />
</button>
</div>
{/* Center: Care Plan Progress (Col 3) */}
<div className="lg:col-span-3 p-2.5 rounded-xl bg-sky-50/60 border border-sky-100 flex flex-col justify-center">
<div className="flex items-center justify-between text-[11px] mb-1">
<span className="font-semibold text-sky-900">Care Plan Cadence</span>
<span className="font-bold text-sky-800">
Visit {patient.carePlan.completedVisits} of {patient.carePlan.totalVisits}
</span>
</div>
<div className="w-full bg-sky-200/80 rounded-full h-1.5 overflow-hidden">
<div
className="bg-sky-600 h-1.5 rounded-full transition-all duration-500"
style={{ width: `${carePlanProgress}%` }}
/>
</div>
<p className="text-[10px] text-sky-700 mt-1 truncate">
{patient.carePlan.frequency} {patient.carePlan.targetCondition}
</p>
</div>
{/* Right: Clinical Red Flags & Contraindications (Col 4) */}
<div className="lg:col-span-4 p-2.5 rounded-xl bg-amber-50/70 border border-amber-200/80 flex flex-col justify-center">
<div className="flex items-center gap-1.5 text-amber-900 font-semibold text-[11px] mb-1">
<AlertTriangle className="w-3.5 h-3.5 text-amber-600 shrink-0" />
<span>Clinical Precautions & Allergies</span>
</div>
<div className="flex flex-wrap gap-1">
{vitals.contraindications.map((c, i) => (
<span
key={i}
className="text-[10px] bg-amber-100/90 text-amber-900 border border-amber-300 px-1.5 py-0.5 rounded font-medium"
>
{c}
</span>
))}
{vitals.allergies.length > 0 ? (
vitals.allergies.map((a, i) => (
<span
key={`all-${i}`}
className="text-[10px] bg-rose-100 text-rose-800 border border-rose-300 px-1.5 py-0.5 rounded font-medium"
>
Allergy: {a}
</span>
))
) : (
<span className="text-[10px] text-slate-500 italic">No drug allergies listed</span>
)}
</div>
</div>
</div>
)}
{/* Quick Edit Vitals Modal */}
{isEditingVitals && (
<div
role="dialog"
aria-modal="true"
aria-label="Update Patient Vitals"
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/40 backdrop-blur-xs"
>
<div className="bg-white rounded-2xl shadow-xl border border-slate-200 w-full max-w-sm p-5 space-y-4">
<div className="flex items-center justify-between border-b border-slate-100 pb-2.5">
<div className="flex items-center gap-2">
<Activity className="w-4 h-4 text-sky-600" />
<h4 className="text-sm font-bold text-slate-900">Update Clinical Vitals</h4>
</div>
<button
onClick={() => setIsEditingVitals(false)}
className="text-slate-400 hover:text-slate-600"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="space-y-3 text-xs">
<div>
<label className="block font-semibold text-slate-700 mb-1">
Blood Pressure (mmHg)
</label>
<input
type="text"
value={editBp}
onChange={(e) => setEditBp(e.target.value)}
placeholder="120/80"
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-slate-800 focus:outline-none focus:ring-2 focus:ring-sky-500"
/>
</div>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="block font-semibold text-slate-700 mb-1">Heart Rate (bpm)</label>
<input
type="number"
value={editHr}
onChange={(e) => setEditHr(Number(e.target.value))}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-slate-800 focus:outline-none focus:ring-2 focus:ring-sky-500"
/>
</div>
<div>
<label className="block font-semibold text-slate-700 mb-1">SpO Oxygen (%)</label>
<input
type="number"
value={editO2}
onChange={(e) => setEditO2(Number(e.target.value))}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-slate-800 focus:outline-none focus:ring-2 focus:ring-sky-500"
/>
</div>
</div>
<div>
<label className="block font-semibold text-slate-700 mb-1">
VAS Pain Level (0 - 10)
</label>
<div className="flex items-center gap-3">
<input
type="range"
min="0"
max="10"
value={editPain}
onChange={(e) => setEditPain(Number(e.target.value))}
className="w-full accent-amber-600"
/>
<span className="font-bold text-amber-700 w-8 text-center">{editPain}/10</span>
</div>
</div>
</div>
<div className="flex items-center justify-end gap-2 pt-2 border-t border-slate-100">
<button
onClick={() => setIsEditingVitals(false)}
className="px-3 py-1.5 text-xs text-slate-600 hover:bg-slate-100 rounded-lg transition-colors"
>
Cancel
</button>
<button
onClick={handleSaveVitals}
className="px-4 py-1.5 text-xs font-semibold text-white bg-sky-600 hover:bg-sky-700 rounded-lg flex items-center gap-1.5 shadow-xs transition-colors"
>
<Check className="w-3.5 h-3.5" /> Save Vitals
</button>
</div>
</div>
</div>
)}
</section>
);
};
+61 -5
View File
@@ -3,6 +3,7 @@
import React, { useState } from 'react';
import { SpinalAdjustmentEntry } from '@/types/clinical';
import { Check, X, Zap, Activity } from 'lucide-react';
import { clinicalAudio } from '@/lib/clinical-audio';
interface SpineVisualizerProps {
adjustments: SpinalAdjustmentEntry[];
@@ -93,6 +94,7 @@ export const SpineVisualizer: React.FC<SpineVisualizerProps> = ({
const handleOpenModal = (v: VertebraDef) => {
if (readOnly) return;
clinicalAudio.playClick();
const existing = getAdjustment(v.id);
if (existing) {
setActiveListing(existing.listing);
@@ -106,6 +108,7 @@ export const SpineVisualizer: React.FC<SpineVisualizerProps> = ({
const handleSaveListing = () => {
if (!selectedVertebra) return;
clinicalAudio.playSuccess();
onToggleAdjustment({
vertebra: selectedVertebra.id,
region: selectedVertebra.region,
@@ -117,6 +120,7 @@ export const SpineVisualizer: React.FC<SpineVisualizerProps> = ({
};
const handleRemoveListing = (vertebraId: string) => {
clinicalAudio.playClick();
const existing = getAdjustment(vertebraId);
if (existing) {
onToggleAdjustment(existing);
@@ -288,13 +292,28 @@ export const SpineVisualizer: React.FC<SpineVisualizerProps> = ({
{/* Quick Subluxation Preset Macros */}
{!readOnly && (
<div className="mt-4 pt-3 border-t border-slate-200">
<div className="text-[11px] font-bold text-slate-600 uppercase tracking-wider mb-2">
<div className="flex items-center justify-between mb-2">
<span className="text-[11px] font-bold text-slate-600 uppercase tracking-wider">
Clinical Fast Macros
</span>
{adjustments.length > 0 && (
<button
type="button"
onClick={() => {
clinicalAudio.playClick();
adjustments.forEach((adj) => onToggleAdjustment(adj));
}}
className="text-[10px] font-semibold text-rose-600 hover:text-rose-700"
>
Clear All
</button>
)}
</div>
<div className="grid grid-cols-2 gap-1.5">
<button
type="button"
onClick={() => {
clinicalAudio.playSuccess();
onToggleAdjustment({
vertebra: 'C1',
region: 'Cervical',
@@ -302,19 +321,20 @@ export const SpineVisualizer: React.FC<SpineVisualizerProps> = ({
technique: 'Diversified',
});
onToggleAdjustment({
vertebra: 'C5',
vertebra: 'C2',
region: 'Cervical',
listing: 'Bilateral Facet Fixation',
listing: 'Right Rotation Fixation',
technique: 'Diversified',
});
}}
className="px-2.5 py-1.5 bg-white hover:bg-slate-100 text-slate-700 border border-slate-200 rounded text-xs font-semibold transition text-left truncate shadow-2xs"
>
+ Cervical Protocol
Upper Cervical
</button>
<button
type="button"
onClick={() => {
clinicalAudio.playSuccess();
onToggleAdjustment({
vertebra: 'L4',
region: 'Lumbar',
@@ -330,7 +350,43 @@ export const SpineVisualizer: React.FC<SpineVisualizerProps> = ({
}}
className="px-2.5 py-1.5 bg-white hover:bg-slate-100 text-slate-700 border border-slate-200 rounded text-xs font-semibold transition text-left truncate shadow-2xs"
>
+ L4/SI Protocol
Lumbo-Sacral
</button>
<button
type="button"
onClick={() => {
clinicalAudio.playSuccess();
onToggleAdjustment({
vertebra: 'T4',
region: 'Thoracic',
listing: 'Posterior Fixation (P)',
technique: 'Diversified',
});
onToggleAdjustment({
vertebra: 'T6',
region: 'Thoracic',
listing: 'Left Rotation (LP)',
technique: 'Diversified',
});
}}
className="px-2.5 py-1.5 bg-white hover:bg-slate-100 text-slate-700 border border-slate-200 rounded text-xs font-semibold transition text-left truncate shadow-2xs"
>
Mid-Thoracic
</button>
<button
type="button"
onClick={() => {
clinicalAudio.playSuccess();
onToggleAdjustment({
vertebra: 'Right Ilium',
region: 'Pelvis/Sacrum',
listing: 'Posterior Inferior (PI)',
technique: 'Thompson Drop',
});
}}
className="px-2.5 py-1.5 bg-white hover:bg-slate-100 text-slate-700 border border-slate-200 rounded text-xs font-semibold transition text-left truncate shadow-2xs"
>
SI Joint Drop
</button>
</div>
</div>
+126
View File
@@ -0,0 +1,126 @@
// Web Audio API Synthesizer for Clinical Tactile Audio Feedback
// Provides zero-latency, zero-asset medical UI sound effects
class ClinicalAudioManager {
private audioCtx: AudioContext | null = null;
private enabled: boolean = true;
constructor() {
// Lazy init audio context on first user interaction
if (typeof window !== 'undefined') {
const stored = localStorage.getItem('mediusa_sound_enabled');
if (stored !== null) {
this.enabled = stored === 'true';
}
}
}
public isEnabled(): boolean {
return this.enabled;
}
public setEnabled(val: boolean) {
this.enabled = val;
if (typeof window !== 'undefined') {
localStorage.setItem('mediusa_sound_enabled', String(val));
}
}
private getContext(): AudioContext | null {
if (!this.enabled) return null;
if (typeof window === 'undefined') return null;
if (!this.audioCtx) {
const AudioContextClass = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
if (AudioContextClass) {
this.audioCtx = new AudioContextClass();
}
}
if (this.audioCtx && this.audioCtx.state === 'suspended') {
this.audioCtx.resume().catch(() => {});
}
return this.audioCtx;
}
// Soft tactile click for spine segment selection, tabs, buttons
public playClick() {
const ctx = this.getContext();
if (!ctx) return;
try {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(800, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(300, ctx.currentTime + 0.04);
gain.gain.setValueAtTime(0.08, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.04);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 0.04);
} catch {
// Audio context might be restricted before interaction
}
}
// Two-tone harmonic chime for chart saving, Stripe payment, or sign-off
public playSuccess() {
const ctx = this.getContext();
if (!ctx) return;
try {
const now = ctx.currentTime;
const notes = [523.25, 659.25]; // C5 to E5
notes.forEach((freq, idx) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, now + idx * 0.08);
gain.gain.setValueAtTime(0.09, now + idx * 0.08);
gain.gain.exponentialRampToValueAtTime(0.001, now + idx * 0.08 + 0.25);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(now + idx * 0.08);
osc.stop(now + idx * 0.08 + 0.26);
});
} catch {}
}
// Gentle medical ping for waitlist SMS dispatch, notification
public playPing() {
const ctx = this.getContext();
if (!ctx) return;
try {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(440, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(880, ctx.currentTime + 0.12);
gain.gain.setValueAtTime(0.08, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.18);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start();
osc.stop(ctx.currentTime + 0.18);
} catch {}
}
}
export const clinicalAudio = new ClinicalAudioManager();
+40
View File
@@ -147,6 +147,16 @@ export const INITIAL_PATIENTS: Patient[] = [
status: 'active',
activeMembership: 'Chiropractic Wellness Club',
packageCreditsRemaining: 4,
vitals: {
bloodPressure: '118/76',
heartRate: 68,
temperature: '98.4 °F',
oxygenSat: 99,
painLevel: 5,
bmi: '23.8',
allergies: ['Penicillin', 'Latex'],
contraindications: ['Prior L4-L5 Microdiscectomy (2021)', 'Avoid aggressive lumbo-pelvic torsion'],
},
carePlan: {
title: 'Lumbar Disc Decompression & Stabilization Protocol',
totalVisits: 12,
@@ -175,6 +185,16 @@ export const INITIAL_PATIENTS: Patient[] = [
daysSinceLastVisit: 18,
status: 'dropout_risk',
packageCreditsRemaining: 0,
vitals: {
bloodPressure: '124/82',
heartRate: 74,
temperature: '98.6 °F',
oxygenSat: 98,
painLevel: 7,
bmi: '22.1',
allergies: ['Sulfa drugs'],
contraindications: ['Hypermobile C4-C5 segment', 'Prefers gentle Activator on cervical spine'],
},
carePlan: {
title: 'Cervical Postural Realignment & Headache Relief',
totalVisits: 8,
@@ -203,6 +223,16 @@ export const INITIAL_PATIENTS: Patient[] = [
daysSinceLastVisit: 1,
status: 'active',
activeMembership: 'Athletic Recovery & Decompression Pass',
vitals: {
bloodPressure: '120/78',
heartRate: 64,
temperature: '98.2 °F',
oxygenSat: 99,
painLevel: 3,
bmi: '25.4',
allergies: [],
contraindications: ['Mild right rotator cuff impingement'],
},
carePlan: {
title: 'Thoracic Mobility & Biomechanical Alignment',
totalVisits: 6,
@@ -230,6 +260,16 @@ export const INITIAL_PATIENTS: Patient[] = [
nextAppointmentDate: '2026-09-08',
daysSinceLastVisit: 4,
status: 'active',
vitals: {
bloodPressure: '112/70',
heartRate: 78,
temperature: '98.7 °F',
oxygenSat: 100,
painLevel: 4,
bmi: '27.2',
allergies: ['Amoxicillin'],
contraindications: ['Pregnancy Week 32 - Webster technique prone pregnancy pillow required'],
},
carePlan: {
title: 'Webster Technique Pelvic Balance & Comfort',
totalVisits: 10,
+12
View File
@@ -46,6 +46,17 @@ export interface CarePlan {
status: 'on_track' | 'lagging' | 'at_risk' | 'completed';
}
export interface PatientVitals {
bloodPressure: string;
heartRate: number;
temperature: string;
oxygenSat: number;
painLevel: number;
bmi: string;
allergies: string[];
contraindications: string[];
}
export interface Patient {
id: string;
tenantId: string;
@@ -66,6 +77,7 @@ export interface Patient {
daysSinceLastVisit: number;
activeMembership?: string;
packageCreditsRemaining?: number;
vitals?: PatientVitals;
}
export type AppointmentStatus = 'booked' | 'confirmed' | 'arrived' | 'in_room' | 'completed' | 'no_show';