feat: complete Jane App feature set with Telehealth, Retail POS, Memberships, Multi-Discipline Charting, Waitlist & RBAC

This commit is contained in:
2026-09-05 15:15:35 -07:00
parent e681f066bb
commit dd5057d4e3
9 changed files with 1641 additions and 102 deletions
+193 -7
View File
@@ -8,6 +8,10 @@ import {
INITIAL_APPOINTMENTS,
INITIAL_SOAP_NOTES,
INITIAL_SUPERBILLS,
INITIAL_PRODUCTS,
INITIAL_MEMBERSHIPS,
INITIAL_PACKAGES,
INITIAL_WAITLIST,
} from '@/lib/mock-data';
import {
ClinicTenant,
@@ -15,6 +19,11 @@ import {
SoapNote,
Superbill,
Patient,
RetailProduct,
WellnessMembership,
PrePaidPackage,
WaitlistEntry,
StaffRole,
} from '@/types/clinical';
import { CalendarView } from '@/components/calendar/CalendarView';
import { SoapChartEditor } from '@/components/charting/SoapChartEditor';
@@ -22,6 +31,10 @@ import { SuperbillView } from '@/components/billing/SuperbillView';
import { RetentionView } from '@/components/retention/RetentionView';
import { PatientPortalView } from '@/components/intake/PatientPortalView';
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 {
Calendar,
FileText,
@@ -32,6 +45,12 @@ import {
ChevronDown,
Stethoscope,
Laptop,
ShoppingBag,
Repeat,
Video,
Shield,
ShieldAlert,
UserCheck,
} from 'lucide-react';
export default function Home() {
@@ -41,13 +60,23 @@ export default function Home() {
// 'clinic' | 'patient' | 'superadmin'
const [portalMode, setPortalMode] = useState<'clinic' | 'patient' | 'superadmin'>('clinic');
// 'calendar' | 'charting' | 'billing' | 'retention'
const [clinicTab, setClinicTab] = useState<'calendar' | 'charting' | 'billing' | 'retention'>('calendar');
// Staff Role: 'doctor' | 'front_desk' | 'billing_admin'
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 [patients, setPatients] = useState<Patient[]>(INITIAL_PATIENTS);
const [soapNotes, setSoapNotes] = useState<SoapNote[]>(INITIAL_SOAP_NOTES);
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 [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);
setActiveSoapNote(existingSoap);
if (apt.isTelehealth) {
setClinicTab('telehealth');
} else {
setClinicTab('charting');
}
};
const handleUpdateAppointmentStatus = (aptId: string, status: Appointment['status']) => {
@@ -159,6 +193,24 @@ export default function Home() {
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 (
<div className="min-h-screen bg-slate-50 text-slate-900 flex flex-col">
{/* Top Hospital Command Header */}
@@ -199,6 +251,52 @@ export default function Home() {
</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" />
</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>
{/* Portal Switcher Tabs */}
@@ -260,6 +358,8 @@ export default function Home() {
Schedule &amp; Encounters
</button>
{/* Clinical Charts - Hidden if in Front Desk HIPAA-safe mode */}
{staffRole !== 'front_desk' ? (
<button
type="button"
onClick={() => setClinicTab('charting')}
@@ -270,7 +370,53 @@ export default function Home() {
}`}
>
<FileText className="w-3.5 h-3.5 text-sky-700" />
Clinical SOAP &amp; Spine Map ({activePatient.firstName} {activePatient.lastName})
SOAP Chart &amp; 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 &amp; 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 &amp; Packages
</button>
<button
@@ -296,7 +442,7 @@ export default function Home() {
}`}
>
<Users className="w-3.5 h-3.5 text-amber-600" />
Care Plan Retention Radar
Retention Radar
</button>
</div>
)}
@@ -311,28 +457,60 @@ export default function Home() {
appointments={appointments}
providers={activeProviders}
activeTenant={activeTenant}
waitlistCount={waitlist.length}
onSelectAppointment={handleSelectAppointment}
onUpdateStatus={handleUpdateAppointmentStatus}
onNewAppointmentClick={() => setPortalMode('patient')}
onOpenWaitlist={() => setIsWaitlistOpen(true)}
/>
)}
{clinicTab === 'charting' && (
{clinicTab === 'charting' && staffRole !== 'front_desk' && (
<SoapChartEditor
patient={activePatient}
provider={activeProvider}
initialSoapNote={activeSoapNote}
onSaveSoapNote={handleSaveSoapNote}
onGenerateSuperbill={handleGenerateSuperbillFromNote}
onLaunchTelehealth={() => setClinicTab('telehealth')}
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' && (
<SuperbillView
superbill={activeSuperbill}
activeTenant={activeTenant}
onBack={() => setClinicTab('charting')}
onBack={() => setClinicTab('calendar')}
onMarkPaid={handleMarkPaid}
/>
)}
@@ -369,11 +547,19 @@ export default function Home() {
)}
</main>
{/* Cancellation Waitlist Modal */}
<WaitlistModal
waitlist={waitlist}
isOpen={isWaitlistOpen}
onClose={() => setIsWaitlistOpen(false)}
onAutoFillPatient={handleAutoFillWaitlistPatient}
/>
{/* Hospital Footer */}
<footer className="bg-white border-t border-slate-200 py-4 px-6 text-center text-xs text-slate-500">
<div className="flex flex-col sm:flex-row items-center justify-between max-w-7xl mx-auto gap-2">
<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 className="font-mono text-slate-500">
hello@aipilots.site HIPAA Certified Sub-30s Clinical Charting Standard
+24 -2
View File
@@ -13,24 +13,30 @@ import {
ChevronLeft,
ChevronRight,
Stethoscope,
Video,
Users,
} from 'lucide-react';
interface CalendarViewProps {
appointments: Appointment[];
providers: Provider[];
activeTenant: ClinicTenant;
waitlistCount?: number;
onSelectAppointment: (apt: Appointment) => void;
onUpdateStatus: (aptId: string, status: Appointment['status']) => void;
onNewAppointmentClick: () => void;
onOpenWaitlist: () => void;
}
export const CalendarView: React.FC<CalendarViewProps> = ({
appointments,
providers,
activeTenant,
waitlistCount = 3,
onSelectAppointment,
onUpdateStatus,
onNewAppointmentClick,
onOpenWaitlist,
}) => {
const [selectedProviderId, setSelectedProviderId] = useState<string>('all');
@@ -112,7 +118,7 @@ export const CalendarView: React.FC<CalendarViewProps> = ({
onClick={() => setSelectedProviderId('all')}
className={`px-3 py-1 rounded-md font-semibold transition ${
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'
}`}
>
@@ -125,7 +131,7 @@ export const CalendarView: React.FC<CalendarViewProps> = ({
onClick={() => setSelectedProviderId(p.id)}
className={`px-3 py-1 rounded-md font-semibold transition ${
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'
}`}
>
@@ -135,6 +141,16 @@ export const CalendarView: React.FC<CalendarViewProps> = ({
</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
type="button"
onClick={onNewAppointmentClick}
@@ -144,6 +160,7 @@ export const CalendarView: React.FC<CalendarViewProps> = ({
Schedule Patient
</button>
</div>
</div>
{/* Grid of Hospital Patient Encounter Cards */}
<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">
{apt.durationMinutes} min
</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>
{getStatusBadge(apt.status)}
</div>
+83 -5
View File
@@ -1,18 +1,27 @@
'use client';
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 { 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 {
FileText,
CheckCircle,
Copy,
DollarSign,
ChevronLeft,
Tag,
ShieldCheck,
Video,
Layers,
} from 'lucide-react';
interface SoapChartEditorProps {
@@ -21,6 +30,7 @@ interface SoapChartEditorProps {
initialSoapNote?: SoapNote;
onSaveSoapNote: (note: SoapNote) => void;
onGenerateSuperbill: (note: SoapNote) => void;
onLaunchTelehealth: () => void;
onBackToCalendar: () => void;
}
@@ -30,8 +40,13 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
initialSoapNote,
onSaveSoapNote,
onGenerateSuperbill,
onLaunchTelehealth,
onBackToCalendar,
}) => {
const [discipline, setDiscipline] = useState<ClinicalDiscipline>(
initialSoapNote?.discipline || 'chiropractic'
);
const [subjective, setSubjective] = useState(
initialSoapNote?.subjective ||
`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);
};
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) => {
setAdjustments((prev) => {
const exists = prev.some((a) => a.vertebra === entry.vertebra);
@@ -125,6 +157,7 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
providerName: provider.name,
date: '2026-09-05',
status: 'signed',
discipline,
vasScore,
subjective,
objective,
@@ -150,6 +183,7 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
providerName: provider.name,
date: '2026-09-05',
status: isSigned ? 'signed' : 'draft',
discipline,
vasScore,
subjective,
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">
Care Plan: Visit {patient.carePlan.completedVisits + 1} of {patient.carePlan.totalVisits}
</span>
{patient.activeMembership && (
<span className="text-xs px-2 py-0.5 rounded-full bg-emerald-50 text-emerald-800 border border-emerald-200 font-bold">
{patient.activeMembership}
</span>
)}
{isSigned ? (
<span className="text-xs px-2.5 py-0.5 rounded-full bg-emerald-50 text-emerald-700 border border-emerald-200 font-semibold flex items-center gap-1">
<ShieldCheck className="w-3.5 h-3.5" /> Signed &amp; Locked
@@ -209,7 +248,16 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
</div>
{/* 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
type="button"
onClick={handleCloneLastNote}
@@ -244,10 +292,40 @@ export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
</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 */}
<AmbientAudioRecorder onApplyExtractedSoap={handleApplyExtractedSoap} />
{/* Spine Subluxation Visualizer */}
{/* Spine Subluxation Visualizer (shown prominently for chiropractic & physical therapy) */}
<SpineVisualizer
adjustments={adjustments}
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 &amp; 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 &amp; 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 &amp; Activate Subscription
</button>
</div>
</form>
)}
</div>
</div>
)}
</div>
);
};
+335
View File
@@ -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 &amp; 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 &quot;Add to Register&quot; 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 &amp; 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 &amp; 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 &amp; 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&apos;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 &amp; Complete Visit
</button>
</div>
</div>
</div>
</div>
);
};
+136
View File
@@ -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 &amp; 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: &quot;{item.notes}&quot;
</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
View File
@@ -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[] = [
{ 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: '97110', description: 'Therapeutic Exercises to Develop Strength/Endurance (15 min)', fee: 50 },
{ 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 },
];
@@ -35,8 +51,8 @@ export const INITIAL_TENANTS: ClinicTenant[] = [
npi: '1942857391',
taxId: '84-2918471',
logoText: 'APEX SPINE',
brandColor: '#0d9488', // Teal 600
accentColor: '#14b8a6',
brandColor: '#0284c7', // Hospital Blue
accentColor: '#0369a1',
plan: 'Pro Clinic',
mrr: 149,
stripeConnected: true,
@@ -54,8 +70,8 @@ export const INITIAL_TENANTS: ClinicTenant[] = [
npi: '1839201948',
taxId: '95-3029182',
logoText: 'ORIGINS',
brandColor: '#0284c7', // Sky 600
accentColor: '#38bdf8',
brandColor: '#0284c7',
accentColor: '#0ea5e9',
plan: 'Pro Clinic',
mrr: 149,
stripeConnected: true,
@@ -73,8 +89,8 @@ export const INITIAL_TENANTS: ClinicTenant[] = [
npi: '1720394812',
taxId: '93-4819203',
logoText: 'ELEVATE',
brandColor: '#6366f1', // Indigo 500
accentColor: '#818cf8',
brandColor: '#1d4ed8',
accentColor: '#3b82f6',
plan: 'Multi-Doc Enterprise',
mrr: 199,
stripeConnected: true,
@@ -94,7 +110,7 @@ export const INITIAL_PROVIDERS: Provider[] = [
email: 'dr.vance@apexspine.clinic',
phone: '(555) 234-8902',
avatarUrl: 'https://images.unsplash.com/photo-1622253692010-333f2da6031d?w=150&auto=format&fit=crop&q=80',
color: '#0d9488',
color: '#0284c7',
},
{
id: 'prov-2',
@@ -107,7 +123,7 @@ export const INITIAL_PROVIDERS: Provider[] = [
email: 'dr.rostova@apexspine.clinic',
phone: '(555) 234-8903',
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',
daysSinceLastVisit: 3,
status: 'active',
activeMembership: 'Chiropractic Wellness Club',
packageCreditsRemaining: 4,
carePlan: {
title: 'Lumbar Disc Decompression & Stabilization Protocol',
totalVisits: 12,
@@ -156,6 +174,7 @@ export const INITIAL_PATIENTS: Patient[] = [
nextAppointmentDate: undefined,
daysSinceLastVisit: 18,
status: 'dropout_risk',
packageCreditsRemaining: 0,
carePlan: {
title: 'Cervical Postural Realignment & Headache Relief',
totalVisits: 8,
@@ -183,6 +202,7 @@ export const INITIAL_PATIENTS: Patient[] = [
nextAppointmentDate: '2026-09-11',
daysSinceLastVisit: 1,
status: 'active',
activeMembership: 'Athletic Recovery & Decompression Pass',
carePlan: {
title: 'Thoracic Mobility & Biomechanical Alignment',
totalVisits: 6,
@@ -311,28 +331,13 @@ export const INITIAL_APPOINTMENTS: Appointment[] = [
providerName: 'Dr. Marcus Vance',
date: '2026-09-05',
time: '02:00 PM',
durationMinutes: 20,
serviceType: 'Cervical Spinal Alignment',
durationMinutes: 25,
serviceType: 'Telehealth Posture & Ergonomics Check',
status: 'confirmed',
room: 'Table 1',
fee: 75,
notes: 'Recheck C2 lateral slip and suboccipital tension.',
},
{
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,
room: 'Virtual Room 1',
fee: 65,
isTelehealth: true,
notes: 'Virtual consult: evaluate ergonomic workstation setup and review suboccipital stretches.',
},
];
@@ -347,6 +352,7 @@ export const INITIAL_SOAP_NOTES: SoapNote[] = [
appointmentId: 'apt-1',
date: '2026-09-05',
status: 'signed',
discipline: 'chiropractic',
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.',
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',
patientName: 'Jessica Morales',
dob: '1991-08-14',
phone: '(555) 302-9912',
email: 'jess.morales@example.com',
chiefComplaints: ['Neck Pain', 'Shoulder Blade Tightness', 'Numbness in Left Fingers'],
painAreas: ['Cervical Spine', 'Left Trapezius', 'Left Medial Scapula'],
vasScore: 6,
painNature: ['Shooting Pain', 'Dull Ache', 'Tingling / Pins & Needles'],
symptomsOnset: '3 weeks ago following a motor vehicle rear-end collision',
medications: 'Ibuprofen 600mg PRN',
priorChiropractic: true,
signatureName: 'Jessica Morales',
submittedAt: '2026-09-05T11:45:00-07:00',
status: 'pending_review',
name: 'Biofreeze Professional Pain Relieving Gel (32 oz Pump)',
sku: 'BIO-GEL-32',
category: 'Topical & Pain Relief',
price: 48.0,
cost: 22.0,
stockQty: 18,
reorderPoint: 5,
supplier: 'Performance Health',
},
{
id: 'prod-2',
tenantId: 'tenant-1',
name: 'Metagenics OmegaGenics EPA-DHA 720 (120 Softgels)',
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 = [
{
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'],
},
},
{
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
View File
@@ -1,3 +1,7 @@
export type StaffRole = 'doctor' | 'front_desk' | 'billing_admin';
export type ClinicalDiscipline = 'chiropractic' | 'physical_therapy' | 'acupuncture' | 'massage_therapy';
export interface ClinicTenant {
id: string;
name: string;
@@ -23,7 +27,7 @@ export interface Provider {
tenantId: string;
name: string;
title: string;
credentials: string; // e.g., "D.C., CCSP"
credentials: string;
specialty: string;
npi: string;
email: string;
@@ -36,7 +40,7 @@ export interface CarePlan {
title: string;
totalVisits: number;
completedVisits: number;
frequency: string; // e.g. "2x / week for 6 weeks"
frequency: string;
targetCondition: string;
startDate: string;
status: 'on_track' | 'lagging' | 'at_risk' | 'completed';
@@ -60,6 +64,8 @@ export interface Patient {
status: 'active' | 'dropout_risk' | 'reactivated' | 'discharged';
chiefComplaint: string;
daysSinceLastVisit: number;
activeMembership?: string;
packageCreditsRemaining?: number;
}
export type AppointmentStatus = 'booked' | 'confirmed' | 'arrived' | 'in_room' | 'completed' | 'no_show';
@@ -72,20 +78,21 @@ export interface Appointment {
patientPhone: string;
providerId: string;
providerName: string;
date: string; // YYYY-MM-DD
time: string; // HH:MM AM/PM
date: string;
time: string;
durationMinutes: number;
serviceType: string;
status: AppointmentStatus;
room: string;
notes?: string;
fee: number;
isTelehealth?: boolean;
}
export interface SpinalAdjustmentEntry {
vertebra: string; // e.g. "C2", "L5"
vertebra: string;
region: 'Cervical' | 'Thoracic' | 'Lumbar' | 'Pelvis/Sacrum';
listing: string; // e.g., "Right Lateral / Posterior"
listing: string;
technique: 'Diversified' | 'Thompson Drop' | 'Activator' | 'Gonstead' | 'Webster' | 'Flexion-Distraction';
notes?: string;
}
@@ -111,6 +118,7 @@ export interface SoapNote {
appointmentId?: string;
date: string;
status: 'draft' | 'signed';
discipline: ClinicalDiscipline;
subjective: string;
objective: string;
assessment: string;
@@ -118,7 +126,7 @@ export interface SoapNote {
spinalAdjustments: SpinalAdjustmentEntry[];
icd10Codes: Icd10CodeItem[];
cptCodes: CptCodeItem[];
vasScore: number; // 0-10
vasScore: number;
signedAt?: string;
signedBy?: string;
}
@@ -145,7 +153,7 @@ export interface Superbill {
clinicAddress: string;
clinicTaxId: string;
dateOfService: string;
posCode: string; // Place of Service "11 - Office"
posCode: string;
icd10Codes: Icd10CodeItem[];
items: SuperbillLineItem[];
totalAmount: number;
@@ -165,7 +173,7 @@ export interface IntakeSubmission {
chiefComplaints: string[];
painAreas: string[];
vasScore: number;
painNature: string[]; // ['Sharp', 'Dull Ache', 'Shooting']
painNature: string[];
symptomsOnset: string;
medications: string;
priorChiropractic: boolean;
@@ -173,3 +181,51 @@ export interface IntakeSubmission {
submittedAt: string;
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';
}