diff --git a/src/app/page.tsx b/src/app/page.tsx index a4805bd..6a359fb 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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() {
setSelectedPatientId(e.target.value)} + className="w-full p-2 bg-slate-50 border border-slate-200 rounded-lg text-slate-800 font-medium focus:bg-white focus:outline-none focus:border-sky-500" + > + {patients.map((p) => ( + + ))} + + ) : ( +
+ setNewPatientName(e.target.value)} + required={patientMode === 'new'} + className="p-2 bg-slate-50 border border-slate-200 rounded-lg text-slate-800 focus:bg-white focus:outline-none focus:border-sky-500" + /> + setNewPatientPhone(e.target.value)} + className="p-2 bg-slate-50 border border-slate-200 rounded-lg text-slate-800 focus:bg-white focus:outline-none focus:border-sky-500" + />
)}
- - {/* Clinical Action Bar */} -
- + {/* Attending Provider & Operatory Room */} +
+
+ + +
-
- {apt.status === 'booked' && ( -
-
+ + {/* Service Type & Fee */} +
+ +
+ {[ + { name: 'Spinal Adjustment & CMT', fee: 75, dur: 20 }, + { name: 'Initial Examination & CMT', fee: 145, dur: 45 }, + { name: 'Triton Disc Decompression', fee: 95, dur: 30 }, + { name: 'Webster Sacral Alignment', fee: 85, dur: 30 }, + ].map((s) => { + const isSelected = scheduleService === s.name; + return ( + + ); + })} +
+
+ + {/* Time Slots */} +
+ +
+ {['08:30 AM', '09:00 AM', '09:45 AM', '10:30 AM', '11:15 AM', '01:15 PM', '02:00 PM', '03:30 PM'].map( + (t) => ( + + ) + )} +
+
+ + {/* Telehealth toggle & Notes */} +
+
+
+ setIsTelehealthVisit(e.target.checked)} + className="w-4 h-4 text-purple-600 rounded border-slate-300" + /> +
+ +
+ + setScheduleNotes(e.target.value)} + className="w-full p-2 bg-slate-50 border border-slate-200 rounded-lg text-slate-800 focus:bg-white focus:outline-none focus:border-sky-500" + /> +
+ + {/* Submit Buttons */} +
+ + +
+
- ))} - + + )} ); }; diff --git a/src/components/charting/SoapChartEditor.tsx b/src/components/charting/SoapChartEditor.tsx index da0afa3..90657a4 100644 --- a/src/components/charting/SoapChartEditor.tsx +++ b/src/components/charting/SoapChartEditor.tsx @@ -90,25 +90,66 @@ export const SoapChartEditor: React.FC = ({ const [signedTimestamp, setSignedTimestamp] = useState(initialSoapNote?.signedAt); const [lastAutoSaved, setLastAutoSaved] = useState(null); - // Restore draft from local storage if note is un-signed + // Sync and restore state when patient or active note changes 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 {} + if (initialSoapNote) { + setDiscipline(initialSoapNote.discipline || 'chiropractic'); + setSubjective(initialSoapNote.subjective); + setObjective(initialSoapNote.objective); + setAssessment(initialSoapNote.assessment); + setPlan(initialSoapNote.plan); + setAdjustments(initialSoapNote.spinalAdjustments || []); + setSelectedIcd10(initialSoapNote.icd10Codes || []); + setSelectedCpt(initialSoapNote.cptCodes || []); + setVasScore(initialSoapNote.vasScore || 3); + setIsSigned(initialSoapNote.status === 'signed'); + setSignedTimestamp(initialSoapNote.signedAt); + } else { + let hasDraft = false; + if (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); + hasDraft = true; + setLastAutoSaved(new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })); + } + } catch {} + } + if (!hasDraft) { + setSubjective( + `Patient presents for scheduled visit ${patient.carePlan.completedVisits + 1} of ${ + patient.carePlan.totalVisits + }. Chief complaint: ${patient.chiefComplaint}. VAS pain reported at 4/10.` + ); + setObjective( + 'Postural examination reveals mild anterior head carriage and unleveling of right iliac crest. Motion palpation identifies segmental hypomobility with paraspinal guarding.' + ); + setAssessment( + 'Segmental and somatic dysfunction of spine. Patient progressing steadily toward functional goals outlined in care plan.' + ); + setPlan( + '1. High velocity low amplitude (HVLA) manipulative therapy delivered to listed subluxations.\n2. Prescribed postural home stabilization exercises.\n3. Return for scheduled care plan visit.' + ); + setVasScore(4); + setAdjustments([ + { vertebra: 'L4', region: 'Lumbar', listing: 'Right Posterior (RP)', technique: 'Diversified' }, + { vertebra: 'Sacrum', region: 'Pelvis/Sacrum', listing: 'Right Sacral Base Anterior', technique: 'Thompson Drop' }, + ]); + setSelectedIcd10([STANDARD_ICD10_CODES[2], STANDARD_ICD10_CODES[5]]); + setSelectedCpt([STANDARD_CPT_CODES[0], STANDARD_CPT_CODES[3]]); + } + setIsSigned(false); + setSignedTimestamp(undefined); } - }, [patient.id, initialSoapNote]); + }, [patient.id, initialSoapNote?.id]); // Debounced auto-save draft to local storage useEffect(() => { @@ -222,6 +263,7 @@ export const SoapChartEditor: React.FC = ({ } catch {} } + const encounterDate = initialSoapNote?.date || new Date().toISOString().substring(0, 10); const savedNote: SoapNote = { id: initialSoapNote?.id || `soap-${Date.now()}`, tenantId: patient.tenantId, @@ -229,7 +271,7 @@ export const SoapChartEditor: React.FC = ({ patientName: `${patient.firstName} ${patient.lastName}`, providerId: provider.id, providerName: provider.name, - date: '2026-09-05', + date: encounterDate, status: 'signed', discipline, vasScore, @@ -277,6 +319,7 @@ export const SoapChartEditor: React.FC = ({ const handleCreateSuperbillClick = () => { clinicalAudio.playSuccess(); + const encounterDate = initialSoapNote?.date || new Date().toISOString().substring(0, 10); const savedNote: SoapNote = { id: initialSoapNote?.id || `soap-${Date.now()}`, tenantId: patient.tenantId, @@ -284,7 +327,7 @@ export const SoapChartEditor: React.FC = ({ patientName: `${patient.firstName} ${patient.lastName}`, providerId: provider.id, providerName: provider.name, - date: '2026-09-05', + date: encounterDate, status: isSigned ? 'signed' : 'draft', discipline, vasScore, diff --git a/src/components/memberships/MembershipsView.tsx b/src/components/memberships/MembershipsView.tsx index a970305..13ef6e6 100644 --- a/src/components/memberships/MembershipsView.tsx +++ b/src/components/memberships/MembershipsView.tsx @@ -31,10 +31,18 @@ export const MembershipsView: React.FC = ({ activeTenant, onEnrollPatient, }) => { + const tenantPatients = patients.filter((p) => !p.tenantId || p.tenantId === activeTenant.id); const [selectedPlanForEnroll, setSelectedPlanForEnroll] = useState(null); - const [selectedPatientId, setSelectedPatientId] = useState(patients[0]?.id || ''); + const [selectedPatientId, setSelectedPatientId] = useState(tenantPatients[0]?.id || ''); const [enrollSuccess, setEnrollSuccess] = useState(false); + // Sync selected patient if clinic changes + React.useEffect(() => { + if (tenantPatients.length > 0 && !tenantPatients.some((p) => p.id === selectedPatientId)) { + setSelectedPatientId(tenantPatients[0].id); + } + }, [activeTenant.id, tenantPatients, selectedPatientId]); + const totalClinicMembershipMRR = memberships.reduce( (sum, m) => sum + m.mrrContribution, 0 @@ -238,7 +246,7 @@ export const MembershipsView: React.FC = ({ onChange={(e) => setSelectedPatientId(e.target.value)} className="w-full bg-slate-50 border border-slate-200 rounded-lg p-3 text-slate-900 focus:bg-white focus:outline-none focus:border-sky-500" > - {patients.map((p) => ( + {tenantPatients.map((p) => ( diff --git a/src/components/retention/RetentionView.tsx b/src/components/retention/RetentionView.tsx index 9f8edb3..c57a281 100644 --- a/src/components/retention/RetentionView.tsx +++ b/src/components/retention/RetentionView.tsx @@ -31,20 +31,21 @@ export const RetentionView: React.FC = ({ const [sentToast, setSentToast] = useState(false); const [filterType, setFilterType] = useState<'all' | 'dropout_risk' | 'active'>('dropout_risk'); - const dropoutPatients = patients.filter((p) => p.status === 'dropout_risk'); - const activePatients = patients.filter((p) => p.status === 'active'); + const tenantPatients = patients.filter((p) => !p.tenantId || p.tenantId === activeTenant.id); + const dropoutPatients = tenantPatients.filter((p) => p.status === 'dropout_risk'); + const activePatients = tenantPatients.filter((p) => p.status === 'active'); const displayedPatients = filterType === 'dropout_risk' ? dropoutPatients : filterType === 'active' ? activePatients - : patients; + : tenantPatients; const handleOpenSmsModal = (patient: Patient) => { setSelectedPatientForSms(patient); setCustomSmsMessage( - `Hi ${patient.firstName}! Dr. Vance noticed you're due for visit ${ + `Hi ${patient.firstName}! Our clinical team at ${activeTenant.name} noticed you're due for visit ${ patient.carePlan.completedVisits + 1 } of your ${patient.carePlan.title}. We want to ensure your recovery stays on track! Click here to pick a quick 15-min adjustment slot this week: https://${ activeTenant.domain diff --git a/src/lib/mock-data.ts b/src/lib/mock-data.ts index 24d0235..9bde943 100644 --- a/src/lib/mock-data.ts +++ b/src/lib/mock-data.ts @@ -576,6 +576,37 @@ export const INITIAL_SOAP_NOTES: SoapNote[] = [ signedAt: '2026-09-05T09:25:00-07:00', signedBy: 'Dr. Marcus Vance, D.C. (NPI 1942857391)', }, + { + id: 'soap-hope-1', + tenantId: 'tenant-hope', + patientId: 'pat-hope-1', + patientName: 'Chloe Adams', + providerId: 'prov-hope-1', + providerName: 'Dr. Hope Sullivan', + appointmentId: 'apt-hope-1', + date: '2026-09-08', + status: 'signed', + discipline: 'chiropractic', + vasScore: 4, + subjective: 'Patient reports persistent upper cervical stiffness and bilateral trapezius achiness after working 50+ hours at desk. No radicular arm numbness.', + objective: 'Cervical range of motion: Right rotation 60 deg with end-range pain, Left rotation 72 deg. Segmental restriction identified at C1-C2 and C5-C6 with suboccipital hypertonicity.', + assessment: 'Segmental somatic dysfunction of cervical region (M99.01) and cervicothoracic junction with postural strain.', + plan: '1. Diversified adjustment to C1 (Right Lateral Mass) and C5.\n2. Suboccipital myofascial release.\n3. Prescribed chin-tuck postural reset exercises.', + spinalAdjustments: [ + { vertebra: 'C1 (Atlas)', region: 'Cervical', listing: 'Right Lateral Mass Anterior', technique: 'Diversified', notes: 'Gentle rotary thrust with audible cavitation' }, + { vertebra: 'C5', region: 'Cervical', listing: 'Posterior Right', technique: 'Diversified', notes: 'Immediate reduction in hypertonicity' }, + ], + icd10Codes: [ + { code: 'M99.01', description: 'Segmental and somatic dysfunction of cervical region' }, + { code: 'M54.2', description: 'Cervicalgia / Neck Pain' }, + ], + cptCodes: [ + { code: '98940', description: 'Chiropractic Manipulative Treatment (CMT); Spinal, 1-2 Regions', fee: 55 }, + { code: '97140', description: 'Manual Therapy Techniques / Myofascial Release (15 min)', fee: 45 }, + ], + signedAt: '2026-09-08T09:30:00-04:00', + signedBy: 'Dr. Hope Sullivan, D.C. (NPI 1982736450)', + }, ]; export const INITIAL_SUPERBILLS: Superbill[] = [ @@ -609,6 +640,64 @@ export const INITIAL_SUPERBILLS: Superbill[] = [ paymentMethod: 'Stripe Card', generatedAt: '2026-09-05T09:30:00-07:00', }, + { + id: 'sb-2', + tenantId: 'tenant-1', + invoiceNumber: 'SB-2026-0902', + patientId: 'pat-2', + patientName: 'Sarah Holloway', + patientDob: '1993-11-20', + patientAddress: '924 Lincoln Ave, San Jose, CA 95125', + providerName: 'Dr. Marcus Vance, D.C.', + providerNpi: '1942857391', + clinicName: 'Apex Spine & Wellness Clinic', + clinicAddress: '1420 Meridian Ave, Suite 300, San Jose, CA 95125', + clinicTaxId: '84-2918471', + dateOfService: '2026-09-05', + posCode: '11 (Office)', + icd10Codes: [ + { code: 'M99.01', description: 'Segmental dysfunction cervical region' }, + { code: 'G44.209', description: 'Tension-type headache, unspecified' }, + ], + items: [ + { cptCode: '98940', description: 'CMT Spinal, 1-2 Regions', units: 1, rate: 55, total: 55 }, + { cptCode: '97110', description: 'Therapeutic Exercise (15 min)', units: 1, rate: 40, total: 40 }, + ], + totalAmount: 95, + patientPaid: 0, + balanceDue: 95, + paymentMethod: 'Unpaid', + generatedAt: '2026-09-05T14:30:00-07:00', + }, + { + id: 'sb-hope-1', + tenantId: 'tenant-hope', + invoiceNumber: 'SB-HOPE-2026-01', + patientId: 'pat-hope-1', + patientName: 'Chloe Adams', + patientDob: '1993-11-22', + patientAddress: '450 Monument Ave, Richmond, VA 23219', + providerName: 'Dr. Hope Sullivan, D.C.', + providerNpi: '1982736450', + clinicName: 'Hope Integrative Health & Chiropractic', + clinicAddress: '1250 Hope Valley Road, Suite 100, Richmond, VA 23219', + clinicTaxId: '54-9812734', + dateOfService: '2026-09-08', + posCode: '11 (Office)', + icd10Codes: [ + { code: 'M99.01', description: 'Segmental and somatic dysfunction of cervical region' }, + { code: 'M54.2', description: 'Cervicalgia / Neck Pain' }, + ], + items: [ + { cptCode: '98940', description: 'CMT Spinal, 1-2 Regions (Cervical)', units: 1, rate: 55, total: 55 }, + { cptCode: '97140', description: 'Manual Therapy Techniques / Myofascial Release', units: 1, rate: 45, total: 45 }, + ], + totalAmount: 100, + patientPaid: 100, + balanceDue: 0, + paymentMethod: 'Stripe Card', + generatedAt: '2026-09-08T09:45:00-04:00', + }, ]; export const INITIAL_PRODUCTS: RetailProduct[] = [