fix(wiring): multi-tenant isolation, in-calendar quick scheduler, telehealth note sync, and superbill history
This commit is contained in:
+210
-82
@@ -349,6 +349,127 @@ export default function Home() {
|
||||
setActiveTenantId(tenantId);
|
||||
setPortalMode('clinic');
|
||||
setClinicTab('calendar');
|
||||
|
||||
// Automatically sync active patient, note, and superbills for the selected clinic
|
||||
const tenantPatients = patients.filter((p) => p.tenantId === tenantId);
|
||||
if (tenantPatients.length > 0) {
|
||||
setActivePatient(tenantPatients[0]);
|
||||
const matchingSoap =
|
||||
soapNotes.find((s) => s.patientId === tenantPatients[0].id) ||
|
||||
soapNotes.find((s) => s.tenantId === tenantId);
|
||||
setActiveSoapNote(matchingSoap);
|
||||
}
|
||||
|
||||
const tenantSuperbills = superbills.filter((sb) => sb.tenantId === tenantId);
|
||||
if (tenantSuperbills.length > 0) {
|
||||
setActiveSuperbill(tenantSuperbills[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickScheduleAppointment = (newAptData: any) => {
|
||||
clinicalAudio.playSuccess();
|
||||
const newApt: Appointment = {
|
||||
id: `apt-${Date.now()}`,
|
||||
...newAptData,
|
||||
};
|
||||
|
||||
const existingPatient = patients.find((p) => p.id === newAptData.patientId);
|
||||
if (!existingPatient) {
|
||||
const nameParts = (newAptData.patientName || 'New Patient').split(' ');
|
||||
const firstName = nameParts[0] || 'Walk-In';
|
||||
const lastName = nameParts.slice(1).join(' ') || 'Patient';
|
||||
const newPatient: Patient = {
|
||||
id: newAptData.patientId,
|
||||
tenantId: activeTenant.id,
|
||||
firstName,
|
||||
lastName,
|
||||
email: `${firstName.toLowerCase()}.${lastName.toLowerCase()}@example.com`,
|
||||
phone: newAptData.patientPhone || '(555) 000-0000',
|
||||
dob: '1990-01-01',
|
||||
gender: 'Undisclosed',
|
||||
address: activeTenant.cityStateZip,
|
||||
insuranceName: 'Direct / Stripe Connect',
|
||||
insuranceId: 'SELF-PAY',
|
||||
chiefComplaint: newAptData.notes || newAptData.serviceType,
|
||||
daysSinceLastVisit: 0,
|
||||
lastVisitDate: newAptData.date,
|
||||
nextAppointmentDate: newAptData.date,
|
||||
status: 'active',
|
||||
carePlan: {
|
||||
title: 'Acute Relief & Biomechanical Alignment',
|
||||
totalVisits: 6,
|
||||
completedVisits: 1,
|
||||
frequency: '1-2x / week',
|
||||
targetCondition: newAptData.serviceType,
|
||||
startDate: newAptData.date,
|
||||
status: 'on_track',
|
||||
},
|
||||
};
|
||||
setPatients((prev) => [newPatient, ...prev]);
|
||||
setActivePatient(newPatient);
|
||||
} else {
|
||||
setActivePatient(existingPatient);
|
||||
}
|
||||
|
||||
setAppointments((prev) => [newApt, ...prev]);
|
||||
addToast(
|
||||
'success',
|
||||
'Encounter Scheduled',
|
||||
`${newApt.patientName} scheduled for ${newApt.time} (${newApt.serviceType})`
|
||||
);
|
||||
};
|
||||
|
||||
const handleEndTelehealthCall = (consultData: {
|
||||
subjective: string;
|
||||
objective: string;
|
||||
assessment: string;
|
||||
plan: string;
|
||||
duration: string;
|
||||
}) => {
|
||||
clinicalAudio.playSuccess();
|
||||
const today = new Date().toISOString().substring(0, 10);
|
||||
const updatedNote: SoapNote = {
|
||||
id: activeSoapNote?.id || `soap-${Date.now()}`,
|
||||
tenantId: activeTenant.id,
|
||||
patientId: activePatient.id,
|
||||
patientName: `${activePatient.firstName} ${activePatient.lastName}`,
|
||||
providerId: activeProvider.id,
|
||||
providerName: activeProvider.name,
|
||||
date: today,
|
||||
status: 'draft',
|
||||
discipline: 'chiropractic',
|
||||
vasScore: 4,
|
||||
subjective: consultData.subjective,
|
||||
objective: consultData.objective,
|
||||
assessment: consultData.assessment,
|
||||
plan: `${consultData.plan}\n\n[Telehealth Video Consult • Duration: ${consultData.duration} • Encrypted WebRTC Session]`,
|
||||
spinalAdjustments: activeSoapNote?.spinalAdjustments || [],
|
||||
icd10Codes: activeSoapNote?.icd10Codes || [
|
||||
{ code: 'M99.01', description: 'Segmental and somatic dysfunction of cervical region' },
|
||||
],
|
||||
cptCodes: [
|
||||
{ code: '99203', description: 'Office/Telehealth Outpatient Visit, 30 min', fee: 110 },
|
||||
{ code: '97110', description: 'Therapeutic Exercises & Ergonomic Protocol', fee: 45 },
|
||||
],
|
||||
};
|
||||
|
||||
setSoapNotes((prev) => {
|
||||
const idx = prev.findIndex((s) => s.patientId === activePatient.id);
|
||||
if (idx >= 0) {
|
||||
const copy = [...prev];
|
||||
copy[idx] = updatedNote;
|
||||
return copy;
|
||||
}
|
||||
return [updatedNote, ...prev];
|
||||
});
|
||||
|
||||
setActiveSoapNote(updatedNote);
|
||||
setClinicTab('charting');
|
||||
addToast(
|
||||
'success',
|
||||
'Telehealth Note Synced to Chart',
|
||||
`Clinical findings for ${activePatient.firstName} ${activePatient.lastName} transferred to SOAP editor (${consultData.duration})`
|
||||
);
|
||||
};
|
||||
|
||||
const handleUpdateProductStock = (productId: string, newStock: number) => {
|
||||
@@ -464,7 +585,7 @@ export default function Home() {
|
||||
<div className="relative">
|
||||
<select
|
||||
value={activeTenantId}
|
||||
onChange={(e) => setActiveTenantId(e.target.value)}
|
||||
onChange={(e) => handleSwitchTenant(e.target.value)}
|
||||
className="appearance-none bg-slate-50 hover:bg-slate-100 border border-slate-300 text-slate-800 text-xs font-bold rounded-lg px-3 py-1.5 pr-8 cursor-pointer focus:outline-none focus:border-sky-500 transition shadow-2xs"
|
||||
>
|
||||
{tenants.map((t) => (
|
||||
@@ -742,94 +863,101 @@ export default function Home() {
|
||||
|
||||
{/* Main Container */}
|
||||
<main className="flex-1 max-w-7xl w-full mx-auto p-4 sm:p-6 lg:p-8">
|
||||
{portalMode === 'clinic' && (
|
||||
<div>
|
||||
{clinicTab === 'calendar' && (
|
||||
<CalendarView
|
||||
appointments={appointments}
|
||||
providers={activeProviders}
|
||||
activeTenant={activeTenant}
|
||||
waitlistCount={waitlist.length}
|
||||
onSelectAppointment={handleSelectAppointment}
|
||||
onUpdateStatus={handleUpdateAppointmentStatus}
|
||||
onNewAppointmentClick={() => setPortalMode('patient')}
|
||||
onOpenWaitlist={() => setIsWaitlistOpen(true)}
|
||||
/>
|
||||
)}
|
||||
{portalMode === 'clinic' && (() => {
|
||||
const tenantPatients = patients.filter((p) => !p.tenantId || p.tenantId === activeTenant.id);
|
||||
const tenantSuperbills = superbills.filter((s) => !s.tenantId || s.tenantId === activeTenant.id);
|
||||
|
||||
{clinicTab === 'charting' && staffRole !== 'front_desk' && (
|
||||
<SoapChartEditor
|
||||
patient={activePatient}
|
||||
provider={activeProvider}
|
||||
initialSoapNote={activeSoapNote}
|
||||
onSaveSoapNote={handleSaveSoapNote}
|
||||
onGenerateSuperbill={handleGenerateSuperbillFromNote}
|
||||
onLaunchTelehealth={() => setClinicTab('telehealth')}
|
||||
onBackToCalendar={() => setClinicTab('calendar')}
|
||||
onOpenCourtVault={() => setIsCourtVaultOpen(true)}
|
||||
/>
|
||||
)}
|
||||
return (
|
||||
<div>
|
||||
{clinicTab === 'calendar' && (
|
||||
<CalendarView
|
||||
appointments={appointments}
|
||||
providers={activeProviders}
|
||||
activeTenant={activeTenant}
|
||||
patients={tenantPatients}
|
||||
waitlistCount={waitlist.length}
|
||||
onSelectAppointment={handleSelectAppointment}
|
||||
onUpdateStatus={handleUpdateAppointmentStatus}
|
||||
onNewAppointmentClick={() => setPortalMode('patient')}
|
||||
onQuickSchedule={handleQuickScheduleAppointment}
|
||||
onOpenWaitlist={() => setIsWaitlistOpen(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{clinicTab === 'telehealth' && staffRole !== 'front_desk' && (
|
||||
<TelehealthRoom
|
||||
patient={activePatient}
|
||||
provider={activeProvider}
|
||||
onEndCall={(noteData) => {
|
||||
setClinicTab('charting');
|
||||
}}
|
||||
onClose={() => setClinicTab('calendar')}
|
||||
/>
|
||||
)}
|
||||
{clinicTab === 'charting' && staffRole !== 'front_desk' && (
|
||||
<SoapChartEditor
|
||||
patient={activePatient}
|
||||
provider={activeProvider}
|
||||
initialSoapNote={activeSoapNote}
|
||||
onSaveSoapNote={handleSaveSoapNote}
|
||||
onGenerateSuperbill={handleGenerateSuperbillFromNote}
|
||||
onLaunchTelehealth={() => setClinicTab('telehealth')}
|
||||
onBackToCalendar={() => setClinicTab('calendar')}
|
||||
onOpenCourtVault={() => setIsCourtVaultOpen(true)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{clinicTab === 'retail' && (
|
||||
<RetailInventoryView
|
||||
products={products}
|
||||
activeTenant={activeTenant}
|
||||
onUpdateStock={handleUpdateProductStock}
|
||||
/>
|
||||
)}
|
||||
{clinicTab === 'telehealth' && staffRole !== 'front_desk' && (
|
||||
<TelehealthRoom
|
||||
patient={activePatient}
|
||||
provider={activeProvider}
|
||||
onEndCall={handleEndTelehealthCall}
|
||||
onClose={() => setClinicTab('calendar')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{clinicTab === 'memberships' && (
|
||||
<MembershipsView
|
||||
memberships={memberships}
|
||||
packages={packages}
|
||||
patients={patients}
|
||||
activeTenant={activeTenant}
|
||||
onEnrollPatient={handleEnrollPatientInMembership}
|
||||
/>
|
||||
)}
|
||||
{clinicTab === 'retail' && (
|
||||
<RetailInventoryView
|
||||
products={products}
|
||||
activeTenant={activeTenant}
|
||||
onUpdateStock={handleUpdateProductStock}
|
||||
/>
|
||||
)}
|
||||
|
||||
{clinicTab === 'billing' && (
|
||||
<SuperbillView
|
||||
superbill={activeSuperbill}
|
||||
activeTenant={activeTenant}
|
||||
onBack={() => setClinicTab('calendar')}
|
||||
onMarkPaid={handleMarkPaid}
|
||||
/>
|
||||
)}
|
||||
{clinicTab === 'memberships' && (
|
||||
<MembershipsView
|
||||
memberships={memberships}
|
||||
packages={packages}
|
||||
patients={tenantPatients}
|
||||
activeTenant={activeTenant}
|
||||
onEnrollPatient={handleEnrollPatientInMembership}
|
||||
/>
|
||||
)}
|
||||
|
||||
{clinicTab === 'retention' && (
|
||||
<RetentionView
|
||||
patients={patients}
|
||||
activeTenant={activeTenant}
|
||||
onSelectPatientChart={(patient) => {
|
||||
setActivePatient(patient);
|
||||
const existing = soapNotes.find((s) => s.patientId === patient.id);
|
||||
setActiveSoapNote(existing);
|
||||
setClinicTab('charting');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{clinicTab === 'billing' && (
|
||||
<SuperbillView
|
||||
superbill={activeSuperbill}
|
||||
superbills={tenantSuperbills}
|
||||
activeTenant={activeTenant}
|
||||
onBack={() => setClinicTab('calendar')}
|
||||
onSelectSuperbill={(sb) => setActiveSuperbill(sb)}
|
||||
onMarkPaid={handleMarkPaid}
|
||||
/>
|
||||
)}
|
||||
|
||||
{clinicTab === 'growth' && (
|
||||
<ClinicGrowthView
|
||||
activeTenant={activeTenant}
|
||||
activeProvider={activeProvider}
|
||||
onOpenBookingPortal={() => setPortalMode('patient')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{clinicTab === 'retention' && (
|
||||
<RetentionView
|
||||
patients={tenantPatients}
|
||||
activeTenant={activeTenant}
|
||||
onSelectPatientChart={(patient) => {
|
||||
setActivePatient(patient);
|
||||
const existing = soapNotes.find((s) => s.patientId === patient.id);
|
||||
setActiveSoapNote(existing);
|
||||
setClinicTab('charting');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{clinicTab === 'growth' && (
|
||||
<ClinicGrowthView
|
||||
activeTenant={activeTenant}
|
||||
activeProvider={activeProvider}
|
||||
onOpenBookingPortal={() => setPortalMode('patient')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
{portalMode === 'patient' && (
|
||||
<PatientPortalView
|
||||
|
||||
Reference in New Issue
Block a user