feat: Initial Mediusa Clinic OS practice management platform ('Jane App But Better')
This commit is contained in:
+388
-59
@@ -1,69 +1,398 @@
|
||||
import Image from "next/image";
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
INITIAL_TENANTS,
|
||||
INITIAL_PROVIDERS,
|
||||
INITIAL_PATIENTS,
|
||||
INITIAL_APPOINTMENTS,
|
||||
INITIAL_SOAP_NOTES,
|
||||
INITIAL_SUPERBILLS,
|
||||
} from '@/lib/mock-data';
|
||||
import {
|
||||
ClinicTenant,
|
||||
Provider,
|
||||
Patient,
|
||||
Appointment,
|
||||
SoapNote,
|
||||
Superbill,
|
||||
} from '@/types/clinical';
|
||||
import { CalendarView } from '@/components/calendar/CalendarView';
|
||||
import { SoapChartEditor } from '@/components/charting/SoapChartEditor';
|
||||
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 {
|
||||
Calendar,
|
||||
FileText,
|
||||
DollarSign,
|
||||
Users,
|
||||
ShieldCheck,
|
||||
Building2,
|
||||
Globe,
|
||||
Sparkles,
|
||||
ChevronDown,
|
||||
Stethoscope,
|
||||
Laptop,
|
||||
CheckCircle2,
|
||||
} from 'lucide-react';
|
||||
|
||||
export default function Home() {
|
||||
// Global Multi-Tenant State
|
||||
const [tenants, setTenants] = useState<ClinicTenant[]>(INITIAL_TENANTS);
|
||||
const [activeTenantId, setActiveTenantId] = useState<string>(INITIAL_TENANTS[0].id);
|
||||
|
||||
// Active Navigation Mode: 'clinic' (EHR Practice OS) | 'patient' (Booking/Intake) | 'superadmin' (Mediusa Command)
|
||||
const [portalMode, setPortalMode] = useState<'clinic' | 'patient' | 'superadmin'>('clinic');
|
||||
|
||||
// Clinic Portal Sub-Tabs: 'calendar' | 'charting' | 'billing' | 'retention'
|
||||
const [clinicTab, setClinicTab] = useState<'calendar' | 'charting' | 'billing' | 'retention'>('calendar');
|
||||
|
||||
// Active Entities
|
||||
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 [activePatient, setActivePatient] = useState<Patient>(INITIAL_PATIENTS[0]); // Marcus Chen
|
||||
const [activeSoapNote, setActiveSoapNote] = useState<SoapNote | undefined>(INITIAL_SOAP_NOTES[0]);
|
||||
const [activeSuperbill, setActiveSuperbill] = useState<Superbill>(INITIAL_SUPERBILLS[0]);
|
||||
|
||||
const activeTenant = tenants.find((t) => t.id === activeTenantId) || tenants[0];
|
||||
const activeProviders = INITIAL_PROVIDERS.filter((p) => p.tenantId === activeTenant.id);
|
||||
const activeProvider = activeProviders[0] || INITIAL_PROVIDERS[0];
|
||||
|
||||
// Handlers
|
||||
const handleSelectAppointment = (apt: Appointment) => {
|
||||
const patientMatch = patients.find((p) => p.id === apt.patientId) || patients[0];
|
||||
setActivePatient(patientMatch);
|
||||
|
||||
const existingSoap = soapNotes.find((s) => s.patientId === apt.patientId && s.appointmentId === apt.id);
|
||||
setActiveSoapNote(existingSoap);
|
||||
setClinicTab('charting');
|
||||
};
|
||||
|
||||
const handleUpdateAppointmentStatus = (aptId: string, status: Appointment['status']) => {
|
||||
setAppointments((prev) =>
|
||||
prev.map((a) => (a.id === aptId ? { ...a, status } : a))
|
||||
);
|
||||
};
|
||||
|
||||
const handleSaveSoapNote = (note: SoapNote) => {
|
||||
setSoapNotes((prev) => {
|
||||
const idx = prev.findIndex((s) => s.id === note.id);
|
||||
if (idx >= 0) {
|
||||
const copy = [...prev];
|
||||
copy[idx] = note;
|
||||
return copy;
|
||||
}
|
||||
return [...prev, note];
|
||||
});
|
||||
setActiveSoapNote(note);
|
||||
};
|
||||
|
||||
const handleGenerateSuperbillFromNote = (note: SoapNote) => {
|
||||
const newSb: Superbill = {
|
||||
id: `sb-${Date.now()}`,
|
||||
tenantId: activeTenant.id,
|
||||
invoiceNumber: `SB-2026-${Math.floor(1000 + Math.random() * 9000)}`,
|
||||
patientId: activePatient.id,
|
||||
patientName: `${activePatient.firstName} ${activePatient.lastName}`,
|
||||
patientDob: activePatient.dob,
|
||||
patientAddress: activePatient.address,
|
||||
providerName: note.providerName,
|
||||
providerNpi: activeProvider.npi,
|
||||
clinicName: activeTenant.name,
|
||||
clinicAddress: activeTenant.address,
|
||||
clinicTaxId: activeTenant.taxId,
|
||||
dateOfService: note.date,
|
||||
posCode: '11 (Office)',
|
||||
icd10Codes: note.icd10Codes,
|
||||
items: note.cptCodes.map((cpt) => ({
|
||||
cptCode: cpt.code,
|
||||
description: cpt.description,
|
||||
units: 1,
|
||||
rate: cpt.fee,
|
||||
total: cpt.fee,
|
||||
})),
|
||||
totalAmount: note.cptCodes.reduce((sum, c) => sum + c.fee, 0),
|
||||
patientPaid: note.cptCodes.reduce((sum, c) => sum + c.fee, 0),
|
||||
balanceDue: 0,
|
||||
paymentMethod: 'Stripe Card',
|
||||
generatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setSuperbills((prev) => [newSb, ...prev]);
|
||||
setActiveSuperbill(newSb);
|
||||
setClinicTab('billing');
|
||||
};
|
||||
|
||||
const handleMarkPaid = (superbillId: string, method: Superbill['paymentMethod']) => {
|
||||
setSuperbills((prev) =>
|
||||
prev.map((sb) =>
|
||||
sb.id === superbillId
|
||||
? { ...sb, balanceDue: 0, patientPaid: sb.totalAmount, paymentMethod: method }
|
||||
: sb
|
||||
)
|
||||
);
|
||||
if (activeSuperbill.id === superbillId) {
|
||||
setActiveSuperbill((prev) => ({
|
||||
...prev,
|
||||
balanceDue: 0,
|
||||
patientPaid: prev.totalAmount,
|
||||
paymentMethod: method,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const handlePatientBookingComplete = (newAptData: any) => {
|
||||
const newApt: Appointment = {
|
||||
id: `apt-${Date.now()}`,
|
||||
tenantId: activeTenant.id,
|
||||
patientId: `pat-${Date.now()}`,
|
||||
...newAptData,
|
||||
};
|
||||
setAppointments((prev) => [newApt, ...prev]);
|
||||
};
|
||||
|
||||
const handleAddTenant = (newTenant: ClinicTenant) => {
|
||||
setTenants((prev) => [...prev, newTenant]);
|
||||
};
|
||||
|
||||
const handleSwitchTenant = (tenantId: string) => {
|
||||
setActiveTenantId(tenantId);
|
||||
setPortalMode('clinic');
|
||||
setClinicTab('calendar');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
<Image
|
||||
className="dark:invert h-5 w-[100px]"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the{" "}
|
||||
<code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
|
||||
page.tsx
|
||||
</code>{" "}
|
||||
file.
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
<div className="min-h-screen bg-slate-950 text-slate-100 flex flex-col">
|
||||
{/* Top Global Command Bar */}
|
||||
<header className="sticky top-0 z-40 bg-slate-950/90 backdrop-blur-md border-b border-slate-800/80 px-4 lg:px-8 py-3">
|
||||
<div className="max-w-7xl mx-auto flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
{/* Brand & Clinic Domain Selector */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span
|
||||
className="w-8 h-8 rounded-xl flex items-center justify-center font-black text-xs text-slate-950 shadow-md"
|
||||
style={{ backgroundColor: activeTenant.brandColor }}
|
||||
>
|
||||
M+
|
||||
</span>
|
||||
<div>
|
||||
<div className="text-xs font-black tracking-tight text-white flex items-center gap-1.5">
|
||||
<span>MEDIUSA CLINIC OS</span>
|
||||
<span className="text-[10px] px-1.5 py-0.2 rounded bg-teal-500/20 text-teal-300 font-mono">
|
||||
v2.4 PRO
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-[11px] text-slate-400 font-mono flex items-center gap-1">
|
||||
<Globe className="w-3 h-3 text-teal-400" />
|
||||
https://{activeTenant.domain}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tenant Switcher Dropdown */}
|
||||
<div className="relative group">
|
||||
<select
|
||||
value={activeTenantId}
|
||||
onChange={(e) => setActiveTenantId(e.target.value)}
|
||||
className="appearance-none bg-slate-900 hover:bg-slate-800 border border-slate-700 text-slate-200 text-xs font-semibold rounded-xl px-3 py-1.5 pr-8 cursor-pointer focus:outline-none focus:border-teal-500 transition"
|
||||
>
|
||||
{tenants.map((t) => (
|
||||
<option key={t.id} value={t.id}>
|
||||
🏥 {t.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<ChevronDown className="w-3.5 h-3.5 text-slate-400 absolute right-2.5 top-1/2 -translate-y-1/2 pointer-events-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Core Portal Mode Switcher */}
|
||||
<div className="flex items-center gap-1 bg-slate-900 p-1 rounded-xl border border-slate-800 text-xs font-semibold">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPortalMode('clinic')}
|
||||
className={`px-3 py-1.5 rounded-lg flex items-center gap-1.5 transition ${
|
||||
portalMode === 'clinic'
|
||||
? 'bg-teal-500 text-slate-950 font-bold shadow'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
<Stethoscope className="w-3.5 h-3.5" />
|
||||
Doctor & Practice OS
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPortalMode('patient')}
|
||||
className={`px-3 py-1.5 rounded-lg flex items-center gap-1.5 transition ${
|
||||
portalMode === 'patient'
|
||||
? 'bg-teal-500 text-slate-950 font-bold shadow'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert h-[14px] w-4"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={14}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
<Laptop className="w-3.5 h-3.5" />
|
||||
Patient Booking & Intake
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPortalMode('superadmin')}
|
||||
className={`px-3 py-1.5 rounded-lg flex items-center gap-1.5 transition ${
|
||||
portalMode === 'superadmin'
|
||||
? 'bg-indigo-600 text-white font-bold shadow'
|
||||
: 'text-slate-400 hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<Building2 className="w-3.5 h-3.5 text-indigo-300" />
|
||||
Mediusa Super-Admin
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sub-Tabs for Doctor Clinic OS */}
|
||||
{portalMode === 'clinic' && (
|
||||
<div className="max-w-7xl mx-auto flex items-center gap-2 mt-3 pt-2 border-t border-slate-800/80 overflow-x-auto text-xs">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setClinicTab('calendar')}
|
||||
className={`px-3 py-1.5 rounded-lg font-medium flex items-center gap-1.5 transition whitespace-nowrap ${
|
||||
clinicTab === 'calendar'
|
||||
? 'bg-slate-800 text-teal-300 border border-teal-500/40 font-bold'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
<Calendar className="w-3.5 h-3.5" />
|
||||
Fast Calendar & Schedule
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setClinicTab('charting')}
|
||||
className={`px-3 py-1.5 rounded-lg font-medium flex items-center gap-1.5 transition whitespace-nowrap ${
|
||||
clinicTab === 'charting'
|
||||
? 'bg-slate-800 text-teal-300 border border-teal-500/40 font-bold'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
<FileText className="w-3.5 h-3.5" />
|
||||
SOAP Chart & 2D Spine Map ({activePatient.firstName} {activePatient.lastName})
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setClinicTab('billing')}
|
||||
className={`px-3 py-1.5 rounded-lg font-medium flex items-center gap-1.5 transition whitespace-nowrap ${
|
||||
clinicTab === 'billing'
|
||||
? 'bg-slate-800 text-teal-300 border border-teal-500/40 font-bold'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
<DollarSign className="w-3.5 h-3.5" />
|
||||
Superbills & Stripe Billing
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setClinicTab('retention')}
|
||||
className={`px-3 py-1.5 rounded-lg font-medium flex items-center gap-1.5 transition whitespace-nowrap ${
|
||||
clinicTab === 'retention'
|
||||
? 'bg-slate-800 text-amber-300 border border-amber-500/40 font-bold'
|
||||
: 'text-slate-400 hover:text-slate-200'
|
||||
}`}
|
||||
>
|
||||
<Users className="w-3.5 h-3.5 text-amber-400" />
|
||||
Care Plan Retention Radar
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* Main App Container */}
|
||||
<main className="flex-1 max-w-7xl w-full mx-auto p-4 sm:p-6 lg:p-8">
|
||||
{/* PORTAL MODE 1: Clinic Practice OS */}
|
||||
{portalMode === 'clinic' && (
|
||||
<div>
|
||||
{clinicTab === 'calendar' && (
|
||||
<CalendarView
|
||||
appointments={appointments}
|
||||
providers={activeProviders}
|
||||
activeTenant={activeTenant}
|
||||
onSelectAppointment={handleSelectAppointment}
|
||||
onUpdateStatus={handleUpdateAppointmentStatus}
|
||||
onNewAppointmentClick={() => setPortalMode('patient')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{clinicTab === 'charting' && (
|
||||
<SoapChartEditor
|
||||
patient={activePatient}
|
||||
provider={activeProvider}
|
||||
initialSoapNote={activeSoapNote}
|
||||
onSaveSoapNote={handleSaveSoapNote}
|
||||
onGenerateSuperbill={handleGenerateSuperbillFromNote}
|
||||
onBackToCalendar={() => setClinicTab('calendar')}
|
||||
/>
|
||||
)}
|
||||
|
||||
{clinicTab === 'billing' && (
|
||||
<SuperbillView
|
||||
superbill={activeSuperbill}
|
||||
activeTenant={activeTenant}
|
||||
onBack={() => setClinicTab('charting')}
|
||||
onMarkPaid={handleMarkPaid}
|
||||
/>
|
||||
)}
|
||||
|
||||
{clinicTab === 'retention' && (
|
||||
<RetentionView
|
||||
patients={patients}
|
||||
activeTenant={activeTenant}
|
||||
onSelectPatientChart={(patient) => {
|
||||
setActivePatient(patient);
|
||||
const existing = soapNotes.find((s) => s.patientId === patient.id);
|
||||
setActiveSoapNote(existing);
|
||||
setClinicTab('charting');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* PORTAL MODE 2: White-Labeled Patient Booking & Intake */}
|
||||
{portalMode === 'patient' && (
|
||||
<PatientPortalView
|
||||
activeTenant={activeTenant}
|
||||
providers={activeProviders}
|
||||
onBookingComplete={handlePatientBookingComplete}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* PORTAL MODE 3: Mediusa Super-Admin Command Center */}
|
||||
{portalMode === 'superadmin' && (
|
||||
<SuperAdminView
|
||||
tenants={tenants}
|
||||
onAddTenant={handleAddTenant}
|
||||
onSwitchTenant={handleSwitchTenant}
|
||||
/>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{/* Footer Branding (Mediusa / AI Pilots Ecosystem) */}
|
||||
<footer className="bg-slate-950 border-t border-slate-800/80 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>
|
||||
Mediusa Clinic OS • Proprietary Practice Management Engine for AI Pilots / Mediusa
|
||||
</span>
|
||||
<span className="font-mono text-slate-400">
|
||||
hello@aipilots.site • HIPAA Certified • Sub-30s Clinical Charting Standard
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user