feat: complete Jane App feature set with Telehealth, Retail POS, Memberships, Multi-Discipline Charting, Waitlist & RBAC
This commit is contained in:
+193
-7
@@ -8,6 +8,10 @@ import {
|
|||||||
INITIAL_APPOINTMENTS,
|
INITIAL_APPOINTMENTS,
|
||||||
INITIAL_SOAP_NOTES,
|
INITIAL_SOAP_NOTES,
|
||||||
INITIAL_SUPERBILLS,
|
INITIAL_SUPERBILLS,
|
||||||
|
INITIAL_PRODUCTS,
|
||||||
|
INITIAL_MEMBERSHIPS,
|
||||||
|
INITIAL_PACKAGES,
|
||||||
|
INITIAL_WAITLIST,
|
||||||
} from '@/lib/mock-data';
|
} from '@/lib/mock-data';
|
||||||
import {
|
import {
|
||||||
ClinicTenant,
|
ClinicTenant,
|
||||||
@@ -15,6 +19,11 @@ import {
|
|||||||
SoapNote,
|
SoapNote,
|
||||||
Superbill,
|
Superbill,
|
||||||
Patient,
|
Patient,
|
||||||
|
RetailProduct,
|
||||||
|
WellnessMembership,
|
||||||
|
PrePaidPackage,
|
||||||
|
WaitlistEntry,
|
||||||
|
StaffRole,
|
||||||
} from '@/types/clinical';
|
} from '@/types/clinical';
|
||||||
import { CalendarView } from '@/components/calendar/CalendarView';
|
import { CalendarView } from '@/components/calendar/CalendarView';
|
||||||
import { SoapChartEditor } from '@/components/charting/SoapChartEditor';
|
import { SoapChartEditor } from '@/components/charting/SoapChartEditor';
|
||||||
@@ -22,6 +31,10 @@ import { SuperbillView } from '@/components/billing/SuperbillView';
|
|||||||
import { RetentionView } from '@/components/retention/RetentionView';
|
import { RetentionView } from '@/components/retention/RetentionView';
|
||||||
import { PatientPortalView } from '@/components/intake/PatientPortalView';
|
import { PatientPortalView } from '@/components/intake/PatientPortalView';
|
||||||
import { SuperAdminView } from '@/components/admin/SuperAdminView';
|
import { SuperAdminView } from '@/components/admin/SuperAdminView';
|
||||||
|
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 {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
FileText,
|
FileText,
|
||||||
@@ -32,6 +45,12 @@ import {
|
|||||||
ChevronDown,
|
ChevronDown,
|
||||||
Stethoscope,
|
Stethoscope,
|
||||||
Laptop,
|
Laptop,
|
||||||
|
ShoppingBag,
|
||||||
|
Repeat,
|
||||||
|
Video,
|
||||||
|
Shield,
|
||||||
|
ShieldAlert,
|
||||||
|
UserCheck,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
@@ -41,13 +60,23 @@ export default function Home() {
|
|||||||
// 'clinic' | 'patient' | 'superadmin'
|
// 'clinic' | 'patient' | 'superadmin'
|
||||||
const [portalMode, setPortalMode] = useState<'clinic' | 'patient' | 'superadmin'>('clinic');
|
const [portalMode, setPortalMode] = useState<'clinic' | 'patient' | 'superadmin'>('clinic');
|
||||||
|
|
||||||
// 'calendar' | 'charting' | 'billing' | 'retention'
|
// Staff Role: 'doctor' | 'front_desk' | 'billing_admin'
|
||||||
const [clinicTab, setClinicTab] = useState<'calendar' | 'charting' | 'billing' | 'retention'>('calendar');
|
const [staffRole, setStaffRole] = useState<StaffRole>('doctor');
|
||||||
|
|
||||||
|
// Clinic Sub-Tabs: 'calendar' | 'charting' | 'telehealth' | 'retail' | 'memberships' | 'billing' | 'retention'
|
||||||
|
const [clinicTab, setClinicTab] = useState<
|
||||||
|
'calendar' | 'charting' | 'telehealth' | 'retail' | 'memberships' | 'billing' | 'retention'
|
||||||
|
>('calendar');
|
||||||
|
|
||||||
const [appointments, setAppointments] = useState<Appointment[]>(INITIAL_APPOINTMENTS);
|
const [appointments, setAppointments] = useState<Appointment[]>(INITIAL_APPOINTMENTS);
|
||||||
const [patients, setPatients] = useState<Patient[]>(INITIAL_PATIENTS);
|
const [patients, setPatients] = useState<Patient[]>(INITIAL_PATIENTS);
|
||||||
const [soapNotes, setSoapNotes] = useState<SoapNote[]>(INITIAL_SOAP_NOTES);
|
const [soapNotes, setSoapNotes] = useState<SoapNote[]>(INITIAL_SOAP_NOTES);
|
||||||
const [superbills, setSuperbills] = useState<Superbill[]>(INITIAL_SUPERBILLS);
|
const [superbills, setSuperbills] = useState<Superbill[]>(INITIAL_SUPERBILLS);
|
||||||
|
const [products, setProducts] = useState<RetailProduct[]>(INITIAL_PRODUCTS);
|
||||||
|
const [memberships, setMemberships] = useState<WellnessMembership[]>(INITIAL_MEMBERSHIPS);
|
||||||
|
const [packages, setPackages] = useState<PrePaidPackage[]>(INITIAL_PACKAGES);
|
||||||
|
const [waitlist, setWaitlist] = useState<WaitlistEntry[]>(INITIAL_WAITLIST);
|
||||||
|
const [isWaitlistOpen, setIsWaitlistOpen] = useState(false);
|
||||||
|
|
||||||
const [activePatient, setActivePatient] = useState<Patient>(INITIAL_PATIENTS[0]);
|
const [activePatient, setActivePatient] = useState<Patient>(INITIAL_PATIENTS[0]);
|
||||||
const [activeSoapNote, setActiveSoapNote] = useState<SoapNote | undefined>(INITIAL_SOAP_NOTES[0]);
|
const [activeSoapNote, setActiveSoapNote] = useState<SoapNote | undefined>(INITIAL_SOAP_NOTES[0]);
|
||||||
@@ -63,7 +92,12 @@ export default function Home() {
|
|||||||
|
|
||||||
const existingSoap = soapNotes.find((s) => s.patientId === apt.patientId && s.appointmentId === apt.id);
|
const existingSoap = soapNotes.find((s) => s.patientId === apt.patientId && s.appointmentId === apt.id);
|
||||||
setActiveSoapNote(existingSoap);
|
setActiveSoapNote(existingSoap);
|
||||||
|
|
||||||
|
if (apt.isTelehealth) {
|
||||||
|
setClinicTab('telehealth');
|
||||||
|
} else {
|
||||||
setClinicTab('charting');
|
setClinicTab('charting');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateAppointmentStatus = (aptId: string, status: Appointment['status']) => {
|
const handleUpdateAppointmentStatus = (aptId: string, status: Appointment['status']) => {
|
||||||
@@ -159,6 +193,24 @@ export default function Home() {
|
|||||||
setClinicTab('calendar');
|
setClinicTab('calendar');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdateProductStock = (productId: string, newStock: number) => {
|
||||||
|
setProducts((prev) =>
|
||||||
|
prev.map((p) => (p.id === productId ? { ...p, stockQty: newStock } : p))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEnrollPatientInMembership = (patientId: string, planName: string) => {
|
||||||
|
setPatients((prev) =>
|
||||||
|
prev.map((p) => (p.id === patientId ? { ...p, activeMembership: planName } : p))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAutoFillWaitlistPatient = (entryId: string) => {
|
||||||
|
setWaitlist((prev) =>
|
||||||
|
prev.map((w) => (w.id === entryId ? { ...w, status: 'notified' } : w))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-slate-50 text-slate-900 flex flex-col">
|
<div className="min-h-screen bg-slate-50 text-slate-900 flex flex-col">
|
||||||
{/* Top Hospital Command Header */}
|
{/* Top Hospital Command Header */}
|
||||||
@@ -199,6 +251,52 @@ export default function Home() {
|
|||||||
</select>
|
</select>
|
||||||
<ChevronDown className="w-3.5 h-3.5 text-slate-500 absolute right-2.5 top-1/2 -translate-y-1/2 pointer-events-none" />
|
<ChevronDown className="w-3.5 h-3.5 text-slate-500 absolute right-2.5 top-1/2 -translate-y-1/2 pointer-events-none" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Staff Role Switcher (RBAC) */}
|
||||||
|
<div className="flex items-center gap-1 bg-slate-100 p-1 rounded-lg border border-slate-200 text-xs">
|
||||||
|
<span className="text-[10px] uppercase font-bold text-slate-500 px-1.5">Role:</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setStaffRole('doctor')}
|
||||||
|
className={`px-2 py-1 rounded text-xs font-bold transition ${
|
||||||
|
staffRole === 'doctor'
|
||||||
|
? 'bg-white text-sky-800 shadow-2xs border border-slate-200'
|
||||||
|
: 'text-slate-600 hover:text-slate-900'
|
||||||
|
}`}
|
||||||
|
title="Full Doctor Clinical Access"
|
||||||
|
>
|
||||||
|
Doctor (D.C.)
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setStaffRole('front_desk');
|
||||||
|
if (clinicTab === 'charting' || clinicTab === 'telehealth') {
|
||||||
|
setClinicTab('calendar');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className={`px-2 py-1 rounded text-xs font-bold transition ${
|
||||||
|
staffRole === 'front_desk'
|
||||||
|
? 'bg-amber-100 text-amber-900 shadow-2xs border border-amber-200'
|
||||||
|
: 'text-slate-600 hover:text-slate-900'
|
||||||
|
}`}
|
||||||
|
title="HIPAA-Safe: Hides Clinical Notes, Shows Schedule, Retail & Billing"
|
||||||
|
>
|
||||||
|
Front Desk
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setStaffRole('billing_admin')}
|
||||||
|
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'
|
||||||
|
: 'text-slate-600 hover:text-slate-900'
|
||||||
|
}`}
|
||||||
|
title="Financial & Billing Administrator"
|
||||||
|
>
|
||||||
|
Billing Admin
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Portal Switcher Tabs */}
|
{/* Portal Switcher Tabs */}
|
||||||
@@ -260,6 +358,8 @@ export default function Home() {
|
|||||||
Schedule & Encounters
|
Schedule & Encounters
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{/* Clinical Charts - Hidden if in Front Desk HIPAA-safe mode */}
|
||||||
|
{staffRole !== 'front_desk' ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setClinicTab('charting')}
|
onClick={() => setClinicTab('charting')}
|
||||||
@@ -270,7 +370,53 @@ export default function Home() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<FileText className="w-3.5 h-3.5 text-sky-700" />
|
<FileText className="w-3.5 h-3.5 text-sky-700" />
|
||||||
Clinical SOAP & Spine Map ({activePatient.firstName} {activePatient.lastName})
|
SOAP Chart & Spine ({activePatient.firstName} {activePatient.lastName})
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<span className="px-2.5 py-1 text-[11px] text-amber-700 bg-amber-50 rounded-md border border-amber-200 font-medium flex items-center gap-1">
|
||||||
|
<Shield className="w-3 h-3 text-amber-600" /> HIPAA: Notes Protected
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{staffRole !== 'front_desk' && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setClinicTab('telehealth')}
|
||||||
|
className={`px-3 py-1.5 rounded-md font-semibold flex items-center gap-1.5 transition whitespace-nowrap ${
|
||||||
|
clinicTab === 'telehealth'
|
||||||
|
? 'bg-purple-50 text-purple-800 border border-purple-200 shadow-2xs font-bold'
|
||||||
|
: 'text-slate-600 hover:text-slate-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Video className="w-3.5 h-3.5 text-purple-600" />
|
||||||
|
Telehealth Virtual Room
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setClinicTab('retail')}
|
||||||
|
className={`px-3 py-1.5 rounded-md font-semibold flex items-center gap-1.5 transition whitespace-nowrap ${
|
||||||
|
clinicTab === 'retail'
|
||||||
|
? 'bg-sky-50 text-sky-800 border border-sky-200 shadow-2xs font-bold'
|
||||||
|
: 'text-slate-600 hover:text-slate-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ShoppingBag className="w-3.5 h-3.5 text-sky-700" />
|
||||||
|
Retail & Supplement POS
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setClinicTab('memberships')}
|
||||||
|
className={`px-3 py-1.5 rounded-md font-semibold flex items-center gap-1.5 transition whitespace-nowrap ${
|
||||||
|
clinicTab === 'memberships'
|
||||||
|
? 'bg-sky-50 text-sky-800 border border-sky-200 shadow-2xs font-bold'
|
||||||
|
: 'text-slate-600 hover:text-slate-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Repeat className="w-3.5 h-3.5 text-sky-700" />
|
||||||
|
Memberships & Packages
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@@ -296,7 +442,7 @@ export default function Home() {
|
|||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Users className="w-3.5 h-3.5 text-amber-600" />
|
<Users className="w-3.5 h-3.5 text-amber-600" />
|
||||||
Care Plan Retention Radar
|
Retention Radar
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -311,28 +457,60 @@ export default function Home() {
|
|||||||
appointments={appointments}
|
appointments={appointments}
|
||||||
providers={activeProviders}
|
providers={activeProviders}
|
||||||
activeTenant={activeTenant}
|
activeTenant={activeTenant}
|
||||||
|
waitlistCount={waitlist.length}
|
||||||
onSelectAppointment={handleSelectAppointment}
|
onSelectAppointment={handleSelectAppointment}
|
||||||
onUpdateStatus={handleUpdateAppointmentStatus}
|
onUpdateStatus={handleUpdateAppointmentStatus}
|
||||||
onNewAppointmentClick={() => setPortalMode('patient')}
|
onNewAppointmentClick={() => setPortalMode('patient')}
|
||||||
|
onOpenWaitlist={() => setIsWaitlistOpen(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{clinicTab === 'charting' && (
|
{clinicTab === 'charting' && staffRole !== 'front_desk' && (
|
||||||
<SoapChartEditor
|
<SoapChartEditor
|
||||||
patient={activePatient}
|
patient={activePatient}
|
||||||
provider={activeProvider}
|
provider={activeProvider}
|
||||||
initialSoapNote={activeSoapNote}
|
initialSoapNote={activeSoapNote}
|
||||||
onSaveSoapNote={handleSaveSoapNote}
|
onSaveSoapNote={handleSaveSoapNote}
|
||||||
onGenerateSuperbill={handleGenerateSuperbillFromNote}
|
onGenerateSuperbill={handleGenerateSuperbillFromNote}
|
||||||
|
onLaunchTelehealth={() => setClinicTab('telehealth')}
|
||||||
onBackToCalendar={() => setClinicTab('calendar')}
|
onBackToCalendar={() => setClinicTab('calendar')}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{clinicTab === 'telehealth' && staffRole !== 'front_desk' && (
|
||||||
|
<TelehealthRoom
|
||||||
|
patient={activePatient}
|
||||||
|
provider={activeProvider}
|
||||||
|
onEndCall={(noteData) => {
|
||||||
|
setClinicTab('charting');
|
||||||
|
}}
|
||||||
|
onClose={() => setClinicTab('calendar')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{clinicTab === 'retail' && (
|
||||||
|
<RetailInventoryView
|
||||||
|
products={products}
|
||||||
|
activeTenant={activeTenant}
|
||||||
|
onUpdateStock={handleUpdateProductStock}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{clinicTab === 'memberships' && (
|
||||||
|
<MembershipsView
|
||||||
|
memberships={memberships}
|
||||||
|
packages={packages}
|
||||||
|
patients={patients}
|
||||||
|
activeTenant={activeTenant}
|
||||||
|
onEnrollPatient={handleEnrollPatientInMembership}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{clinicTab === 'billing' && (
|
{clinicTab === 'billing' && (
|
||||||
<SuperbillView
|
<SuperbillView
|
||||||
superbill={activeSuperbill}
|
superbill={activeSuperbill}
|
||||||
activeTenant={activeTenant}
|
activeTenant={activeTenant}
|
||||||
onBack={() => setClinicTab('charting')}
|
onBack={() => setClinicTab('calendar')}
|
||||||
onMarkPaid={handleMarkPaid}
|
onMarkPaid={handleMarkPaid}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -369,11 +547,19 @@ export default function Home() {
|
|||||||
)}
|
)}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
{/* Cancellation Waitlist Modal */}
|
||||||
|
<WaitlistModal
|
||||||
|
waitlist={waitlist}
|
||||||
|
isOpen={isWaitlistOpen}
|
||||||
|
onClose={() => setIsWaitlistOpen(false)}
|
||||||
|
onAutoFillPatient={handleAutoFillWaitlistPatient}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Hospital Footer */}
|
{/* Hospital Footer */}
|
||||||
<footer className="bg-white border-t border-slate-200 py-4 px-6 text-center text-xs text-slate-500">
|
<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">
|
<div className="flex flex-col sm:flex-row items-center justify-between max-w-7xl mx-auto gap-2">
|
||||||
<span className="font-medium text-slate-600">
|
<span className="font-medium text-slate-600">
|
||||||
Mediusa Clinic OS • Hospital-Grade Practice Management Platform
|
Mediusa Clinic OS • Complete Hospital-Grade Practice Management Platform
|
||||||
</span>
|
</span>
|
||||||
<span className="font-mono text-slate-500">
|
<span className="font-mono text-slate-500">
|
||||||
hello@aipilots.site • HIPAA Certified • Sub-30s Clinical Charting Standard
|
hello@aipilots.site • HIPAA Certified • Sub-30s Clinical Charting Standard
|
||||||
|
|||||||
@@ -13,24 +13,30 @@ import {
|
|||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
Stethoscope,
|
Stethoscope,
|
||||||
|
Video,
|
||||||
|
Users,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
interface CalendarViewProps {
|
interface CalendarViewProps {
|
||||||
appointments: Appointment[];
|
appointments: Appointment[];
|
||||||
providers: Provider[];
|
providers: Provider[];
|
||||||
activeTenant: ClinicTenant;
|
activeTenant: ClinicTenant;
|
||||||
|
waitlistCount?: number;
|
||||||
onSelectAppointment: (apt: Appointment) => void;
|
onSelectAppointment: (apt: Appointment) => void;
|
||||||
onUpdateStatus: (aptId: string, status: Appointment['status']) => void;
|
onUpdateStatus: (aptId: string, status: Appointment['status']) => void;
|
||||||
onNewAppointmentClick: () => void;
|
onNewAppointmentClick: () => void;
|
||||||
|
onOpenWaitlist: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CalendarView: React.FC<CalendarViewProps> = ({
|
export const CalendarView: React.FC<CalendarViewProps> = ({
|
||||||
appointments,
|
appointments,
|
||||||
providers,
|
providers,
|
||||||
activeTenant,
|
activeTenant,
|
||||||
|
waitlistCount = 3,
|
||||||
onSelectAppointment,
|
onSelectAppointment,
|
||||||
onUpdateStatus,
|
onUpdateStatus,
|
||||||
onNewAppointmentClick,
|
onNewAppointmentClick,
|
||||||
|
onOpenWaitlist,
|
||||||
}) => {
|
}) => {
|
||||||
const [selectedProviderId, setSelectedProviderId] = useState<string>('all');
|
const [selectedProviderId, setSelectedProviderId] = useState<string>('all');
|
||||||
|
|
||||||
@@ -112,7 +118,7 @@ export const CalendarView: React.FC<CalendarViewProps> = ({
|
|||||||
onClick={() => setSelectedProviderId('all')}
|
onClick={() => setSelectedProviderId('all')}
|
||||||
className={`px-3 py-1 rounded-md font-semibold transition ${
|
className={`px-3 py-1 rounded-md font-semibold transition ${
|
||||||
selectedProviderId === 'all'
|
selectedProviderId === 'all'
|
||||||
? 'bg-white text-sky-800 shadow-xs border border-slate-200/60'
|
? 'bg-white text-sky-800 shadow-xs border border-slate-200/80'
|
||||||
: 'text-slate-600 hover:text-slate-900'
|
: 'text-slate-600 hover:text-slate-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -125,7 +131,7 @@ export const CalendarView: React.FC<CalendarViewProps> = ({
|
|||||||
onClick={() => setSelectedProviderId(p.id)}
|
onClick={() => setSelectedProviderId(p.id)}
|
||||||
className={`px-3 py-1 rounded-md font-semibold transition ${
|
className={`px-3 py-1 rounded-md font-semibold transition ${
|
||||||
selectedProviderId === p.id
|
selectedProviderId === p.id
|
||||||
? 'bg-white text-sky-800 shadow-xs border border-slate-200/60'
|
? 'bg-white text-sky-800 shadow-xs border border-slate-200/80'
|
||||||
: 'text-slate-600 hover:text-slate-900'
|
: 'text-slate-600 hover:text-slate-900'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -135,6 +141,16 @@ export const CalendarView: React.FC<CalendarViewProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onOpenWaitlist}
|
||||||
|
className="px-3 py-2 rounded-lg bg-slate-100 hover:bg-slate-200 text-slate-700 text-xs font-bold flex items-center gap-1.5 border border-slate-200 transition"
|
||||||
|
>
|
||||||
|
<Users className="w-3.5 h-3.5 text-sky-700" />
|
||||||
|
Cancellation Waitlist ({waitlistCount})
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onNewAppointmentClick}
|
onClick={onNewAppointmentClick}
|
||||||
@@ -144,6 +160,7 @@ export const CalendarView: React.FC<CalendarViewProps> = ({
|
|||||||
Schedule Patient
|
Schedule Patient
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Grid of Hospital Patient Encounter Cards */}
|
{/* Grid of Hospital Patient Encounter Cards */}
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
@@ -163,6 +180,11 @@ export const CalendarView: React.FC<CalendarViewProps> = ({
|
|||||||
<span className="text-[11px] text-slate-500 font-medium">
|
<span className="text-[11px] text-slate-500 font-medium">
|
||||||
{apt.durationMinutes} min
|
{apt.durationMinutes} min
|
||||||
</span>
|
</span>
|
||||||
|
{apt.isTelehealth && (
|
||||||
|
<span className="px-2 py-0.5 rounded bg-purple-50 text-purple-700 text-[10px] font-bold border border-purple-200 flex items-center gap-1">
|
||||||
|
<Video className="w-3 h-3 text-purple-600" /> Telehealth
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{getStatusBadge(apt.status)}
|
{getStatusBadge(apt.status)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,18 +1,27 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { SoapNote, Patient, Provider, SpinalAdjustmentEntry, CptCodeItem, Icd10CodeItem } from '@/types/clinical';
|
import {
|
||||||
|
SoapNote,
|
||||||
|
Patient,
|
||||||
|
Provider,
|
||||||
|
SpinalAdjustmentEntry,
|
||||||
|
CptCodeItem,
|
||||||
|
Icd10CodeItem,
|
||||||
|
ClinicalDiscipline,
|
||||||
|
} from '@/types/clinical';
|
||||||
import { SpineVisualizer } from '@/components/ui/SpineVisualizer';
|
import { SpineVisualizer } from '@/components/ui/SpineVisualizer';
|
||||||
import { AmbientAudioRecorder } from '@/components/ui/AmbientAudioRecorder';
|
import { AmbientAudioRecorder } from '@/components/ui/AmbientAudioRecorder';
|
||||||
import { STANDARD_CPT_CODES, STANDARD_ICD10_CODES } from '@/lib/mock-data';
|
import { STANDARD_CPT_CODES, STANDARD_ICD10_CODES, DISCIPLINE_PRESETS } from '@/lib/mock-data';
|
||||||
import {
|
import {
|
||||||
FileText,
|
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Copy,
|
Copy,
|
||||||
DollarSign,
|
DollarSign,
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
Tag,
|
Tag,
|
||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
|
Video,
|
||||||
|
Layers,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
interface SoapChartEditorProps {
|
interface SoapChartEditorProps {
|
||||||
@@ -21,6 +30,7 @@ interface SoapChartEditorProps {
|
|||||||
initialSoapNote?: SoapNote;
|
initialSoapNote?: SoapNote;
|
||||||
onSaveSoapNote: (note: SoapNote) => void;
|
onSaveSoapNote: (note: SoapNote) => void;
|
||||||
onGenerateSuperbill: (note: SoapNote) => void;
|
onGenerateSuperbill: (note: SoapNote) => void;
|
||||||
|
onLaunchTelehealth: () => void;
|
||||||
onBackToCalendar: () => void;
|
onBackToCalendar: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,8 +40,13 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
|
|||||||
initialSoapNote,
|
initialSoapNote,
|
||||||
onSaveSoapNote,
|
onSaveSoapNote,
|
||||||
onGenerateSuperbill,
|
onGenerateSuperbill,
|
||||||
|
onLaunchTelehealth,
|
||||||
onBackToCalendar,
|
onBackToCalendar,
|
||||||
}) => {
|
}) => {
|
||||||
|
const [discipline, setDiscipline] = useState<ClinicalDiscipline>(
|
||||||
|
initialSoapNote?.discipline || 'chiropractic'
|
||||||
|
);
|
||||||
|
|
||||||
const [subjective, setSubjective] = useState(
|
const [subjective, setSubjective] = useState(
|
||||||
initialSoapNote?.subjective ||
|
initialSoapNote?.subjective ||
|
||||||
`Patient presents for scheduled visit ${patient.carePlan.completedVisits + 1} of ${
|
`Patient presents for scheduled visit ${patient.carePlan.completedVisits + 1} of ${
|
||||||
@@ -93,6 +108,23 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
|
|||||||
if (matchedCpts.length > 0) setSelectedCpt(matchedCpts);
|
if (matchedCpts.length > 0) setSelectedCpt(matchedCpts);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleSwitchDiscipline = (newDiscipline: ClinicalDiscipline) => {
|
||||||
|
setDiscipline(newDiscipline);
|
||||||
|
const preset = DISCIPLINE_PRESETS[newDiscipline];
|
||||||
|
if (preset) {
|
||||||
|
setSubjective(preset.sampleSubjective);
|
||||||
|
setObjective(preset.sampleObjective);
|
||||||
|
setAssessment(preset.sampleAssessment);
|
||||||
|
setPlan(preset.samplePlan);
|
||||||
|
|
||||||
|
const matchedIcds = STANDARD_ICD10_CODES.filter((icd) => preset.icd10Codes.includes(icd.code));
|
||||||
|
if (matchedIcds.length > 0) setSelectedIcd10(matchedIcds);
|
||||||
|
|
||||||
|
const matchedCpts = STANDARD_CPT_CODES.filter((cpt) => preset.cptCodes.includes(cpt.code));
|
||||||
|
if (matchedCpts.length > 0) setSelectedCpt(matchedCpts);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleToggleAdjustment = (entry: SpinalAdjustmentEntry) => {
|
const handleToggleAdjustment = (entry: SpinalAdjustmentEntry) => {
|
||||||
setAdjustments((prev) => {
|
setAdjustments((prev) => {
|
||||||
const exists = prev.some((a) => a.vertebra === entry.vertebra);
|
const exists = prev.some((a) => a.vertebra === entry.vertebra);
|
||||||
@@ -125,6 +157,7 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
|
|||||||
providerName: provider.name,
|
providerName: provider.name,
|
||||||
date: '2026-09-05',
|
date: '2026-09-05',
|
||||||
status: 'signed',
|
status: 'signed',
|
||||||
|
discipline,
|
||||||
vasScore,
|
vasScore,
|
||||||
subjective,
|
subjective,
|
||||||
objective,
|
objective,
|
||||||
@@ -150,6 +183,7 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
|
|||||||
providerName: provider.name,
|
providerName: provider.name,
|
||||||
date: '2026-09-05',
|
date: '2026-09-05',
|
||||||
status: isSigned ? 'signed' : 'draft',
|
status: isSigned ? 'signed' : 'draft',
|
||||||
|
discipline,
|
||||||
vasScore,
|
vasScore,
|
||||||
subjective,
|
subjective,
|
||||||
objective,
|
objective,
|
||||||
@@ -187,6 +221,11 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
|
|||||||
<span className="text-xs px-2.5 py-0.5 rounded-full bg-sky-50 text-sky-800 border border-sky-200 font-semibold">
|
<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}
|
Care Plan: Visit {patient.carePlan.completedVisits + 1} of {patient.carePlan.totalVisits}
|
||||||
</span>
|
</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>
|
||||||
|
)}
|
||||||
{isSigned ? (
|
{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">
|
<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 & Locked
|
<ShieldCheck className="w-3.5 h-3.5" /> Signed & Locked
|
||||||
@@ -209,7 +248,16 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onLaunchTelehealth}
|
||||||
|
className="px-3.5 py-2 rounded-lg bg-sky-100 hover:bg-sky-200 text-sky-900 text-xs font-bold flex items-center gap-1.5 transition border border-sky-200"
|
||||||
|
>
|
||||||
|
<Video className="w-3.5 h-3.5 text-sky-700" />
|
||||||
|
Launch Telehealth
|
||||||
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleCloneLastNote}
|
onClick={handleCloneLastNote}
|
||||||
@@ -244,10 +292,40 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Multi-Discipline Template Presets Bar */}
|
||||||
|
<div className="bg-white border border-slate-200 rounded-xl p-3 shadow-xs flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs">
|
||||||
|
<div className="flex items-center gap-2 font-bold text-slate-700">
|
||||||
|
<Layers className="w-4 h-4 text-sky-700" />
|
||||||
|
<span>Clinical Discipline Template:</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1 bg-slate-100 p-1 rounded-lg border border-slate-200">
|
||||||
|
{[
|
||||||
|
{ id: 'chiropractic', label: '🦴 Chiropractic (Spine CMT)' },
|
||||||
|
{ id: 'physical_therapy', label: '🏃 Physical Therapy & Rehab' },
|
||||||
|
{ id: 'acupuncture', label: '🪡 Acupuncture Meridian' },
|
||||||
|
{ id: 'massage_therapy', label: '💆 Medical Massage' },
|
||||||
|
].map((d) => (
|
||||||
|
<button
|
||||||
|
key={d.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleSwitchDiscipline(d.id as ClinicalDiscipline)}
|
||||||
|
className={`px-3 py-1.5 rounded-md font-semibold transition whitespace-nowrap ${
|
||||||
|
discipline === d.id
|
||||||
|
? 'bg-white text-sky-800 shadow-xs border border-slate-200/80'
|
||||||
|
: 'text-slate-600 hover:text-slate-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{d.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Ambient AI Scribe */}
|
{/* Ambient AI Scribe */}
|
||||||
<AmbientAudioRecorder onApplyExtractedSoap={handleApplyExtractedSoap} />
|
<AmbientAudioRecorder onApplyExtractedSoap={handleApplyExtractedSoap} />
|
||||||
|
|
||||||
{/* Spine Subluxation Visualizer */}
|
{/* Spine Subluxation Visualizer (shown prominently for chiropractic & physical therapy) */}
|
||||||
<SpineVisualizer
|
<SpineVisualizer
|
||||||
adjustments={adjustments}
|
adjustments={adjustments}
|
||||||
onToggleAdjustment={handleToggleAdjustment}
|
onToggleAdjustment={handleToggleAdjustment}
|
||||||
|
|||||||
@@ -0,0 +1,277 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { WellnessMembership, PrePaidPackage, Patient, ClinicTenant } from '@/types/clinical';
|
||||||
|
import {
|
||||||
|
Sparkles,
|
||||||
|
DollarSign,
|
||||||
|
Users,
|
||||||
|
Repeat,
|
||||||
|
Package,
|
||||||
|
CheckCircle2,
|
||||||
|
Plus,
|
||||||
|
ArrowRight,
|
||||||
|
ShieldCheck,
|
||||||
|
CreditCard,
|
||||||
|
Layers,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface MembershipsViewProps {
|
||||||
|
memberships: WellnessMembership[];
|
||||||
|
packages: PrePaidPackage[];
|
||||||
|
patients: Patient[];
|
||||||
|
activeTenant: ClinicTenant;
|
||||||
|
onEnrollPatient: (patientId: string, planName: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MembershipsView: React.FC<MembershipsViewProps> = ({
|
||||||
|
memberships,
|
||||||
|
packages,
|
||||||
|
patients,
|
||||||
|
activeTenant,
|
||||||
|
onEnrollPatient,
|
||||||
|
}) => {
|
||||||
|
const [selectedPlanForEnroll, setSelectedPlanForEnroll] = useState<string | null>(null);
|
||||||
|
const [selectedPatientId, setSelectedPatientId] = useState<string>(patients[0]?.id || '');
|
||||||
|
const [enrollSuccess, setEnrollSuccess] = useState(false);
|
||||||
|
|
||||||
|
const totalClinicMembershipMRR = memberships.reduce(
|
||||||
|
(sum, m) => sum + m.mrrContribution,
|
||||||
|
0
|
||||||
|
);
|
||||||
|
const totalActiveMembers = memberships.reduce((sum, m) => sum + m.activeMembers, 0);
|
||||||
|
|
||||||
|
const handleEnrollSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!selectedPlanForEnroll || !selectedPatientId) return;
|
||||||
|
|
||||||
|
onEnrollPatient(selectedPatientId, selectedPlanForEnroll);
|
||||||
|
setEnrollSuccess(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
setEnrollSuccess(false);
|
||||||
|
setSelectedPlanForEnroll(null);
|
||||||
|
}, 1500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Top Banner */}
|
||||||
|
<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>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="p-1.5 rounded-lg bg-sky-50 text-sky-700 border border-sky-100">
|
||||||
|
<Repeat className="w-4 h-4" />
|
||||||
|
</span>
|
||||||
|
<h3 className="font-bold text-slate-900 text-base tracking-tight">
|
||||||
|
Wellness Memberships & Pre-Paid Visit Packages
|
||||||
|
</h3>
|
||||||
|
<span className="text-xs font-semibold px-2.5 py-0.5 rounded-full bg-emerald-50 text-emerald-800 border border-emerald-200">
|
||||||
|
Stripe Auto-Draft Enabled
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500 mt-1">
|
||||||
|
Build predictable monthly recurring revenue for the clinic with recurring patient subscriptions and pre-paid visit blocks.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 text-xs font-mono">
|
||||||
|
<div className="bg-slate-50 p-3 rounded-lg border border-slate-200">
|
||||||
|
<div className="text-slate-500 text-[10px] uppercase tracking-wider">
|
||||||
|
Clinic Membership MRR
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-black text-emerald-700 font-mono">
|
||||||
|
${totalClinicMembershipMRR.toLocaleString()}.00/mo
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-slate-50 p-3 rounded-lg border border-slate-200">
|
||||||
|
<div className="text-slate-500 text-[10px] uppercase tracking-wider">
|
||||||
|
Enrolled Members
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-black text-sky-800 font-mono">
|
||||||
|
{totalActiveMembers} Patients
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section 1: Recurring Monthly Subscriptions */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h4 className="text-sm font-bold text-slate-900 flex items-center gap-1.5">
|
||||||
|
<Repeat className="w-4 h-4 text-sky-700" />
|
||||||
|
Recurring Monthly Wellness Memberships
|
||||||
|
</h4>
|
||||||
|
<span className="text-xs text-slate-500">Auto-billed on the 1st of each month</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-5">
|
||||||
|
{memberships.map((m) => (
|
||||||
|
<div
|
||||||
|
key={m.id}
|
||||||
|
className="bg-white border border-slate-200 rounded-xl p-5 shadow-xs hover:shadow-md transition flex flex-col justify-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<h5 className="font-bold text-base text-slate-900">{m.name}</h5>
|
||||||
|
<div className="text-xs text-sky-700 font-medium mt-0.5">
|
||||||
|
{m.includedVisits} adjustments / month
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-mono bg-sky-50 text-sky-800 px-2 py-0.5 rounded font-bold border border-sky-100">
|
||||||
|
{m.activeMembers} active
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 text-2xl font-black text-slate-900 font-mono">
|
||||||
|
${m.monthlyFee}
|
||||||
|
<span className="text-xs font-normal text-slate-500 font-sans"> / month</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-slate-600 mt-2 leading-relaxed">{m.description}</p>
|
||||||
|
|
||||||
|
<div className="mt-3 p-2.5 bg-slate-50 rounded-lg border border-slate-200 text-xs text-slate-700 space-y-1">
|
||||||
|
<div className="flex items-center gap-1.5 font-medium">
|
||||||
|
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-600 shrink-0" />
|
||||||
|
<span>{m.additionalVisitDiscount}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 font-medium">
|
||||||
|
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-600 shrink-0" />
|
||||||
|
<span>Unused visits rollover for 60 days</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedPlanForEnroll(m.name)}
|
||||||
|
className="mt-4 w-full py-2 px-4 bg-sky-700 hover:bg-sky-800 text-white font-bold rounded-lg text-xs transition shadow-xs flex items-center justify-center gap-1.5"
|
||||||
|
>
|
||||||
|
<Plus className="w-3.5 h-3.5" />
|
||||||
|
Enroll Patient in Plan
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Section 2: Pre-Paid Visit Packages */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<h4 className="text-sm font-bold text-slate-900 flex items-center gap-1.5">
|
||||||
|
<Layers className="w-4 h-4 text-emerald-700" />
|
||||||
|
Pre-Paid Visit Block Packages (Cash Discount)
|
||||||
|
</h4>
|
||||||
|
<span className="text-xs text-slate-500">Auto-decrements remaining visits on chart</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-5">
|
||||||
|
{packages.map((pkg) => (
|
||||||
|
<div
|
||||||
|
key={pkg.id}
|
||||||
|
className="bg-white border border-slate-200 rounded-xl p-5 shadow-xs flex flex-col justify-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<h5 className="font-bold text-base text-slate-900">{pkg.name}</h5>
|
||||||
|
<div className="text-xs text-emerald-700 font-medium mt-0.5">
|
||||||
|
{pkg.totalVisits} Total Sessions Included
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs font-mono bg-emerald-50 text-emerald-800 px-2 py-0.5 rounded font-bold border border-emerald-200">
|
||||||
|
{pkg.activeSold} active in clinic
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-3 flex items-baseline gap-2">
|
||||||
|
<span className="text-2xl font-black text-slate-900 font-mono">
|
||||||
|
${pkg.price}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-slate-500">
|
||||||
|
(${pkg.perVisitRate}/visit)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-slate-600 mt-2">{pkg.savings}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedPlanForEnroll(pkg.name)}
|
||||||
|
className="mt-4 w-full py-2 px-4 bg-emerald-700 hover:bg-emerald-800 text-white font-bold rounded-lg text-xs transition shadow-xs flex items-center justify-center gap-1.5"
|
||||||
|
>
|
||||||
|
<CreditCard className="w-3.5 h-3.5" />
|
||||||
|
Sell Package to Patient
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Enrollment Modal */}
|
||||||
|
{selectedPlanForEnroll && (
|
||||||
|
<div className="fixed inset-0 z-50 bg-slate-900/40 backdrop-blur-xs flex items-center justify-center p-4">
|
||||||
|
<div className="bg-white border border-slate-200 rounded-2xl max-w-md w-full p-6 shadow-xl text-slate-900">
|
||||||
|
<div className="flex items-center justify-between pb-3 border-b border-slate-100">
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-slate-900 text-sm">Enroll Patient in Plan</h4>
|
||||||
|
<p className="text-xs text-sky-800 font-semibold">{selectedPlanForEnroll}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{enrollSuccess ? (
|
||||||
|
<div className="py-6 text-center text-emerald-800">
|
||||||
|
<CheckCircle2 className="w-10 h-10 text-emerald-600 mx-auto mb-2 animate-bounce" />
|
||||||
|
<h4 className="font-bold text-sm">Patient Enrolled Successfully!</h4>
|
||||||
|
<p className="text-xs text-slate-500 mt-1">
|
||||||
|
Stripe recurring auto-draft scheduled and membership badge added to patient EHR chart.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={handleEnrollSubmit} className="mt-4 space-y-4 text-xs">
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-600 mb-1 font-medium">Select Patient</label>
|
||||||
|
<select
|
||||||
|
value={selectedPatientId}
|
||||||
|
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) => (
|
||||||
|
<option key={p.id} value={p.id}>
|
||||||
|
{p.firstName} {p.lastName} ({p.phone})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-3 bg-sky-50 border border-sky-100 rounded-lg text-xs text-sky-900 space-y-1">
|
||||||
|
<div className="font-bold">Automated Billing Rules:</div>
|
||||||
|
<div>• Card on file charged automatically via Stripe Connect.</div>
|
||||||
|
<div>• Patient receives digital membership card & SMS receipt.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-3 border-t border-slate-100 flex justify-end gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedPlanForEnroll(null)}
|
||||||
|
className="px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 font-semibold rounded-lg transition"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className="px-5 py-2 bg-sky-700 hover:bg-sky-800 text-white font-bold rounded-lg shadow-xs transition"
|
||||||
|
>
|
||||||
|
Confirm & Activate Subscription
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,335 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { RetailProduct, ClinicTenant } from '@/types/clinical';
|
||||||
|
import {
|
||||||
|
ShoppingBag,
|
||||||
|
Package,
|
||||||
|
Search,
|
||||||
|
Plus,
|
||||||
|
Minus,
|
||||||
|
Trash2,
|
||||||
|
CheckCircle2,
|
||||||
|
CreditCard,
|
||||||
|
AlertTriangle,
|
||||||
|
Receipt,
|
||||||
|
DollarSign,
|
||||||
|
Barcode,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface RetailInventoryViewProps {
|
||||||
|
products: RetailProduct[];
|
||||||
|
activeTenant: ClinicTenant;
|
||||||
|
onUpdateStock: (productId: string, newStock: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CartItem {
|
||||||
|
product: RetailProduct;
|
||||||
|
quantity: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RetailInventoryView: React.FC<RetailInventoryViewProps> = ({
|
||||||
|
products,
|
||||||
|
activeTenant,
|
||||||
|
onUpdateStock,
|
||||||
|
}) => {
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<string>('All');
|
||||||
|
const [cart, setCart] = useState<CartItem[]>([]);
|
||||||
|
const [checkoutSuccess, setCheckoutSuccess] = useState(false);
|
||||||
|
const [lastReceiptNumber, setLastReceiptNumber] = useState('');
|
||||||
|
|
||||||
|
const filteredProducts = products.filter((p) => {
|
||||||
|
const matchesSearch =
|
||||||
|
p.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
p.sku.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
|
const matchesCategory =
|
||||||
|
selectedCategory === 'All' || p.category === selectedCategory;
|
||||||
|
return matchesSearch && matchesCategory;
|
||||||
|
});
|
||||||
|
|
||||||
|
const addToCart = (product: RetailProduct) => {
|
||||||
|
setCart((prev) => {
|
||||||
|
const existing = prev.find((item) => item.product.id === product.id);
|
||||||
|
if (existing) {
|
||||||
|
return prev.map((item) =>
|
||||||
|
item.product.id === product.id
|
||||||
|
? { ...item, quantity: item.quantity + 1 }
|
||||||
|
: item
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [...prev, { product, quantity: 1 }];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateQuantity = (productId: string, delta: number) => {
|
||||||
|
setCart((prev) =>
|
||||||
|
prev
|
||||||
|
.map((item) => {
|
||||||
|
if (item.product.id === productId) {
|
||||||
|
const newQty = item.quantity + delta;
|
||||||
|
return newQty > 0 ? { ...item, quantity: newQty } : null;
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
})
|
||||||
|
.filter(Boolean) as CartItem[]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeFromCart = (productId: string) => {
|
||||||
|
setCart((prev) => prev.filter((item) => item.product.id !== productId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const subtotal = cart.reduce((sum, item) => sum + item.product.price * item.quantity, 0);
|
||||||
|
const tax = subtotal * 0.0825; // 8.25% CA sales tax
|
||||||
|
const total = subtotal + tax;
|
||||||
|
|
||||||
|
const handleCompleteSale = () => {
|
||||||
|
// Deduct stock
|
||||||
|
cart.forEach((item) => {
|
||||||
|
onUpdateStock(item.product.id, Math.max(0, item.product.stockQty - item.quantity));
|
||||||
|
});
|
||||||
|
|
||||||
|
const receiptId = `RCP-2026-${Math.floor(10000 + Math.random() * 90000)}`;
|
||||||
|
setLastReceiptNumber(receiptId);
|
||||||
|
setCheckoutSuccess(true);
|
||||||
|
setCart([]);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
setCheckoutSuccess(false);
|
||||||
|
}, 4000);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Top Banner */}
|
||||||
|
<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>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="p-1.5 rounded-lg bg-sky-50 text-sky-700 border border-sky-100">
|
||||||
|
<ShoppingBag className="w-4 h-4" />
|
||||||
|
</span>
|
||||||
|
<h3 className="font-bold text-slate-900 text-base tracking-tight">
|
||||||
|
Clinic Retail & Supplement Point of Sale (POS)
|
||||||
|
</h3>
|
||||||
|
<span className="text-xs font-semibold px-2.5 py-0.5 rounded-full bg-emerald-50 text-emerald-800 border border-emerald-200">
|
||||||
|
Stripe Terminal Integrated
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-slate-500 mt-1">
|
||||||
|
Sell professional supplements, cervical orthotics, topical pain relief, and rehab tools directly at front desk checkout.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 text-xs font-mono bg-slate-50 p-2 rounded-lg border border-slate-200">
|
||||||
|
<span className="text-slate-500">Retail Revenue This Month:</span>
|
||||||
|
<span className="font-bold text-slate-900">$4,820.00</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||||
|
{/* Left 8 Cols: Product Catalog & Search */}
|
||||||
|
<div className="lg:col-span-8 space-y-4">
|
||||||
|
<div className="bg-white border border-slate-200 rounded-xl p-4 shadow-xs flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||||
|
{/* Search Bar */}
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Search className="w-4 h-4 text-slate-400 absolute left-3 top-1/2 -translate-y-1/2" />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search products by name, SKU, or brand..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="w-full pl-9 pr-3 py-2 bg-slate-50 border border-slate-200 rounded-lg text-xs text-slate-900 focus:bg-white focus:outline-none focus:border-sky-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Category Filter */}
|
||||||
|
<div className="flex items-center gap-1 overflow-x-auto text-xs">
|
||||||
|
{['All', 'Supplements', 'Orthotics & Pillows', 'Topical & Pain Relief', 'Rehab Gear'].map(
|
||||||
|
(cat) => (
|
||||||
|
<button
|
||||||
|
key={cat}
|
||||||
|
type="button"
|
||||||
|
onClick={() => setSelectedCategory(cat)}
|
||||||
|
className={`px-3 py-1.5 rounded-md font-semibold transition whitespace-nowrap ${
|
||||||
|
selectedCategory === cat
|
||||||
|
? 'bg-sky-700 text-white shadow-xs'
|
||||||
|
: 'bg-slate-100 text-slate-600 hover:text-slate-900'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{cat}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Product Grid */}
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
{filteredProducts.map((p) => {
|
||||||
|
const isLowStock = p.stockQty <= p.reorderPoint;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={p.id}
|
||||||
|
className="bg-white border border-slate-200 rounded-xl p-4 shadow-xs hover:shadow-md transition flex flex-col justify-between"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-start justify-between gap-2">
|
||||||
|
<div>
|
||||||
|
<span className="text-[10px] font-mono font-bold uppercase tracking-wider text-slate-500">
|
||||||
|
{p.category}
|
||||||
|
</span>
|
||||||
|
<h4 className="font-bold text-slate-900 text-sm mt-0.5 leading-snug">
|
||||||
|
{p.name}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-sm font-bold text-sky-800 bg-sky-50 px-2 py-0.5 rounded border border-sky-100 shrink-0">
|
||||||
|
${p.price.toFixed(2)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-3 text-xs text-slate-500 mt-2">
|
||||||
|
<span className="font-mono">SKU: {p.sku}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>Supplier: {p.supplier}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 pt-3 border-t border-slate-100 flex items-center justify-between">
|
||||||
|
<div>
|
||||||
|
{isLowStock ? (
|
||||||
|
<span className="text-[11px] text-amber-700 font-bold flex items-center gap-1">
|
||||||
|
<AlertTriangle className="w-3 h-3 text-amber-600" />
|
||||||
|
Low Stock: {p.stockQty} left
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-[11px] text-slate-600 font-medium">
|
||||||
|
In Stock: <strong>{p.stockQty}</strong> units
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => addToCart(p)}
|
||||||
|
disabled={p.stockQty === 0}
|
||||||
|
className="px-3 py-1.5 bg-sky-700 hover:bg-sky-800 disabled:bg-slate-200 disabled:text-slate-400 text-white text-xs font-bold rounded-lg transition flex items-center gap-1.5 shadow-xs"
|
||||||
|
>
|
||||||
|
<Plus className="w-3.5 h-3.5" />
|
||||||
|
Add to Register
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right 4 Cols: Front Desk Register / Cart */}
|
||||||
|
<div className="lg:col-span-4 bg-white border border-slate-200 rounded-xl p-5 shadow-xs flex flex-col justify-between h-fit">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between pb-3 border-b border-slate-100">
|
||||||
|
<h4 className="font-bold text-slate-900 text-sm flex items-center gap-1.5">
|
||||||
|
<Receipt className="w-4 h-4 text-sky-700" />
|
||||||
|
Register Checkout
|
||||||
|
</h4>
|
||||||
|
<span className="text-xs font-mono font-bold text-sky-800 bg-sky-50 px-2 py-0.5 rounded">
|
||||||
|
{cart.reduce((sum, item) => sum + item.quantity, 0)} Items
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{checkoutSuccess && (
|
||||||
|
<div className="my-3 p-3 bg-emerald-50 border border-emerald-200 rounded-lg text-xs text-emerald-800 text-center animate-in fade-in">
|
||||||
|
<CheckCircle2 className="w-6 h-6 text-emerald-600 mx-auto mb-1" />
|
||||||
|
<div className="font-bold">Transaction Complete!</div>
|
||||||
|
<div className="text-[11px] text-emerald-700 font-mono mt-0.5">
|
||||||
|
Receipt #{lastReceiptNumber}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{cart.length === 0 ? (
|
||||||
|
<div className="py-12 text-center text-slate-400">
|
||||||
|
<Package className="w-8 h-8 mx-auto mb-2 opacity-50 text-slate-400" />
|
||||||
|
<p className="text-xs font-medium">Cart is empty</p>
|
||||||
|
<p className="text-[11px] text-slate-400 mt-1">
|
||||||
|
Click "Add to Register" on any supplement or product.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="divide-y divide-slate-100 mt-2 max-h-[320px] overflow-y-auto pr-1">
|
||||||
|
{cart.map((item) => (
|
||||||
|
<div key={item.product.id} className="py-3 flex items-center justify-between gap-2">
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div className="text-xs font-bold text-slate-900 truncate">
|
||||||
|
{item.product.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-slate-500 font-mono">
|
||||||
|
${item.product.price.toFixed(2)} ea
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-1.5 shrink-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => updateQuantity(item.product.id, -1)}
|
||||||
|
className="w-6 h-6 rounded bg-slate-100 hover:bg-slate-200 text-slate-700 flex items-center justify-center transition"
|
||||||
|
>
|
||||||
|
<Minus className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
<span className="w-6 text-center text-xs font-mono font-bold text-slate-900">
|
||||||
|
{item.quantity}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => updateQuantity(item.product.id, 1)}
|
||||||
|
className="w-6 h-6 rounded bg-slate-100 hover:bg-slate-200 text-slate-700 flex items-center justify-center transition"
|
||||||
|
>
|
||||||
|
<Plus className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => removeFromCart(item.product.id)}
|
||||||
|
className="text-slate-400 hover:text-red-600 p-1 ml-1 transition"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Cart Totals & Checkout Button */}
|
||||||
|
{cart.length > 0 && (
|
||||||
|
<div className="mt-4 pt-4 border-t border-slate-100 space-y-2 text-xs">
|
||||||
|
<div className="flex justify-between text-slate-600">
|
||||||
|
<span>Subtotal:</span>
|
||||||
|
<span className="font-mono text-slate-900 font-semibold">${subtotal.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between text-slate-600">
|
||||||
|
<span>Estimated Sales Tax (8.25%):</span>
|
||||||
|
<span className="font-mono text-slate-900">${tax.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="pt-2 border-t border-slate-200 flex justify-between text-sm font-bold text-slate-900">
|
||||||
|
<span>Total Due:</span>
|
||||||
|
<span className="font-mono text-sky-800 text-base">${total.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleCompleteSale}
|
||||||
|
className="mt-3 w-full py-2.5 px-4 bg-emerald-700 hover:bg-emerald-800 text-white font-bold rounded-lg shadow-xs transition flex items-center justify-center gap-2 text-xs"
|
||||||
|
>
|
||||||
|
<CreditCard className="w-4 h-4" />
|
||||||
|
Charge with Stripe (${total.toFixed(2)})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,265 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Patient, Provider, SoapNote, SpinalAdjustmentEntry } from '@/types/clinical';
|
||||||
|
import {
|
||||||
|
Video,
|
||||||
|
VideoOff,
|
||||||
|
Mic,
|
||||||
|
MicOff,
|
||||||
|
PhoneOff,
|
||||||
|
ShieldCheck,
|
||||||
|
Maximize2,
|
||||||
|
FileText,
|
||||||
|
User,
|
||||||
|
Sparkles,
|
||||||
|
CheckCircle,
|
||||||
|
Clock,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface TelehealthRoomProps {
|
||||||
|
patient: Patient;
|
||||||
|
provider: Provider;
|
||||||
|
onEndCall: (soapNoteData: any) => void;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TelehealthRoom: React.FC<TelehealthRoomProps> = ({
|
||||||
|
patient,
|
||||||
|
provider,
|
||||||
|
onEndCall,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const [isMicOn, setIsMicOn] = useState(true);
|
||||||
|
const [isVideoOn, setIsVideoOn] = useState(true);
|
||||||
|
const [callDurationSecs, setCallDurationSecs] = useState(0);
|
||||||
|
|
||||||
|
// Live SOAP notes during telehealth
|
||||||
|
const [subjective, setSubjective] = useState(
|
||||||
|
`Virtual Ergonomic Consult: Patient ${patient.firstName} connects from home workstation. Reports persistent cervicothoracic fatigue and tension headaches around 3 PM daily.`
|
||||||
|
);
|
||||||
|
const [objective, setObjective] = useState(
|
||||||
|
'Visual examination via video: Noticeable forward head posture (approx 2 inches anterior). Screen height is 4 inches below eye level. Active cervical rotation right: 55 deg, left: 75 deg.'
|
||||||
|
);
|
||||||
|
const [assessment, setAssessment] = useState(
|
||||||
|
'Cervicogenic cephalalgia and upper cross postural imbalance secondary to workstation ergonomics.'
|
||||||
|
);
|
||||||
|
const [plan, setPlan] = useState(
|
||||||
|
'1. Instructed patient to elevate laptop monitor by 3.5 inches.\n2. Prescribed chin-tuck isometric contractions and doorway pectoralis stretching.\n3. Follow up in-clinic in 5 days for spinal adjustment.'
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
setCallDurationSecs((prev) => prev + 1);
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const formatTimer = (secs: number) => {
|
||||||
|
const mins = Math.floor(secs / 60);
|
||||||
|
const remainder = secs % 60;
|
||||||
|
return `${mins.toString().padStart(2, '0')}:${remainder.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFinishConsult = () => {
|
||||||
|
onEndCall({
|
||||||
|
subjective,
|
||||||
|
objective,
|
||||||
|
assessment,
|
||||||
|
plan,
|
||||||
|
duration: formatTimer(callDurationSecs),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Telehealth Top Header */}
|
||||||
|
<div className="bg-slate-900 border border-slate-800 rounded-xl p-4 text-white flex items-center justify-between shadow-md">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-3 h-3 rounded-full bg-emerald-500 animate-pulse" />
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-bold text-sm">
|
||||||
|
Virtual Operatory: {patient.firstName} {patient.lastName}
|
||||||
|
</h3>
|
||||||
|
<span className="text-[11px] font-mono px-2 py-0.5 rounded bg-slate-800 text-sky-400 border border-slate-700 font-bold">
|
||||||
|
{formatTimer(callDurationSecs)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-[11px] text-slate-400 mt-0.5 flex items-center gap-2">
|
||||||
|
<span>Attending: {provider.name}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span className="flex items-center gap-1 text-emerald-400 font-medium">
|
||||||
|
<ShieldCheck className="w-3.5 h-3.5" /> HIPAA End-to-End Encrypted WebRTC
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleFinishConsult}
|
||||||
|
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white font-bold text-xs rounded-lg transition flex items-center gap-1.5 shadow-xs"
|
||||||
|
>
|
||||||
|
<PhoneOff className="w-4 h-4" />
|
||||||
|
End Consult & Save Note
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Split Screen Grid: Left Video / Right Live Charting */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-12 gap-5">
|
||||||
|
{/* Left 6 Cols: Video Feeds & Controls */}
|
||||||
|
<div className="lg:col-span-6 flex flex-col justify-between space-y-4">
|
||||||
|
{/* Main Patient Video Window */}
|
||||||
|
<div className="relative bg-slate-900 border border-slate-800 rounded-2xl h-[340px] sm:h-[400px] overflow-hidden flex items-center justify-center shadow-lg">
|
||||||
|
{/* Simulated Patient Video Feed */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-t from-slate-950 via-slate-900 to-slate-950 flex flex-col items-center justify-center p-6 text-center">
|
||||||
|
<div className="w-24 h-24 rounded-full bg-sky-950/80 border-2 border-sky-500/40 flex items-center justify-center shadow-xl text-sky-300 font-bold text-2xl mb-3">
|
||||||
|
{patient.firstName[0]}
|
||||||
|
{patient.lastName[0]}
|
||||||
|
</div>
|
||||||
|
<h4 className="font-bold text-white text-base">
|
||||||
|
{patient.firstName} {patient.lastName} (Patient)
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-emerald-400 mt-1 flex items-center gap-1">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-emerald-400 animate-ping" />
|
||||||
|
Connected • 1080p 60fps HD Stream
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Doctor Self-View Picture-in-Picture */}
|
||||||
|
<div className="absolute top-4 right-4 w-32 h-24 bg-slate-800 rounded-xl border border-slate-700 overflow-hidden shadow-2xl flex flex-col items-center justify-center text-white">
|
||||||
|
{isVideoOn ? (
|
||||||
|
<div className="text-center p-2">
|
||||||
|
<div className="w-8 h-8 rounded-full bg-sky-700 mx-auto flex items-center justify-center text-xs font-bold mb-1">
|
||||||
|
Dr
|
||||||
|
</div>
|
||||||
|
<span className="text-[10px] text-slate-300 font-semibold block truncate max-w-[100px]">
|
||||||
|
You (Dr. Vance)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-slate-500 text-[10px] flex flex-col items-center">
|
||||||
|
<VideoOff className="w-4 h-4 mb-1 text-slate-400" />
|
||||||
|
Camera Off
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* In-Call Controls Bar */}
|
||||||
|
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-3 bg-slate-950/90 backdrop-blur-md px-4 py-2 rounded-full border border-slate-700 shadow-2xl">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsMicOn(!isMicOn)}
|
||||||
|
className={`p-2.5 rounded-full transition ${
|
||||||
|
isMicOn ? 'bg-slate-800 hover:bg-slate-700 text-white' : 'bg-red-500/20 text-red-400 border border-red-500/40'
|
||||||
|
}`}
|
||||||
|
title={isMicOn ? 'Mute Mic' : 'Unmute Mic'}
|
||||||
|
>
|
||||||
|
{isMicOn ? <Mic className="w-4 h-4" /> : <MicOff className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setIsVideoOn(!isVideoOn)}
|
||||||
|
className={`p-2.5 rounded-full transition ${
|
||||||
|
isVideoOn ? 'bg-slate-800 hover:bg-slate-700 text-white' : 'bg-red-500/20 text-red-400 border border-red-500/40'
|
||||||
|
}`}
|
||||||
|
title={isVideoOn ? 'Turn Off Camera' : 'Turn On Camera'}
|
||||||
|
>
|
||||||
|
{isVideoOn ? <Video className="w-4 h-4" /> : <VideoOff className="w-4 h-4" />}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleFinishConsult}
|
||||||
|
className="px-4 py-2 bg-red-600 hover:bg-red-700 text-white text-xs font-bold rounded-full transition flex items-center gap-1.5"
|
||||||
|
>
|
||||||
|
<PhoneOff className="w-3.5 h-3.5" />
|
||||||
|
End Call
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right 6 Cols: Side-by-Side Live SOAP Charting */}
|
||||||
|
<div className="lg:col-span-6 bg-white border border-slate-200 rounded-2xl p-5 shadow-xs flex flex-col justify-between space-y-4">
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center justify-between pb-3 border-b border-slate-100">
|
||||||
|
<h4 className="font-bold text-slate-900 text-sm flex items-center gap-1.5">
|
||||||
|
<FileText className="w-4 h-4 text-sky-700" />
|
||||||
|
Simultaneous Telehealth Clinical Note
|
||||||
|
</h4>
|
||||||
|
<span className="text-[11px] text-sky-800 bg-sky-50 font-semibold px-2 py-0.5 rounded border border-sky-100">
|
||||||
|
CPT 99203 / 97110
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3 mt-3 text-xs">
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-700 font-bold mb-1">
|
||||||
|
Subjective History (Video Consult)
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={2}
|
||||||
|
value={subjective}
|
||||||
|
onChange={(e) => setSubjective(e.target.value)}
|
||||||
|
className="w-full bg-slate-50 border border-slate-200 rounded-lg p-2.5 text-xs text-slate-800 focus:bg-white focus:outline-none focus:border-sky-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-700 font-bold mb-1">
|
||||||
|
Objective Visual Assessment & Ergonomics
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={2}
|
||||||
|
value={objective}
|
||||||
|
onChange={(e) => setObjective(e.target.value)}
|
||||||
|
className="w-full bg-slate-50 border border-slate-200 rounded-lg p-2.5 text-xs text-slate-800 focus:bg-white focus:outline-none focus:border-sky-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-700 font-bold mb-1">
|
||||||
|
Clinical Assessment
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={2}
|
||||||
|
value={assessment}
|
||||||
|
onChange={(e) => setAssessment(e.target.value)}
|
||||||
|
className="w-full bg-slate-50 border border-slate-200 rounded-lg p-2.5 text-xs text-slate-800 focus:bg-white focus:outline-none focus:border-sky-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="block text-slate-700 font-bold mb-1">
|
||||||
|
Home Rehab & In-Person Follow-Up Plan
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
rows={2}
|
||||||
|
value={plan}
|
||||||
|
onChange={(e) => setPlan(e.target.value)}
|
||||||
|
className="w-full bg-slate-50 border border-slate-200 rounded-lg p-2.5 text-xs text-slate-800 focus:bg-white focus:outline-none focus:border-sky-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-3 border-t border-slate-100 flex items-center justify-between">
|
||||||
|
<span className="text-[11px] text-slate-500">
|
||||||
|
Note automatically syncs to patient's medical records upon ending call.
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={handleFinishConsult}
|
||||||
|
className="px-4 py-2 bg-sky-700 hover:bg-sky-800 text-white text-xs font-bold rounded-lg shadow-xs transition"
|
||||||
|
>
|
||||||
|
Save & Complete Visit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import { WaitlistEntry } from '@/types/clinical';
|
||||||
|
import {
|
||||||
|
Users,
|
||||||
|
Clock,
|
||||||
|
Phone,
|
||||||
|
Send,
|
||||||
|
CheckCircle2,
|
||||||
|
X,
|
||||||
|
Sparkles,
|
||||||
|
AlertCircle,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
interface WaitlistModalProps {
|
||||||
|
waitlist: WaitlistEntry[];
|
||||||
|
isOpen: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
onAutoFillPatient: (entryId: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const WaitlistModal: React.FC<WaitlistModalProps> = ({
|
||||||
|
waitlist,
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
onAutoFillPatient,
|
||||||
|
}) => {
|
||||||
|
const [notifiedEntryId, setNotifiedEntryId] = useState<string | null>(null);
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
const handleNotify = (id: string) => {
|
||||||
|
setNotifiedEntryId(id);
|
||||||
|
onAutoFillPatient(id);
|
||||||
|
setTimeout(() => {
|
||||||
|
setNotifiedEntryId(null);
|
||||||
|
}, 2500);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 bg-slate-900/40 backdrop-blur-xs flex items-center justify-center p-4">
|
||||||
|
<div className="bg-white border border-slate-200 rounded-2xl max-w-xl w-full p-6 shadow-xl text-slate-900 animate-in fade-in zoom-in-95 duration-150">
|
||||||
|
<div className="flex items-center justify-between pb-3 border-b border-slate-100">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="p-2 rounded-lg bg-sky-50 text-sky-700">
|
||||||
|
<Users className="w-5 h-5" />
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<h4 className="font-bold text-slate-900 text-sm">
|
||||||
|
Cancellation Waitlist & Auto-Fill Engine
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-slate-500">
|
||||||
|
Instantly fill canceled operatory slots with waiting patients via 2-way SMS
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-slate-400 hover:text-slate-600 p-1"
|
||||||
|
>
|
||||||
|
<X className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 space-y-3">
|
||||||
|
{waitlist.map((item) => {
|
||||||
|
const isJustNotified = notifiedEntryId === item.id;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className="p-3.5 bg-slate-50 border border-slate-200 rounded-xl flex flex-col sm:flex-row sm:items-center justify-between gap-3 text-xs"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="font-bold text-slate-900 text-sm">{item.patientName}</span>
|
||||||
|
<span className="text-[10px] font-mono bg-sky-100 text-sky-800 px-2 py-0.5 rounded font-bold">
|
||||||
|
{item.requestedService}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-3 text-slate-500 mt-1">
|
||||||
|
<span className="flex items-center gap-1 font-mono">
|
||||||
|
<Phone className="w-3 h-3 text-sky-700" />
|
||||||
|
{item.phone}
|
||||||
|
</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{item.preferredDays}</span>
|
||||||
|
<span>•</span>
|
||||||
|
<span>{item.preferredTimeRange}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{item.notes && (
|
||||||
|
<div className="mt-1 text-[11px] text-slate-600 italic">
|
||||||
|
Note: "{item.notes}"
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="shrink-0">
|
||||||
|
{isJustNotified ? (
|
||||||
|
<span className="px-3 py-1.5 bg-emerald-100 text-emerald-800 rounded-lg font-bold flex items-center gap-1 text-xs">
|
||||||
|
<CheckCircle2 className="w-3.5 h-3.5 text-emerald-600" />
|
||||||
|
SMS Dispatched!
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleNotify(item.id)}
|
||||||
|
className="px-3.5 py-1.5 bg-sky-700 hover:bg-sky-800 text-white font-bold rounded-lg transition flex items-center gap-1.5 shadow-2xs"
|
||||||
|
>
|
||||||
|
<Send className="w-3.5 h-3.5" />
|
||||||
|
Dispatch Open Slot
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 pt-3 border-t border-slate-100 flex items-center justify-between text-xs text-slate-500">
|
||||||
|
<span>First patient to reply YES is booked automatically.</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 bg-slate-100 hover:bg-slate-200 text-slate-700 font-semibold rounded-lg transition"
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
+251
-67
@@ -1,4 +1,18 @@
|
|||||||
import { ClinicTenant, Provider, Patient, Appointment, SoapNote, Superbill, IntakeSubmission, CptCodeItem, Icd10CodeItem } from '@/types/clinical';
|
import {
|
||||||
|
ClinicTenant,
|
||||||
|
Provider,
|
||||||
|
Patient,
|
||||||
|
Appointment,
|
||||||
|
SoapNote,
|
||||||
|
Superbill,
|
||||||
|
IntakeSubmission,
|
||||||
|
CptCodeItem,
|
||||||
|
Icd10CodeItem,
|
||||||
|
RetailProduct,
|
||||||
|
WellnessMembership,
|
||||||
|
PrePaidPackage,
|
||||||
|
WaitlistEntry,
|
||||||
|
} from '@/types/clinical';
|
||||||
|
|
||||||
export const STANDARD_CPT_CODES: CptCodeItem[] = [
|
export const STANDARD_CPT_CODES: CptCodeItem[] = [
|
||||||
{ code: '98940', description: 'Chiropractic Manipulative Treatment (CMT); Spinal, 1-2 Regions', fee: 55 },
|
{ code: '98940', description: 'Chiropractic Manipulative Treatment (CMT); Spinal, 1-2 Regions', fee: 55 },
|
||||||
@@ -7,6 +21,8 @@ export const STANDARD_CPT_CODES: CptCodeItem[] = [
|
|||||||
{ code: '97140', description: 'Manual Therapy Techniques (Mobilization, Myofascial Release, 15 min)', fee: 45 },
|
{ code: '97140', description: 'Manual Therapy Techniques (Mobilization, Myofascial Release, 15 min)', fee: 45 },
|
||||||
{ code: '97110', description: 'Therapeutic Exercises to Develop Strength/Endurance (15 min)', fee: 50 },
|
{ code: '97110', description: 'Therapeutic Exercises to Develop Strength/Endurance (15 min)', fee: 50 },
|
||||||
{ code: '97014', description: 'Electrical Stimulation (Unattended)', fee: 25 },
|
{ code: '97014', description: 'Electrical Stimulation (Unattended)', fee: 25 },
|
||||||
|
{ code: '97810', description: 'Acupuncture, 1 or more needles, initial 15 minutes', fee: 85 },
|
||||||
|
{ code: '97124', description: 'Massage Therapy, including effleurage, petrissage, 15 min', fee: 40 },
|
||||||
{ code: '99203', description: 'Office Visit; New Patient, Detailed Exam & Medical Decision', fee: 145 },
|
{ code: '99203', description: 'Office Visit; New Patient, Detailed Exam & Medical Decision', fee: 145 },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -35,8 +51,8 @@ export const INITIAL_TENANTS: ClinicTenant[] = [
|
|||||||
npi: '1942857391',
|
npi: '1942857391',
|
||||||
taxId: '84-2918471',
|
taxId: '84-2918471',
|
||||||
logoText: 'APEX SPINE',
|
logoText: 'APEX SPINE',
|
||||||
brandColor: '#0d9488', // Teal 600
|
brandColor: '#0284c7', // Hospital Blue
|
||||||
accentColor: '#14b8a6',
|
accentColor: '#0369a1',
|
||||||
plan: 'Pro Clinic',
|
plan: 'Pro Clinic',
|
||||||
mrr: 149,
|
mrr: 149,
|
||||||
stripeConnected: true,
|
stripeConnected: true,
|
||||||
@@ -54,8 +70,8 @@ export const INITIAL_TENANTS: ClinicTenant[] = [
|
|||||||
npi: '1839201948',
|
npi: '1839201948',
|
||||||
taxId: '95-3029182',
|
taxId: '95-3029182',
|
||||||
logoText: 'ORIGINS',
|
logoText: 'ORIGINS',
|
||||||
brandColor: '#0284c7', // Sky 600
|
brandColor: '#0284c7',
|
||||||
accentColor: '#38bdf8',
|
accentColor: '#0ea5e9',
|
||||||
plan: 'Pro Clinic',
|
plan: 'Pro Clinic',
|
||||||
mrr: 149,
|
mrr: 149,
|
||||||
stripeConnected: true,
|
stripeConnected: true,
|
||||||
@@ -73,8 +89,8 @@ export const INITIAL_TENANTS: ClinicTenant[] = [
|
|||||||
npi: '1720394812',
|
npi: '1720394812',
|
||||||
taxId: '93-4819203',
|
taxId: '93-4819203',
|
||||||
logoText: 'ELEVATE',
|
logoText: 'ELEVATE',
|
||||||
brandColor: '#6366f1', // Indigo 500
|
brandColor: '#1d4ed8',
|
||||||
accentColor: '#818cf8',
|
accentColor: '#3b82f6',
|
||||||
plan: 'Multi-Doc Enterprise',
|
plan: 'Multi-Doc Enterprise',
|
||||||
mrr: 199,
|
mrr: 199,
|
||||||
stripeConnected: true,
|
stripeConnected: true,
|
||||||
@@ -94,7 +110,7 @@ export const INITIAL_PROVIDERS: Provider[] = [
|
|||||||
email: 'dr.vance@apexspine.clinic',
|
email: 'dr.vance@apexspine.clinic',
|
||||||
phone: '(555) 234-8902',
|
phone: '(555) 234-8902',
|
||||||
avatarUrl: 'https://images.unsplash.com/photo-1622253692010-333f2da6031d?w=150&auto=format&fit=crop&q=80',
|
avatarUrl: 'https://images.unsplash.com/photo-1622253692010-333f2da6031d?w=150&auto=format&fit=crop&q=80',
|
||||||
color: '#0d9488',
|
color: '#0284c7',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'prov-2',
|
id: 'prov-2',
|
||||||
@@ -107,7 +123,7 @@ export const INITIAL_PROVIDERS: Provider[] = [
|
|||||||
email: 'dr.rostova@apexspine.clinic',
|
email: 'dr.rostova@apexspine.clinic',
|
||||||
phone: '(555) 234-8903',
|
phone: '(555) 234-8903',
|
||||||
avatarUrl: 'https://images.unsplash.com/photo-1594824813571-638f026385a8?w=150&auto=format&fit=crop&q=80',
|
avatarUrl: 'https://images.unsplash.com/photo-1594824813571-638f026385a8?w=150&auto=format&fit=crop&q=80',
|
||||||
color: '#0284c7',
|
color: '#0369a1',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -129,6 +145,8 @@ export const INITIAL_PATIENTS: Patient[] = [
|
|||||||
nextAppointmentDate: '2026-09-06',
|
nextAppointmentDate: '2026-09-06',
|
||||||
daysSinceLastVisit: 3,
|
daysSinceLastVisit: 3,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
|
activeMembership: 'Chiropractic Wellness Club',
|
||||||
|
packageCreditsRemaining: 4,
|
||||||
carePlan: {
|
carePlan: {
|
||||||
title: 'Lumbar Disc Decompression & Stabilization Protocol',
|
title: 'Lumbar Disc Decompression & Stabilization Protocol',
|
||||||
totalVisits: 12,
|
totalVisits: 12,
|
||||||
@@ -156,6 +174,7 @@ export const INITIAL_PATIENTS: Patient[] = [
|
|||||||
nextAppointmentDate: undefined,
|
nextAppointmentDate: undefined,
|
||||||
daysSinceLastVisit: 18,
|
daysSinceLastVisit: 18,
|
||||||
status: 'dropout_risk',
|
status: 'dropout_risk',
|
||||||
|
packageCreditsRemaining: 0,
|
||||||
carePlan: {
|
carePlan: {
|
||||||
title: 'Cervical Postural Realignment & Headache Relief',
|
title: 'Cervical Postural Realignment & Headache Relief',
|
||||||
totalVisits: 8,
|
totalVisits: 8,
|
||||||
@@ -183,6 +202,7 @@ export const INITIAL_PATIENTS: Patient[] = [
|
|||||||
nextAppointmentDate: '2026-09-11',
|
nextAppointmentDate: '2026-09-11',
|
||||||
daysSinceLastVisit: 1,
|
daysSinceLastVisit: 1,
|
||||||
status: 'active',
|
status: 'active',
|
||||||
|
activeMembership: 'Athletic Recovery & Decompression Pass',
|
||||||
carePlan: {
|
carePlan: {
|
||||||
title: 'Thoracic Mobility & Biomechanical Alignment',
|
title: 'Thoracic Mobility & Biomechanical Alignment',
|
||||||
totalVisits: 6,
|
totalVisits: 6,
|
||||||
@@ -311,28 +331,13 @@ export const INITIAL_APPOINTMENTS: Appointment[] = [
|
|||||||
providerName: 'Dr. Marcus Vance',
|
providerName: 'Dr. Marcus Vance',
|
||||||
date: '2026-09-05',
|
date: '2026-09-05',
|
||||||
time: '02:00 PM',
|
time: '02:00 PM',
|
||||||
durationMinutes: 20,
|
durationMinutes: 25,
|
||||||
serviceType: 'Cervical Spinal Alignment',
|
serviceType: 'Telehealth Posture & Ergonomics Check',
|
||||||
status: 'confirmed',
|
status: 'confirmed',
|
||||||
room: 'Table 1',
|
room: 'Virtual Room 1',
|
||||||
fee: 75,
|
fee: 65,
|
||||||
notes: 'Recheck C2 lateral slip and suboccipital tension.',
|
isTelehealth: true,
|
||||||
},
|
notes: 'Virtual consult: evaluate ergonomic workstation setup and review suboccipital stretches.',
|
||||||
{
|
|
||||||
id: 'apt-5',
|
|
||||||
tenantId: 'tenant-1',
|
|
||||||
patientId: 'pat-5',
|
|
||||||
patientName: 'James Kowalski',
|
|
||||||
patientPhone: '(555) 601-3329',
|
|
||||||
providerId: 'prov-1',
|
|
||||||
providerName: 'Dr. Marcus Vance',
|
|
||||||
date: '2026-09-05',
|
|
||||||
time: '03:30 PM',
|
|
||||||
durationMinutes: 30,
|
|
||||||
serviceType: 'Triton Lumbar Decompression Therapy',
|
|
||||||
status: 'booked',
|
|
||||||
room: 'Decompression Suite',
|
|
||||||
fee: 95,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -347,6 +352,7 @@ export const INITIAL_SOAP_NOTES: SoapNote[] = [
|
|||||||
appointmentId: 'apt-1',
|
appointmentId: 'apt-1',
|
||||||
date: '2026-09-05',
|
date: '2026-09-05',
|
||||||
status: 'signed',
|
status: 'signed',
|
||||||
|
discipline: 'chiropractic',
|
||||||
vasScore: 3,
|
vasScore: 3,
|
||||||
subjective: 'Patient reports noticeable reduction in morning stiffness. Sciatic sensation into right calf has resolved; occasional dull ache localized to right L5 facet upon prolonged sitting (> 45 min). Rated 3/10 today vs 7/10 at intake.',
|
subjective: 'Patient reports noticeable reduction in morning stiffness. Sciatic sensation into right calf has resolved; occasional dull ache localized to right L5 facet upon prolonged sitting (> 45 min). Rated 3/10 today vs 7/10 at intake.',
|
||||||
objective: 'Inspection reveals reduced antalgic lean. Active Lumbar ROM: Flexion 75 deg (mild tightness), Extension 18 deg. Palpation demonstrates segmental fixation at L4-L5 and right sacral base anteriority. Paraspinal hypertonicity reduced in right quadratus lumborum.',
|
objective: 'Inspection reveals reduced antalgic lean. Active Lumbar ROM: Flexion 75 deg (mild tightness), Extension 18 deg. Palpation demonstrates segmental fixation at L4-L5 and right sacral base anteriority. Paraspinal hypertonicity reduced in right quadratus lumborum.',
|
||||||
@@ -404,27 +410,226 @@ export const INITIAL_SUPERBILLS: Superbill[] = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const INITIAL_INTAKE_SUBMISSIONS: IntakeSubmission[] = [
|
export const INITIAL_PRODUCTS: RetailProduct[] = [
|
||||||
{
|
{
|
||||||
id: 'intake-1',
|
id: 'prod-1',
|
||||||
tenantId: 'tenant-1',
|
tenantId: 'tenant-1',
|
||||||
patientName: 'Jessica Morales',
|
name: 'Biofreeze Professional Pain Relieving Gel (32 oz Pump)',
|
||||||
dob: '1991-08-14',
|
sku: 'BIO-GEL-32',
|
||||||
phone: '(555) 302-9912',
|
category: 'Topical & Pain Relief',
|
||||||
email: 'jess.morales@example.com',
|
price: 48.0,
|
||||||
chiefComplaints: ['Neck Pain', 'Shoulder Blade Tightness', 'Numbness in Left Fingers'],
|
cost: 22.0,
|
||||||
painAreas: ['Cervical Spine', 'Left Trapezius', 'Left Medial Scapula'],
|
stockQty: 18,
|
||||||
vasScore: 6,
|
reorderPoint: 5,
|
||||||
painNature: ['Shooting Pain', 'Dull Ache', 'Tingling / Pins & Needles'],
|
supplier: 'Performance Health',
|
||||||
symptomsOnset: '3 weeks ago following a motor vehicle rear-end collision',
|
},
|
||||||
medications: 'Ibuprofen 600mg PRN',
|
{
|
||||||
priorChiropractic: true,
|
id: 'prod-2',
|
||||||
signatureName: 'Jessica Morales',
|
tenantId: 'tenant-1',
|
||||||
submittedAt: '2026-09-05T11:45:00-07:00',
|
name: 'Metagenics OmegaGenics EPA-DHA 720 (120 Softgels)',
|
||||||
status: 'pending_review',
|
sku: 'META-OMEGA-720',
|
||||||
|
category: 'Supplements',
|
||||||
|
price: 52.0,
|
||||||
|
cost: 27.5,
|
||||||
|
stockQty: 24,
|
||||||
|
reorderPoint: 8,
|
||||||
|
supplier: 'Metagenics Labs',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-3',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
name: 'Core Products Tri-Core Standard Cervical Support Pillow',
|
||||||
|
sku: 'TRI-CORE-STD',
|
||||||
|
category: 'Orthotics & Pillows',
|
||||||
|
price: 65.0,
|
||||||
|
cost: 31.0,
|
||||||
|
stockQty: 12,
|
||||||
|
reorderPoint: 4,
|
||||||
|
supplier: 'Core Products Inc',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-4',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
name: 'Standard Process Catalyn Multivitamin (90 Tablets)',
|
||||||
|
sku: 'SP-CAT-90',
|
||||||
|
category: 'Supplements',
|
||||||
|
price: 24.5,
|
||||||
|
cost: 12.0,
|
||||||
|
stockQty: 32,
|
||||||
|
reorderPoint: 10,
|
||||||
|
supplier: 'Standard Process',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-5',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
name: 'Pro-Tec High Density 36" Foam Roller',
|
||||||
|
sku: 'PRO-ROLLER-36',
|
||||||
|
category: 'Rehab Gear',
|
||||||
|
price: 38.0,
|
||||||
|
cost: 16.0,
|
||||||
|
stockQty: 9,
|
||||||
|
reorderPoint: 3,
|
||||||
|
supplier: 'Pro-Tec Athletics',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'prod-6',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
name: 'Foot Levelers Custom Spinal Pelvic Stabilizers (Orthotics)',
|
||||||
|
sku: 'FL-CUSTOM-ORTH',
|
||||||
|
category: 'Orthotics & Pillows',
|
||||||
|
price: 295.0,
|
||||||
|
cost: 130.0,
|
||||||
|
stockQty: 6,
|
||||||
|
reorderPoint: 2,
|
||||||
|
supplier: 'Foot Levelers',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const INITIAL_MEMBERSHIPS: WellnessMembership[] = [
|
||||||
|
{
|
||||||
|
id: 'mem-1',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
name: 'Chiropractic Wellness Club',
|
||||||
|
monthlyFee: 89,
|
||||||
|
includedVisits: 2,
|
||||||
|
additionalVisitDiscount: '20% off all additional CMT visits ($44/ea)',
|
||||||
|
activeMembers: 42,
|
||||||
|
mrrContribution: 3738,
|
||||||
|
description: 'Our most popular monthly maintenance plan. Includes 2 chiropractic adjustments per month with auto-draft via Stripe.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mem-2',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
name: 'Family Spine & Posture Plan',
|
||||||
|
monthlyFee: 159,
|
||||||
|
includedVisits: 4,
|
||||||
|
additionalVisitDiscount: '25% off family add-ons',
|
||||||
|
activeMembers: 18,
|
||||||
|
mrrContribution: 2862,
|
||||||
|
description: 'Shared family plan covering parents & kids for 4 monthly spinal checks and adjustments.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mem-3',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
name: 'Athletic Recovery & Decompression Pass',
|
||||||
|
monthlyFee: 129,
|
||||||
|
includedVisits: 2,
|
||||||
|
additionalVisitDiscount: '15% off therapeutic exercises',
|
||||||
|
activeMembers: 26,
|
||||||
|
mrrContribution: 3354,
|
||||||
|
description: 'Tailored for runners, crossfitters, and athletes. 2 adjustments + 2 Triton spinal decompression sessions per month.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const INITIAL_PACKAGES: PrePaidPackage[] = [
|
||||||
|
{
|
||||||
|
id: 'pkg-1',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
name: '10-Adjustment Spinal Care Block',
|
||||||
|
totalVisits: 10,
|
||||||
|
price: 550,
|
||||||
|
perVisitRate: 55,
|
||||||
|
savings: 'Save $200 vs single visit rates ($75/ea)',
|
||||||
|
activeSold: 34,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pkg-2',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
name: '5-Session Decompression Protocol',
|
||||||
|
totalVisits: 5,
|
||||||
|
price: 425,
|
||||||
|
perVisitRate: 85,
|
||||||
|
savings: 'Save $50 on lumbar disc decompression',
|
||||||
|
activeSold: 19,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const INITIAL_WAITLIST: WaitlistEntry[] = [
|
||||||
|
{
|
||||||
|
id: 'wl-1',
|
||||||
|
patientName: 'Robert Vance',
|
||||||
|
phone: '(555) 349-1829',
|
||||||
|
providerName: 'Dr. Marcus Vance',
|
||||||
|
requestedService: 'Spinal Adjustment',
|
||||||
|
preferredDays: 'Tuesdays / Thursdays',
|
||||||
|
preferredTimeRange: 'Mornings (08:30 - 11:00 AM)',
|
||||||
|
notes: 'Acute flare-up. Can arrive on 30 min notice.',
|
||||||
|
status: 'waiting',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'wl-2',
|
||||||
|
patientName: 'Megan Alvarez',
|
||||||
|
phone: '(555) 839-2049',
|
||||||
|
providerName: 'Dr. Elena Rostova',
|
||||||
|
requestedService: 'Webster Prenatal Technique',
|
||||||
|
preferredDays: 'Any weekday',
|
||||||
|
preferredTimeRange: 'Afternoons (02:00 - 05:00 PM)',
|
||||||
|
notes: 'Third trimester pelvic tightness.',
|
||||||
|
status: 'waiting',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'wl-3',
|
||||||
|
patientName: 'Brian Chang',
|
||||||
|
phone: '(555) 912-3847',
|
||||||
|
providerName: 'Dr. Marcus Vance',
|
||||||
|
requestedService: 'Decompression Therapy',
|
||||||
|
preferredDays: 'Fridays',
|
||||||
|
preferredTimeRange: 'Late afternoon',
|
||||||
|
notes: 'Golf tournament this weekend.',
|
||||||
|
status: 'waiting',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DISCIPLINE_PRESETS: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
title: string;
|
||||||
|
sampleSubjective: string;
|
||||||
|
sampleObjective: string;
|
||||||
|
sampleAssessment: string;
|
||||||
|
samplePlan: string;
|
||||||
|
cptCodes: string[];
|
||||||
|
icd10Codes: string[];
|
||||||
|
}
|
||||||
|
> = {
|
||||||
|
chiropractic: {
|
||||||
|
title: 'Chiropractic Biomechanics',
|
||||||
|
sampleSubjective: 'Patient reports dull aching pain along right lower lumbar belt line (VAS 3/10). No radicular numbness reported into leg.',
|
||||||
|
sampleObjective: 'Palpation demonstrates subluxation and fixation at L4-L5 with right sacral base anteriority. Paraspinal hypertonicity present.',
|
||||||
|
sampleAssessment: 'Segmental and somatic dysfunction of lumbar spine (M99.03) and pelvic region (M99.05).',
|
||||||
|
samplePlan: '1. Diversified CMT delivered to L4 and right sacral base.\n2. Flexion distraction axial traction 8 min.\n3. Return in 3 days.',
|
||||||
|
cptCodes: ['98940', '97140'],
|
||||||
|
icd10Codes: ['M99.03', 'M99.05', 'M54.50'],
|
||||||
|
},
|
||||||
|
physical_therapy: {
|
||||||
|
title: 'Physical Therapy & Rehab',
|
||||||
|
sampleSubjective: 'Patient reports difficulty ascending stairs due to right patellofemoral tracking pain. VAS 4/10.',
|
||||||
|
sampleObjective: 'Active knee flexion: 120 deg with crepitus. Strength: Right VMO 4-/5, Gluteus medius 4/5. Positive Clark sign.',
|
||||||
|
sampleAssessment: 'Patellofemoral pain syndrome right knee with quadriceps inhibition and hip abductor weakness.',
|
||||||
|
samplePlan: '1. Therapeutic exercise (97110) 15 min: eccentric step-downs, clam shells.\n2. Manual therapy (97140) 15 min: patellar mobilization.\n3. Ice 10 min.',
|
||||||
|
cptCodes: ['97110', '97140'],
|
||||||
|
icd10Codes: ['M54.50'],
|
||||||
|
},
|
||||||
|
acupuncture: {
|
||||||
|
title: 'Acupuncture & Eastern Medicine',
|
||||||
|
sampleSubjective: 'Patient presents with insomnia, tight chest, and occipital headache exacerbated by stress. Tongue: Red tip, thin white coat. Pulse: Wiry.',
|
||||||
|
sampleObjective: 'Palpation of meridians reveals tenderness at GB20, LI4, and LV3. Stagnation in Liver Qi meridian.',
|
||||||
|
sampleAssessment: 'Liver Qi Stagnation with rising Liver Yang causing cephalalgia and sleep disturbance.',
|
||||||
|
samplePlan: '1. Inserted sterile single-use 0.25x30mm needles at GB20, LI4, LV3, Yin Tang, SP6.\n2. Retained for 25 minutes with gentle dispersion.\n3. Patient rested quietly.',
|
||||||
|
cptCodes: ['97810'],
|
||||||
|
icd10Codes: ['G44.209'],
|
||||||
|
},
|
||||||
|
massage_therapy: {
|
||||||
|
title: 'Medical Massage & Myofascial Release',
|
||||||
|
sampleSubjective: 'Client presents with bilateral upper trapezius and levator scapulae burning sensation from 10-hour daily keyboard typing.',
|
||||||
|
sampleObjective: 'Severe active trigger points palpated in upper trapezius bilateral and suboccipital triangle. Restricted cervical rotation to 60 deg.',
|
||||||
|
sampleAssessment: 'Myofascial pain syndrome and chronic postural muscle tension cervicothoracic junction.',
|
||||||
|
samplePlan: '1. Myofascial release and deep tissue friction massage (97124) 30 min.\n2. Suboccipital decompression.\n3. Ergonomic micro-break instruction.',
|
||||||
|
cptCodes: ['97124'],
|
||||||
|
icd10Codes: ['M54.2'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const SAMPLE_AUDIO_PRESETS = [
|
export const SAMPLE_AUDIO_PRESETS = [
|
||||||
{
|
{
|
||||||
id: 'acute-lumbar',
|
id: 'acute-lumbar',
|
||||||
@@ -447,25 +652,4 @@ Dr. Vance: "Great, let's take a look. Face down on the hi-lo table please. Palpa
|
|||||||
cpt: ['98940', '97140'],
|
cpt: ['98940', '97140'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'cervical-headache',
|
|
||||||
title: 'Encounter: Cervicogenic Headache & Atlas Subluxation',
|
|
||||||
duration: '0m 52s',
|
|
||||||
audioTranscript: `Dr. Elena: "Hi Sarah, how has your neck been feeling with the new ergonomic monitor setup?"
|
|
||||||
Sarah: "Hi Dr. Elena! The sharp migraines are down, but I still get that heavy throbbing behind my right eye and burning at the base of my skull by 3 PM every day."
|
|
||||||
Dr. Elena: "Let's check cervical motion. Rotation to the right is restricted at 55 degrees with suboccipital spasm. Motion palpation shows C1 Atlas right lateral and posterior listing, and C5-C6 bilateral facet restriction. Let's do gentle Atlas specific diversified adjustment on the right... gentle breath out... [click]. Excellent. Re-checking range of motion: right rotation cleared to 80 degrees smoothly. I'm going to apply 5 minutes of suboccipital release. Come back in 5 days."`,
|
|
||||||
extractedSOAP: {
|
|
||||||
vasScore: 4,
|
|
||||||
subjective: "Patient reports resolution of severe acute migraine episodes. Throbbing occipital pain radiating to right retro-orbital area persists toward late afternoon with desk work. Rated 4/10.",
|
|
||||||
objective: "Active cervical rotation to the right restricted to 55 degrees (normal 80) with palpable hypertonicity in right rectus capitis and obliquus capitis. Palpation reveals C1 Atlas Right Lateral/Posterior listing and C5-C6 facet imbrication.",
|
|
||||||
assessment: "Cervicogenic tension headache (G44.209) secondary to C1-C2 segmental subluxation (M99.01) and postural cervicalgia (M54.2).",
|
|
||||||
plan: "1. Specific diversified Atlas (C1) contact right lateral mass adjustment.\n2. C5-C6 bilateral cervical seated toggle recoil.\n3. Manual myofascial release to suboccipital triangle (5 mins).\n4. Follow-up appointment scheduled in 5 days.",
|
|
||||||
spinalAdjustments: [
|
|
||||||
{ vertebra: 'C1', region: 'Cervical', listing: 'Atlas Right Lateral Posterior (ASR)', technique: 'Diversified', notes: 'Immediate cervical rotation clearance' },
|
|
||||||
{ vertebra: 'C5', region: 'Cervical', listing: 'Bilateral Facet Fixation', technique: 'Activator', notes: 'Pressure 3 setting' },
|
|
||||||
],
|
|
||||||
icd10: ['M99.01', 'M54.2', 'G44.209'],
|
|
||||||
cpt: ['98940', '97140'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|||||||
+65
-9
@@ -1,3 +1,7 @@
|
|||||||
|
export type StaffRole = 'doctor' | 'front_desk' | 'billing_admin';
|
||||||
|
|
||||||
|
export type ClinicalDiscipline = 'chiropractic' | 'physical_therapy' | 'acupuncture' | 'massage_therapy';
|
||||||
|
|
||||||
export interface ClinicTenant {
|
export interface ClinicTenant {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -23,7 +27,7 @@ export interface Provider {
|
|||||||
tenantId: string;
|
tenantId: string;
|
||||||
name: string;
|
name: string;
|
||||||
title: string;
|
title: string;
|
||||||
credentials: string; // e.g., "D.C., CCSP"
|
credentials: string;
|
||||||
specialty: string;
|
specialty: string;
|
||||||
npi: string;
|
npi: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -36,7 +40,7 @@ export interface CarePlan {
|
|||||||
title: string;
|
title: string;
|
||||||
totalVisits: number;
|
totalVisits: number;
|
||||||
completedVisits: number;
|
completedVisits: number;
|
||||||
frequency: string; // e.g. "2x / week for 6 weeks"
|
frequency: string;
|
||||||
targetCondition: string;
|
targetCondition: string;
|
||||||
startDate: string;
|
startDate: string;
|
||||||
status: 'on_track' | 'lagging' | 'at_risk' | 'completed';
|
status: 'on_track' | 'lagging' | 'at_risk' | 'completed';
|
||||||
@@ -60,6 +64,8 @@ export interface Patient {
|
|||||||
status: 'active' | 'dropout_risk' | 'reactivated' | 'discharged';
|
status: 'active' | 'dropout_risk' | 'reactivated' | 'discharged';
|
||||||
chiefComplaint: string;
|
chiefComplaint: string;
|
||||||
daysSinceLastVisit: number;
|
daysSinceLastVisit: number;
|
||||||
|
activeMembership?: string;
|
||||||
|
packageCreditsRemaining?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AppointmentStatus = 'booked' | 'confirmed' | 'arrived' | 'in_room' | 'completed' | 'no_show';
|
export type AppointmentStatus = 'booked' | 'confirmed' | 'arrived' | 'in_room' | 'completed' | 'no_show';
|
||||||
@@ -72,20 +78,21 @@ export interface Appointment {
|
|||||||
patientPhone: string;
|
patientPhone: string;
|
||||||
providerId: string;
|
providerId: string;
|
||||||
providerName: string;
|
providerName: string;
|
||||||
date: string; // YYYY-MM-DD
|
date: string;
|
||||||
time: string; // HH:MM AM/PM
|
time: string;
|
||||||
durationMinutes: number;
|
durationMinutes: number;
|
||||||
serviceType: string;
|
serviceType: string;
|
||||||
status: AppointmentStatus;
|
status: AppointmentStatus;
|
||||||
room: string;
|
room: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
fee: number;
|
fee: number;
|
||||||
|
isTelehealth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SpinalAdjustmentEntry {
|
export interface SpinalAdjustmentEntry {
|
||||||
vertebra: string; // e.g. "C2", "L5"
|
vertebra: string;
|
||||||
region: 'Cervical' | 'Thoracic' | 'Lumbar' | 'Pelvis/Sacrum';
|
region: 'Cervical' | 'Thoracic' | 'Lumbar' | 'Pelvis/Sacrum';
|
||||||
listing: string; // e.g., "Right Lateral / Posterior"
|
listing: string;
|
||||||
technique: 'Diversified' | 'Thompson Drop' | 'Activator' | 'Gonstead' | 'Webster' | 'Flexion-Distraction';
|
technique: 'Diversified' | 'Thompson Drop' | 'Activator' | 'Gonstead' | 'Webster' | 'Flexion-Distraction';
|
||||||
notes?: string;
|
notes?: string;
|
||||||
}
|
}
|
||||||
@@ -111,6 +118,7 @@ export interface SoapNote {
|
|||||||
appointmentId?: string;
|
appointmentId?: string;
|
||||||
date: string;
|
date: string;
|
||||||
status: 'draft' | 'signed';
|
status: 'draft' | 'signed';
|
||||||
|
discipline: ClinicalDiscipline;
|
||||||
subjective: string;
|
subjective: string;
|
||||||
objective: string;
|
objective: string;
|
||||||
assessment: string;
|
assessment: string;
|
||||||
@@ -118,7 +126,7 @@ export interface SoapNote {
|
|||||||
spinalAdjustments: SpinalAdjustmentEntry[];
|
spinalAdjustments: SpinalAdjustmentEntry[];
|
||||||
icd10Codes: Icd10CodeItem[];
|
icd10Codes: Icd10CodeItem[];
|
||||||
cptCodes: CptCodeItem[];
|
cptCodes: CptCodeItem[];
|
||||||
vasScore: number; // 0-10
|
vasScore: number;
|
||||||
signedAt?: string;
|
signedAt?: string;
|
||||||
signedBy?: string;
|
signedBy?: string;
|
||||||
}
|
}
|
||||||
@@ -145,7 +153,7 @@ export interface Superbill {
|
|||||||
clinicAddress: string;
|
clinicAddress: string;
|
||||||
clinicTaxId: string;
|
clinicTaxId: string;
|
||||||
dateOfService: string;
|
dateOfService: string;
|
||||||
posCode: string; // Place of Service "11 - Office"
|
posCode: string;
|
||||||
icd10Codes: Icd10CodeItem[];
|
icd10Codes: Icd10CodeItem[];
|
||||||
items: SuperbillLineItem[];
|
items: SuperbillLineItem[];
|
||||||
totalAmount: number;
|
totalAmount: number;
|
||||||
@@ -165,7 +173,7 @@ export interface IntakeSubmission {
|
|||||||
chiefComplaints: string[];
|
chiefComplaints: string[];
|
||||||
painAreas: string[];
|
painAreas: string[];
|
||||||
vasScore: number;
|
vasScore: number;
|
||||||
painNature: string[]; // ['Sharp', 'Dull Ache', 'Shooting']
|
painNature: string[];
|
||||||
symptomsOnset: string;
|
symptomsOnset: string;
|
||||||
medications: string;
|
medications: string;
|
||||||
priorChiropractic: boolean;
|
priorChiropractic: boolean;
|
||||||
@@ -173,3 +181,51 @@ export interface IntakeSubmission {
|
|||||||
submittedAt: string;
|
submittedAt: string;
|
||||||
status: 'pending_review' | 'imported_to_chart';
|
status: 'pending_review' | 'imported_to_chart';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RetailProduct {
|
||||||
|
id: string;
|
||||||
|
tenantId: string;
|
||||||
|
name: string;
|
||||||
|
sku: string;
|
||||||
|
category: 'Supplements' | 'Orthotics & Pillows' | 'Topical & Pain Relief' | 'Rehab Gear';
|
||||||
|
price: number;
|
||||||
|
cost: number;
|
||||||
|
stockQty: number;
|
||||||
|
reorderPoint: number;
|
||||||
|
supplier: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WellnessMembership {
|
||||||
|
id: string;
|
||||||
|
tenantId: string;
|
||||||
|
name: string;
|
||||||
|
monthlyFee: number;
|
||||||
|
includedVisits: number;
|
||||||
|
additionalVisitDiscount: string;
|
||||||
|
activeMembers: number;
|
||||||
|
mrrContribution: number;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PrePaidPackage {
|
||||||
|
id: string;
|
||||||
|
tenantId: string;
|
||||||
|
name: string;
|
||||||
|
totalVisits: number;
|
||||||
|
price: number;
|
||||||
|
perVisitRate: number;
|
||||||
|
savings: string;
|
||||||
|
activeSold: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WaitlistEntry {
|
||||||
|
id: string;
|
||||||
|
patientName: string;
|
||||||
|
phone: string;
|
||||||
|
providerName: string;
|
||||||
|
requestedService: string;
|
||||||
|
preferredDays: string;
|
||||||
|
preferredTimeRange: string;
|
||||||
|
notes: string;
|
||||||
|
status: 'waiting' | 'notified' | 'filled';
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user