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:
+155
-5
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user