feat: Initial Mediusa Clinic OS practice management platform ('Jane App But Better')

This commit is contained in:
2026-09-05 14:12:19 -07:00
parent 441e5eb57d
commit deb5197e53
17 changed files with 4675 additions and 71 deletions
+478
View File
@@ -0,0 +1,478 @@
'use client';
import React, { useState } from 'react';
import { SoapNote, Patient, Provider, SpinalAdjustmentEntry, CptCodeItem, Icd10CodeItem } 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 {
FileText,
Sparkles,
CheckCircle,
Copy,
PenTool,
Save,
DollarSign,
ChevronLeft,
Tag,
ShieldCheck,
AlertCircle,
} from 'lucide-react';
interface SoapChartEditorProps {
patient: Patient;
provider: Provider;
initialSoapNote?: SoapNote;
onSaveSoapNote: (note: SoapNote) => void;
onGenerateSuperbill: (note: SoapNote) => void;
onBackToCalendar: () => void;
}
export const SoapChartEditor: React.FC<SoapChartEditorProps> = ({
patient,
provider,
initialSoapNote,
onSaveSoapNote,
onGenerateSuperbill,
onBackToCalendar,
}) => {
const [subjective, setSubjective] = useState(
initialSoapNote?.subjective ||
`Patient presents for scheduled visit ${patient.carePlan.completedVisits + 1} of ${
patient.carePlan.totalVisits
}. Chief complaint: ${patient.chiefComplaint}. VAS pain reported at 4/10.`
);
const [objective, setObjective] = useState(
initialSoapNote?.objective ||
'Postural examination reveals mild anterior head carriage and unleveling of right iliac crest. Motion palpation identifies segmental hypomobility with paraspinal guarding.'
);
const [assessment, setAssessment] = useState(
initialSoapNote?.assessment ||
'Segmental and somatic dysfunction of lumbar and cervical spine. Patient progressing steadily toward functional goals outlined in care plan.'
);
const [plan, setPlan] = useState(
initialSoapNote?.plan ||
'1. High velocity low amplitude (HVLA) manipulative therapy delivered to listed subluxations.\n2. Prescribed postural home stabilization exercises.\n3. Return in 3 days for scheduled care plan visit.'
);
const [adjustments, setAdjustments] = useState<SpinalAdjustmentEntry[]>(
initialSoapNote?.spinalAdjustments || [
{ vertebra: 'L4', region: 'Lumbar', listing: 'Right Posterior (RP)', technique: 'Diversified' },
{ vertebra: 'Sacrum', region: 'Pelvis/Sacrum', listing: 'Right Sacral Base Anterior', technique: 'Thompson Drop' },
]
);
const [selectedIcd10, setSelectedIcd10] = useState<Icd10CodeItem[]>(
initialSoapNote?.icd10Codes || [STANDARD_ICD10_CODES[2], STANDARD_ICD10_CODES[5]] // M99.03 & M54.50
);
const [selectedCpt, setSelectedCpt] = useState<CptCodeItem[]>(
initialSoapNote?.cptCodes || [STANDARD_CPT_CODES[0], STANDARD_CPT_CODES[3]] // 98940 & 97140
);
const [vasScore, setVasScore] = useState<number>(initialSoapNote?.vasScore || 3);
const [isSigned, setIsSigned] = useState<boolean>(initialSoapNote?.status === 'signed');
const [signedTimestamp, setSignedTimestamp] = useState<string | undefined>(initialSoapNote?.signedAt);
// Ambient AI Scribe callback
const handleApplyExtractedSoap = (data: {
vasScore: number;
subjective: string;
objective: string;
assessment: string;
plan: string;
spinalAdjustments: SpinalAdjustmentEntry[];
icd10: string[];
cpt: string[];
}) => {
setVasScore(data.vasScore);
setSubjective(data.subjective);
setObjective(data.objective);
setAssessment(data.assessment);
setPlan(data.plan);
if (data.spinalAdjustments && data.spinalAdjustments.length > 0) {
setAdjustments(data.spinalAdjustments);
}
// Auto map ICD10
const matchedIcds = STANDARD_ICD10_CODES.filter((icd) => data.icd10.includes(icd.code));
if (matchedIcds.length > 0) setSelectedIcd10(matchedIcds);
// Auto map CPT
const matchedCpts = STANDARD_CPT_CODES.filter((cpt) => data.cpt.includes(cpt.code));
if (matchedCpts.length > 0) setSelectedCpt(matchedCpts);
};
const handleToggleAdjustment = (entry: SpinalAdjustmentEntry) => {
setAdjustments((prev) => {
const exists = prev.some((a) => a.vertebra === entry.vertebra);
if (exists) {
return prev.filter((a) => a.vertebra !== entry.vertebra);
} else {
return [...prev, entry];
}
});
};
const handleCloneLastNote = () => {
setSubjective('Patient reports continued symptom improvement following last spinal adjustment. Morning stiffness resolved within 10 minutes. Current pain rated 3/10.');
setObjective('Palpation reveals decreased tone in lumbar paraspinals. Persistent fixation noted at L4-L5 with right sacral torsion.');
setAssessment('Care plan compliance high. Significant restoration of active range of motion noted.');
setPlan('1. Diversified adjustment delivered to indicated segments.\n2. Manual myofascial release 10 minutes.\n3. Continue active home rehabilitation.');
};
const handleSignChart = () => {
const now = new Date().toISOString();
setIsSigned(true);
setSignedTimestamp(now);
const savedNote: SoapNote = {
id: initialSoapNote?.id || `soap-${Date.now()}`,
tenantId: patient.tenantId,
patientId: patient.id,
patientName: `${patient.firstName} ${patient.lastName}`,
providerId: provider.id,
providerName: provider.name,
date: '2026-09-05',
status: 'signed',
vasScore,
subjective,
objective,
assessment,
plan,
spinalAdjustments: adjustments,
icd10Codes: selectedIcd10,
cptCodes: selectedCpt,
signedAt: now,
signedBy: `${provider.name}, ${provider.credentials} (NPI ${provider.npi})`,
};
onSaveSoapNote(savedNote);
};
const handleCreateSuperbillClick = () => {
const savedNote: SoapNote = {
id: initialSoapNote?.id || `soap-${Date.now()}`,
tenantId: patient.tenantId,
patientId: patient.id,
patientName: `${patient.firstName} ${patient.lastName}`,
providerId: provider.id,
providerName: provider.name,
date: '2026-09-05',
status: isSigned ? 'signed' : 'draft',
vasScore,
subjective,
objective,
assessment,
plan,
spinalAdjustments: adjustments,
icd10Codes: selectedIcd10,
cptCodes: selectedCpt,
signedAt: signedTimestamp,
signedBy: `${provider.name}, ${provider.credentials}`,
};
onGenerateSuperbill(savedNote);
};
return (
<div className="space-y-6">
{/* Top Breadcrumb & Patient Header */}
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-5 shadow-xl flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-center gap-4">
<button
type="button"
onClick={onBackToCalendar}
className="p-2 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-xl transition"
title="Back to Calendar"
>
<ChevronLeft className="w-5 h-5" />
</button>
<div>
<div className="flex items-center gap-2">
<h3 className="text-lg font-black text-white">
{patient.firstName} {patient.lastName}
</h3>
<span className="text-xs px-2.5 py-0.5 rounded-full bg-teal-500/20 text-teal-300 border border-teal-500/30 font-semibold">
Care Plan: Visit {patient.carePlan.completedVisits + 1} of {patient.carePlan.totalVisits}
</span>
{isSigned ? (
<span className="text-xs px-2.5 py-0.5 rounded-full bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 font-semibold flex items-center gap-1">
<ShieldCheck className="w-3.5 h-3.5" /> Signed &amp; Locked
</span>
) : (
<span className="text-xs px-2.5 py-0.5 rounded-full bg-amber-500/20 text-amber-300 border border-amber-500/30 font-semibold">
In Progress (Draft)
</span>
)}
</div>
<div className="flex flex-wrap items-center gap-3 text-xs text-slate-400 mt-1">
<span>DOB: {patient.dob}</span>
<span></span>
<span>Insurance: {patient.insuranceName}</span>
<span></span>
<span className="text-teal-400 font-medium">Attending: {provider.name}</span>
</div>
</div>
</div>
{/* Action buttons */}
<div className="flex items-center gap-2">
<button
type="button"
onClick={handleCloneLastNote}
className="px-3 py-2 rounded-xl bg-slate-800 hover:bg-slate-700 text-slate-300 text-xs font-semibold flex items-center gap-1.5 transition"
>
<Copy className="w-3.5 h-3.5" />
Clone Last Note
</button>
<button
type="button"
onClick={handleSignChart}
disabled={isSigned}
className={`px-4 py-2 rounded-xl text-xs font-bold flex items-center gap-1.5 shadow-lg transition ${
isSigned
? 'bg-emerald-500/20 text-emerald-400 border border-emerald-500/40 cursor-default'
: 'bg-teal-500 hover:bg-teal-400 text-slate-950 shadow-teal-500/20'
}`}
>
<CheckCircle className="w-4 h-4" />
{isSigned ? 'Signed by Doctor' : 'Sign & Complete Note'}
</button>
<button
type="button"
onClick={handleCreateSuperbillClick}
className="px-4 py-2 rounded-xl bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold flex items-center gap-1.5 shadow-lg shadow-indigo-600/20 transition"
>
<DollarSign className="w-4 h-4" />
Generate Superbill
</button>
</div>
</div>
{/* Ambient AI Clinical Scribe Section */}
<AmbientAudioRecorder onApplyExtractedSoap={handleApplyExtractedSoap} />
{/* 2D Spine Visualizer Subluxation Clicker */}
<SpineVisualizer
adjustments={adjustments}
onToggleAdjustment={handleToggleAdjustment}
readOnly={isSigned}
/>
{/* Structured SOAP Fields Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{/* Subjective */}
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 shadow-xl flex flex-col justify-between">
<div>
<div className="flex items-center justify-between pb-2 border-b border-slate-800 mb-2">
<span className="text-xs font-bold text-teal-400 uppercase tracking-wider flex items-center gap-1.5">
<span className="w-5 h-5 rounded-md bg-teal-500/20 flex items-center justify-center font-black text-teal-300">
S
</span>
Subjective Complaint &amp; History
</span>
<span className="text-xs font-mono text-slate-400">VAS: {vasScore}/10</span>
</div>
<textarea
rows={4}
value={subjective}
disabled={isSigned}
onChange={(e) => setSubjective(e.target.value)}
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-xs text-slate-200 focus:outline-none focus:border-teal-500 resize-none font-sans"
placeholder="Patient reports symptoms, VAS rating, changes since last visit..."
/>
</div>
<div className="text-[10px] text-slate-500 mt-2">
Auto-populated from Intake and Ambient Scribe.
</div>
</div>
{/* Objective */}
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 shadow-xl flex flex-col justify-between">
<div>
<div className="flex items-center justify-between pb-2 border-b border-slate-800 mb-2">
<span className="text-xs font-bold text-teal-400 uppercase tracking-wider flex items-center gap-1.5">
<span className="w-5 h-5 rounded-md bg-teal-500/20 flex items-center justify-center font-black text-teal-300">
O
</span>
Objective Findings &amp; Palpation
</span>
<span className="text-xs font-mono text-slate-400">
{adjustments.length} Segment(s) Listed
</span>
</div>
<textarea
rows={4}
value={objective}
disabled={isSigned}
onChange={(e) => setObjective(e.target.value)}
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-xs text-slate-200 focus:outline-none focus:border-teal-500 resize-none font-sans"
placeholder="Physical findings, spinal fixations, muscle spasms, range of motion..."
/>
</div>
<div className="text-[10px] text-slate-500 mt-2">
Vertebrae selected in 2D Spine Map sync automatically.
</div>
</div>
{/* Assessment */}
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 shadow-xl flex flex-col justify-between">
<div>
<div className="flex items-center justify-between pb-2 border-b border-slate-800 mb-2">
<span className="text-xs font-bold text-teal-400 uppercase tracking-wider flex items-center gap-1.5">
<span className="w-5 h-5 rounded-md bg-teal-500/20 flex items-center justify-center font-black text-teal-300">
A
</span>
Clinical Assessment &amp; Progress
</span>
</div>
<textarea
rows={4}
value={assessment}
disabled={isSigned}
onChange={(e) => setAssessment(e.target.value)}
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-xs text-slate-200 focus:outline-none focus:border-teal-500 resize-none font-sans"
placeholder="Clinical impression, diagnosis evaluation, treatment response..."
/>
</div>
<div className="text-[10px] text-slate-500 mt-2">
Maps to ICD-10 diagnostic codes below.
</div>
</div>
{/* Plan */}
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 shadow-xl flex flex-col justify-between">
<div>
<div className="flex items-center justify-between pb-2 border-b border-slate-800 mb-2">
<span className="text-xs font-bold text-teal-400 uppercase tracking-wider flex items-center gap-1.5">
<span className="w-5 h-5 rounded-md bg-teal-500/20 flex items-center justify-center font-black text-teal-300">
P
</span>
Treatment Plan &amp; Home Care
</span>
</div>
<textarea
rows={4}
value={plan}
disabled={isSigned}
onChange={(e) => setPlan(e.target.value)}
className="w-full bg-slate-950 border border-slate-800 rounded-xl p-3 text-xs text-slate-200 focus:outline-none focus:border-teal-500 resize-none font-sans"
placeholder="Techniques delivered, rehabilitation modalities, next visit schedule..."
/>
</div>
<div className="text-[10px] text-slate-500 mt-2">
Determines CPT procedure line items on Superbill.
</div>
</div>
</div>
{/* ICD-10 & CPT Billing Code Selectors */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{/* ICD-10 Codes */}
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 shadow-xl">
<div className="flex items-center justify-between pb-2 border-b border-slate-800 mb-3">
<span className="text-xs font-bold text-slate-200 uppercase tracking-wider flex items-center gap-1.5">
<Tag className="w-3.5 h-3.5 text-teal-400" />
ICD-10 Diagnostic Codes
</span>
<span className="text-xs text-teal-400 font-mono">{selectedIcd10.length} Selected</span>
</div>
<div className="flex flex-wrap gap-1.5 max-h-40 overflow-y-auto pr-1">
{STANDARD_ICD10_CODES.map((item) => {
const active = selectedIcd10.some((c) => c.code === item.code);
return (
<button
key={item.code}
type="button"
disabled={isSigned}
onClick={() => {
if (active) {
setSelectedIcd10((prev) => prev.filter((c) => c.code !== item.code));
} else {
setSelectedIcd10((prev) => [...prev, item]);
}
}}
className={`px-2.5 py-1.5 rounded-lg text-xs font-medium text-left transition ${
active
? 'bg-teal-500 text-slate-950 font-bold shadow-sm'
: 'bg-slate-800 text-slate-300 hover:bg-slate-700'
}`}
>
<span className="font-mono font-bold mr-1">{item.code}</span>
<span className="text-[11px] opacity-90 truncate">{item.description}</span>
</button>
);
})}
</div>
</div>
{/* CPT Codes */}
<div className="bg-slate-900 border border-slate-800 rounded-2xl p-4 shadow-xl">
<div className="flex items-center justify-between pb-2 border-b border-slate-800 mb-3">
<span className="text-xs font-bold text-slate-200 uppercase tracking-wider flex items-center gap-1.5">
<DollarSign className="w-3.5 h-3.5 text-teal-400" />
CPT Procedure Codes &amp; Fee Schedule
</span>
<span className="text-xs text-teal-400 font-mono">
Total: ${selectedCpt.reduce((sum, c) => sum + c.fee, 0)}
</span>
</div>
<div className="flex flex-wrap gap-1.5 max-h-40 overflow-y-auto pr-1">
{STANDARD_CPT_CODES.map((item) => {
const active = selectedCpt.some((c) => c.code === item.code);
return (
<button
key={item.code}
type="button"
disabled={isSigned}
onClick={() => {
if (active) {
setSelectedCpt((prev) => prev.filter((c) => c.code !== item.code));
} else {
setSelectedCpt((prev) => [...prev, item]);
}
}}
className={`px-2.5 py-1.5 rounded-lg text-xs font-medium text-left transition flex items-center justify-between gap-2 ${
active
? 'bg-indigo-600 text-white font-bold shadow-sm'
: 'bg-slate-800 text-slate-300 hover:bg-slate-700'
}`}
>
<div>
<span className="font-mono font-bold mr-1">{item.code}</span>
<span className="text-[11px] opacity-90 truncate">{item.description}</span>
</div>
<span className="font-mono text-teal-300 shrink-0 font-bold">${item.fee}</span>
</button>
);
})}
</div>
</div>
</div>
{/* Signature Attestation Footer */}
{isSigned && (
<div className="p-4 bg-emerald-500/10 border border-emerald-500/30 rounded-2xl flex items-center justify-between text-xs text-emerald-300">
<div className="flex items-center gap-2">
<ShieldCheck className="w-5 h-5 text-emerald-400" />
<div>
<strong>Electronically Signed by:</strong> {provider.name}, {provider.credentials} (NPI {provider.npi})
<div className="text-[11px] text-emerald-400/80 mt-0.5">
Timestamp: {signedTimestamp || '2026-09-05T09:25:00-07:00'} Immutable Legal Record
</div>
</div>
</div>
<button
type="button"
onClick={handleCreateSuperbillClick}
className="px-3 py-1.5 bg-emerald-500 hover:bg-emerald-400 text-slate-950 font-bold rounded-lg transition"
>
Open Superbill
</button>
</div>
)}
</div>
);
};