Spaces:
Running
Running
| class HealeoApp { | |
| constructor() { | |
| this.currentPatient = null; | |
| this.currentTab = 'overview'; | |
| this.currentFilter = 'all'; | |
| this.recommendations = []; | |
| this.approvalHistory = []; | |
| this.patients = [ | |
| { | |
| id: 1, | |
| name: "John Doe", | |
| age: 58, | |
| gender: "Male", | |
| avatar: "JD", | |
| conditions: ["Type 2 Diabetes", "Hypertension"], | |
| lastVisit: "2024-01-15", | |
| nextFollowup: "2024-02-01", | |
| adherence: 87, | |
| risk: "Moderate", | |
| status: "active", | |
| carePlan: { | |
| activeConditions: [ | |
| { condition: "Type 2 Diabetes", severity: "moderate", status: "managed" }, | |
| { condition: "Hypertension", severity: "mild", status: "improving" } | |
| ], | |
| medications: [ | |
| { name: "Metformin", dosage: "1000mg", frequency: "Twice daily", adherence: 95 }, | |
| { name: "Lisinopril", dosage: "10mg", frequency: "Once daily", adherence: 82 } | |
| ], | |
| goals: [ | |
| { text: "Reduce HbA1c to <7%", target: "6 months", progress: 65 }, | |
| { text: "Blood pressure <130/80", target: "3 months", progress: 45 } | |
| ], | |
| lifestyle: [ | |
| "Mediterranean diet - strict adherence", | |
| "30min walking 5x/week", | |
| "Sleep hygiene protocol" | |
| ] | |
| }, | |
| labs: [ | |
| { date: "2024-01-15", type: "HbA1c", value: "7.2", unit: "%", trend: "down" }, | |
| { date: "2024-01-15", type: "Blood Pressure", value: "138/85", unit: "mmHg", trend: "stable" } | |
| ] | |
| }, | |
| { | |
| id: 2, | |
| name: "Sarah Chen", | |
| age: 34, | |
| gender: "Female", | |
| avatar: "SC", | |
| conditions: ["Asthma", "Anxiety Disorder"], | |
| lastVisit: "2024-01-10", | |
| nextFollowup: "2024-01-25", | |
| adherence: 92, | |
| risk: "Low", | |
| status: "stable", | |
| carePlan: { | |
| activeConditions: [ | |
| { condition: "Asthma", severity: "moderate", status: "controlled" }, | |
| { condition: "Anxiety Disorder", severity: "mild", status: "managed" } | |
| ], | |
| medications: [ | |
| { name: "Albuterol", dosage: "90mcg", frequency: "As needed", adherence: 88 }, | |
| { name: "Sertraline", dosage: "50mg", frequency: "Once daily", adherence: 98 } | |
| ], | |
| goals: [ | |
| { text: "Reduce anxiety attacks to <2/month", target: "Ongoing", progress: 80 }, | |
| { text: "Peak flow >350 L/min", target: "Maintenance", progress: 90 } | |
| ], | |
| lifestyle: [ | |
| "Daily meditation practice", | |
| "Yoga 3x/week", | |
| "Air quality monitoring" | |
| ] | |
| }, | |
| labs: [ | |
| { date: "2024-01-10", type: "Spirometry", value: "85%", unit: "predicted", trend: "up" } | |
| ] | |
| }, | |
| { | |
| id: 3, | |
| name: "Robert Martinez", | |
| age: 67, | |
| gender: "Male", | |
| avatar: "RM", | |
| conditions: ["COPD", "Heart Failure"], | |
| lastVisit: "2024-01-08", | |
| nextFollowup: "2024-01-22", | |
| adherence: 73, | |
| risk: "High", | |
| status: "critical", | |
| carePlan: { | |
| activeConditions: [ | |
| { condition: "COPD", severity: "severe", status: "worsening" }, | |
| { condition: "Heart Failure", severity: "moderate", status: "stable" } | |
| ], | |
| medications: [ | |
| { name: "Furosemide", dosage: "40mg", frequency: "Once daily", adherence: 65 }, | |
| { name: "Spironolactone", dosage: "25mg", frequency: "Once daily", adherence: 70 } | |
| ], | |
| goals: [ | |
| { text: "Reduce ED visits", target: "6 months", progress: 30 }, | |
| { text: "Weight stability ±2lbs", target: "Monthly", progress: 60 } | |
| ], | |
| lifestyle: [ | |
| "Sodium restriction <2g/day", | |
| "Daily weight monitoring", | |
| "Pulmonary rehab 2x/week" | |
| ] | |
| }, | |
| labs: [ | |
| { date: "2024-01-08", type: "BNP", value: "450", unit: "pg/mL", trend: "up" }, | |
| { date: "2024-01-08", type: "eGFR", value: "45", unit: "mL/min", trend: "down" } | |
| ] | |
| }, | |
| { | |
| id: 4, | |
| name: "Emily Watson", | |
| age: 45, | |
| gender: "Female", | |
| avatar: "EW", | |
| conditions: ["Rheumatoid Arthritis"], | |
| lastVisit: "2024-01-12", | |
| nextFollowup: "2024-02-12", | |
| adherence: 96, | |
| risk: "Low", | |
| status: "review", | |
| carePlan: { | |
| activeConditions: [ | |
| { condition: "Rheumatoid Arthritis", severity: "moderate", status: "improving" } | |
| ], | |
| medications: [ | |
| { name: "Methotrexate", dosage: "15mg", frequency: "Weekly", adherence: 96 }, | |
| { name: "Folic Acid", dosage: "1mg", frequency: "Daily", adherence: 100 } | |
| ], | |
| goals: [ | |
| { text: "DAS28 <3.2", target: "3 months", progress: 75 }, | |
| { text: "Pain score <4/10", target: "Ongoing", progress: 60 } | |
| ], | |
| lifestyle: [ | |
| "Joint protection techniques", | |
| "Aquatic therapy", | |
| "Anti-inflammatory diet" | |
| ] | |
| }, | |
| labs: [ | |
| { date: "2024-01-12", type: "CRP", value: "8.5", unit: "mg/L", trend: "down" } | |
| ] | |
| } | |
| ]; | |
| this.aiRecommendations = [ | |
| { | |
| id: 1, | |
| patientId: 1, | |
| type: "medication", | |
| title: "Dosage Adjustment Suggested", | |
| description: "Increase Metformin to 1500mg based on current HbA1c trending and BMI.", | |
| rationale: "Current HbA1c of 7.2% suggests suboptimal glycemic control. Patient BMI 32 indicates insulin resistance.", | |
| confidence: 92, | |
| action: "Adjust prescription", | |
| timestamp: "10 mins ago" | |
| }, | |
| { | |
| id: 2, | |
| patientId: 1, | |
| type: "followup", | |
| title: "Follow-up Timeline Update", | |
| description: "Move next visit from 2/1 to 1/28 for earlier intervention.", | |
| rationale: "Recent lab trends show glucose variability. Earlier assessment recommended.", | |
| confidence: 87, | |
| action: "Reschedule visit", | |
| timestamp: "15 mins ago" | |
| }, | |
| { | |
| id: 3, | |
| patientId: 1, | |
| type: "monitoring", | |
| title: "Add Continuous Glucose Monitor", | |
| description: "Recommend CGM for 2 weeks to assess glycemic patterns.", | |
| rationale: "Identifying fasting vs post-prandial spikes will optimize medication timing.", | |
| confidence: 95, | |
| action: "Order device", | |
| timestamp: "20 mins ago" | |
| } | |
| ]; | |
| this.timeline = [ | |
| { date: "2024-01-15", type: "visit", title: "Routine Visit", description: "HbA1c discussed, adherence reviewed", icon: "stethoscope" }, | |
| { date: "2024-01-10", type: "medication", title: "Medication Refill", description: "Metformin 90-day supply issued", icon: "pill" }, | |
| { date: "2024-01-05", type: "lab", title: "Lab Results", description: "HbA1c: 7.2% (down from 7.8%)", icon: "flask-conical", highlight: true }, | |
| { date: "2023-12-20", type: "therapy", title: "Nutritionist Consult", description: "Mediterranean diet counseling", icon: "apple" }, | |
| { date: "2023-12-01", type: "visit", title: "Emergency Visit", description: "Hypoglycemic episode resolved", icon: "alert-circle", highlight: true } | |
| ]; | |
| this.init(); | |
| } | |
| init() { | |
| this.renderPatientList(); | |
| this.selectPatient(this.patients[0]); | |
| lucide.createIcons(); | |
| } | |
| filterPatients(query) { | |
| const filtered = this.patients.filter(p => | |
| p.name.toLowerCase().includes(query.toLowerCase()) || | |
| p.conditions.some(c => c.toLowerCase().includes(query.toLowerCase())) | |
| ); | |
| this.renderPatientList(filtered); | |
| } | |
| setFilter(filter) { | |
| this.currentFilter = filter; | |
| document.querySelectorAll('.filter-btn').forEach(btn => { | |
| if(btn.dataset.filter === filter) { | |
| btn.classList.remove('bg-gray-100', 'text-gray-600', 'border-gray-200'); | |
| btn.classList.add('bg-blue-100', 'text-blue-700', 'border-blue-200'); | |
| } else { | |
| btn.classList.add('bg-gray-100', 'text-gray-600', 'border-gray-200'); | |
| btn.classList.remove('bg-blue-100', 'text-blue-700', 'border-blue-200'); | |
| } | |
| }); | |
| let filtered = this.patients; | |
| if(filter === 'critical') filtered = this.patients.filter(p => p.status === 'critical'); | |
| if(filter === 'review') filtered = this.patients.filter(p => p.status === 'review'); | |
| this.renderPatientList(filtered); | |
| } | |
| renderPatientList(patients = this.patients) { | |
| const container = document.getElementById('patientList'); | |
| container.innerHTML = ''; | |
| patients.forEach(patient => { | |
| const card = document.createElement('div'); | |
| card.className = `p-3 rounded-lg border cursor-pointer transition-all hover:shadow-md ${this.currentPatient?.id === patient.id ? 'bg-blue-50 border-blue-300 shadow-sm' : 'bg-white border-gray-200 hover:border-blue-300'}`; | |
| card.onclick = () => this.selectPatient(patient); | |
| const statusColor = patient.status === 'critical' ? 'bg-red-100 text-red-700' : patient.status === 'review' ? 'bg-amber-100 text-amber-700' : 'bg-green-100 text-green-700'; | |
| card.innerHTML = ` | |
| <div class="flex items-start gap-3"> | |
| <div class="w-10 h-10 rounded-full bg-gradient-to-br from-blue-400 to-blue-600 flex items-center justify-center text-white font-bold text-sm shrink-0"> | |
| ${patient.avatar} | |
| </div> | |
| <div class="flex-1 min-w-0"> | |
| <div class="flex items-center justify-between"> | |
| <h4 class="font-semibold text-gray-900 text-sm truncate">${patient.name}</h4> | |
| <span class="px-2 py-0.5 text-xs font-medium rounded-full ${statusColor}">${patient.status}</span> | |
| </div> | |
| <p class="text-xs text-gray-500 mt-0.5">${patient.age}y • ${patient.gender}</p> | |
| <div class="flex flex-wrap gap-1 mt-2"> | |
| ${patient.conditions.slice(0, 2).map(c => `<span class="px-1.5 py-0.5 bg-gray-100 text-gray-600 text-xs rounded">${c}</span>`).join('')} | |
| ${patient.conditions.length > 2 ? `<span class="px-1.5 py-0.5 bg-gray-100 text-gray-600 text-xs rounded">+${patient.conditions.length - 2}</span>` : ''} | |
| </div> | |
| <div class="flex items-center gap-3 mt-2 text-xs text-gray-500"> | |
| <span class="flex items-center gap-1"> | |
| <i data-lucide="activity" class="w-3 h-3"></i> | |
| ${patient.adherence}% adherence | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| container.appendChild(card); | |
| }); | |
| document.getElementById('patientCount').textContent = patients.length; | |
| lucide.createIcons(); | |
| } | |
| selectPatient(patient) { | |
| this.currentPatient = patient; | |
| this.renderPatientList(); | |
| this.updatePatientHeader(); | |
| this.renderTabContent(); | |
| this.renderRecommendations(); | |
| this.showToast(`Loaded care plan for ${patient.name}`, 'success'); | |
| } | |
| updatePatientHeader() { | |
| const p = this.currentPatient; | |
| document.getElementById('patientAvatar').textContent = p.avatar; | |
| document.getElementById('patientName').textContent = p.name; | |
| document.getElementById('patientMeta').textContent = `${p.age} years old • ${p.gender} • MRN: 00${p.id}2345`; | |
| document.getElementById('lastVisit').textContent = p.lastVisit; | |
| document.getElementById('nextFollowup').textContent = p.nextFollowup; | |
| document.getElementById('adherenceScore').textContent = p.adherence + '%'; | |
| const riskColors = { 'Low': 'text-green-600', 'Moderate': 'text-yellow-600', 'High': 'text-red-600' }; | |
| const riskEl = document.getElementById('riskLevel'); | |
| riskEl.textContent = p.risk; | |
| riskEl.className = `text-sm font-semibold ${riskColors[p.risk] || 'text-gray-600'}`; | |
| const tagsContainer = document.getElementById('patientTags'); | |
| tagsContainer.innerHTML = p.conditions.map(c => | |
| `<span class="px-2 py-1 bg-blue-50 text-blue-700 text-xs font-medium rounded-md border border-blue-200">${c}</span>` | |
| ).join(''); | |
| } | |
| setTab(tab) { | |
| this.currentTab = tab; | |
| document.querySelectorAll('.tab-btn').forEach(btn => { | |
| if(btn.dataset.tab === tab) { | |
| btn.classList.add('border-blue-600', 'text-blue-600', 'active'); | |
| btn.classList.remove('border-transparent', 'text-gray-600'); | |
| } else { | |
| btn.classList.remove('border-blue-600', 'text-blue-600', 'active'); | |
| btn.classList.add('border-transparent', 'text-gray-600'); | |
| } | |
| }); | |
| this.renderTabContent(); | |
| } | |
| renderTabContent() { | |
| const container = document.getElementById('tabContent'); | |
| container.innerHTML = ''; | |
| if(this.currentTab === 'overview') this.renderOverview(container); | |
| else if(this.currentTab === 'timeline') this.renderTimeline(container); | |
| else if(this.currentTab === 'history') this.renderHistory(container); | |
| else if(this.currentTab === 'labs') this.renderLabs(container); | |
| lucide.createIcons(); | |
| } | |
| renderOverview(container) { | |
| const plan = this.currentPatient.carePlan; | |
| const sections = [ | |
| { id: 'conditions', title: 'Active Conditions', icon: 'activity', content: this.renderConditions(plan.activeConditions) }, | |
| { id: 'medications', title: 'Medication Plan', icon: 'pill', content: this.renderMedications(plan.medications) }, | |
| { id: 'goals', title: 'Treatment Goals', icon: 'target', content: this.renderGoals(plan.goals) }, | |
| { id: 'lifestyle', title: 'Lifestyle Recommendations', icon: 'heart', content: this.renderLifestyle(plan.lifestyle) } | |
| ]; | |
| sections.forEach(section => { | |
| const card = document.createElement('div'); | |
| card.className = 'bg-white rounded-xl border border-gray-200 overflow-hidden'; | |
| card.innerHTML = ` | |
| <div class="px-4 py-3 border-b border-gray-100 bg-gray-50 flex items-center justify-between"> | |
| <div class="flex items-center gap-2"> | |
| <i data-lucide="${section.icon}" class="w-4 h-4 text-blue-600"></i> | |
| <h3 class="font-semibold text-gray-900">${section.title}</h3> | |
| </div> | |
| <button onclick="this.closest('.bg-white').querySelector('.section-content').classList.toggle('hidden')" class="p-1 hover:bg-gray-200 rounded"> | |
| <i data-lucide="chevron-down" class="w-4 h-4 text-gray-500"></i> | |
| </button> | |
| </div> | |
| <div class="section-content p-4"> | |
| ${section.content} | |
| </div> | |
| `; | |
| container.appendChild(card); | |
| }); | |
| } | |
| renderConditions(conditions) { | |
| return `<div class="space-y-3"> | |
| ${conditions.map(c => ` | |
| <div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg border border-gray-100 group hover:border-blue-200 transition-colors"> | |
| <div> | |
| <h4 class="font-medium text-gray-900">${c.condition}</h4> | |
| <p class="text-xs text-gray-500 mt-0.5">Severity: ${c.severity} • Status: ${c.status}</p> | |
| </div> | |
| <div class="flex items-center gap-2 opacity-0 group-hover:opacity-100 transition-opacity"> | |
| <button class="p-1.5 text-blue-600 hover:bg-blue-50 rounded" title="Edit"> | |
| <i data-lucide="edit-2" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div>`; | |
| } | |
| renderMedications(medications) { | |
| return `<div class="space-y-3"> | |
| ${medications.map(m => ` | |
| <div class="p-3 border border-gray-200 rounded-lg hover:border-blue-300 transition-colors"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h4 class="font-semibold text-gray-900">${m.name}</h4> | |
| <span class="px-2 py-0.5 bg-green-50 text-green-700 text-xs font-medium rounded">${m.adherence}% adherence</span> | |
| </div> | |
| <div class="grid grid-cols-2 gap-2 text-sm text-gray-600 mb-2"> | |
| <span contenteditable="true" class="editable-field cursor-text border-b border-transparent hover:border-gray-300">${m.dosage}</span> | |
| <span contenteditable="true" class="editable-field cursor-text border-b border-transparent hover:border-gray-300">${m.frequency}</span> | |
| </div> | |
| <div class="flex gap-2 mt-3"> | |
| <button class="px-3 py-1.5 bg-blue-50 text-blue-700 text-xs font-medium rounded hover:bg-blue-100 transition-colors">Adjust Dose</button> | |
| <button class="px-3 py-1.5 bg-gray-50 text-gray-600 text-xs font-medium rounded hover:bg-gray-100 transition-colors">Refill</button> | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div>`; | |
| } | |
| renderGoals(goals) { | |
| return `<div class="space-y-4"> | |
| ${goals.map(g => ` | |
| <div class="p-3 bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg border border-blue-100"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <h4 class="font-medium text-gray-900" contenteditable="true" class="editable-field">${g.text}</h4> | |
| <span class="text-xs text-gray-500 bg-white px-2 py-1 rounded border border-gray-200">${g.target}</span> | |
| </div> | |
| <div class="flex items-center gap-3"> | |
| <div class="flex-1 bg-white rounded-full h-2 border border-gray-200"> | |
| <div class="bg-gradient-to-r from-blue-500 to-indigo-600 h-2 rounded-full transition-all" style="width: ${g.progress}%"></div> | |
| </div> | |
| <span class="text-xs font-semibold text-blue-600 min-w-fit">${g.progress}%</span> | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div>`; | |
| } | |
| renderLifestyle(items) { | |
| return `<div class="space-y-2"> | |
| ${items.map((item, idx) => ` | |
| <div class="flex items-start gap-3 p-3 bg-green-50 rounded-lg border border-green-100"> | |
| <i data-lucide="check-circle-2" class="w-5 h-5 text-green-600 shrink-0 mt-0.5"></i> | |
| <div class="flex-1"> | |
| <p contenteditable="true" class="editable-field text-sm text-gray-800 leading-relaxed">${item}</p> | |
| <div class="flex gap-2 mt-2"> | |
| <button class="text-xs text-green-700 hover:underline font-medium">Mark Complete</button> | |
| <button class="text-xs text-gray-500 hover:underline">Modify</button> | |
| </div> | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div>`; | |
| } | |
| renderTimeline(container) { | |
| const timelineHtml = ` | |
| <div class="relative pl-8 space-y-6"> | |
| <div class="timeline-line"></div> | |
| ${this.timeline.map((event, idx) => ` | |
| <div class="relative slide-in" style="animation-delay: ${idx * 0.1}s"> | |
| <div class="timeline-dot" style="top: 6px;"></div> | |
| <div class="bg-white p-4 rounded-lg border border-gray-200 shadow-sm ml-4 hover:shadow-md transition-shadow"> | |
| <div class="flex items-start justify-between mb-2"> | |
| <div class="flex items-center gap-2"> | |
| <div class="p-1.5 ${event.highlight ? 'bg-blue-50 text-blue-600' : 'bg-gray-50 text-gray-600'} rounded-md"> | |
| <i data-lucide="${event.icon}" class="w-4 h-4"></i> | |
| </div> | |
| <div> | |
| <h4 class="font-semibold text-gray-900 text-sm">${event.title}</h4> | |
| <p class="text-xs text-gray-500">${event.date}</p> | |
| </div> | |
| </div> | |
| ${event.highlight ? '<span class="px-2 py-0.5 bg-blue-100 text-blue-700 text-xs font-medium rounded">Key Event</span>' : ''} | |
| </div> | |
| <p class="text-sm text-gray-600 pl-9">${event.description}</p> | |
| <div class="flex gap-2 mt-3 pl-9"> | |
| <button class="text-xs text-blue-600 hover:text-blue-700 font-medium">View Details</button> | |
| <button class="text-xs text-gray-400 hover:text-gray-600">• Add Note</button> | |
| </div> | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| <button onclick="app.addTimelineEvent()" class="w-full mt-6 py-3 border-2 border-dashed border-gray-300 rounded-lg text-gray-500 hover:border-blue-400 hover:text-blue-600 transition-colors text-sm font-medium flex items-center justify-center gap-2"> | |
| <i data-lucide="plus" class="w-4 h-4"></i> | |
| Add Timeline Event | |
| </button> | |
| `; | |
| container.innerHTML = timelineHtml; | |
| } | |
| renderHistory(container) { | |
| container.innerHTML = ` | |
| <div class="space-y-4"> | |
| <div class="bg-white p-4 rounded-lg border border-gray-200"> | |
| <h3 class="font-semibold text-gray-900 mb-3">Care Plan Revisions</h3> | |
| <div class="space-y-3"> | |
| <div class="flex items-start gap-3 pb-3 border-b border-gray-100"> | |
| <div class="w-8 h-8 bg-blue-50 rounded-full flex items-center justify-center shrink-0"> | |
| <i data-lucide="git-commit" class="w-4 h-4 text-blue-600"></i> | |
| </div> | |
| <div> | |
| <p class="text-sm font-medium text-gray-900">Medication dosage updated</p> | |
| <p class="text-xs text-gray-500 mt-1">By Dr. Smith • Jan 15, 2024 • 10:30 AM</p> | |
| <div class="mt-2 p-2 bg-gray-50 rounded text-xs text-gray-600 font-mono"> | |
| Metformin: 500mg → 1000mg | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex items-start gap-3"> | |
| <div class="w-8 h-8 bg-green-50 rounded-full flex items-center justify-center shrink-0"> | |
| <i data-lucide="check" class="w-4 h-4 text-green-600"></i> | |
| </div> | |
| <div> | |
| <p class="text-sm font-medium text-gray-900">AI Recommendation Approved</p> | |
| <p class="text-xs text-gray-500 mt-1">By Dr. Smith • Jan 14, 2024 • 3:45 PM</p> | |
| <p class="text-xs text-gray-600 mt-1">Added CGM monitoring for 14 days</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| renderLabs(container) { | |
| const labs = this.currentPatient.labs || []; | |
| container.innerHTML = ` | |
| <div class="grid gap-4"> | |
| ${labs.map(lab => ` | |
| <div class="bg-white p-4 rounded-lg border border-gray-200 flex items-center justify-between"> | |
| <div class="flex items-center gap-3"> | |
| <div class="p-2 bg-purple-50 rounded-lg"> | |
| <i data-lucide="flask-conical" class="w-5 h-5 text-purple-600"></i> | |
| </div> | |
| <div> | |
| <h4 class="font-semibold text-gray-900">${lab.type}</h4> | |
| <p class="text-xs text-gray-500">${lab.date}</p> | |
| </div> | |
| </div> | |
| <div class="text-right"> | |
| <p class="text-2xl font-bold text-gray-900">${lab.value} <span class="text-sm font-normal text-gray-500">${lab.unit}</span></p> | |
| <p class="text-xs ${lab.trend === 'up' ? 'text-red-500' : lab.trend === 'down' ? 'text-green-500' : 'text-gray-500'} flex items-center justify-end gap-1"> | |
| <i data-lucide="${lab.trend === 'up' ? 'trending-up' : lab.trend === 'down' ? 'trending-down' : 'minus'}" class="w-3 h-3"></i> | |
| ${lab.trend === 'up' ? 'Increased' : lab.trend === 'down' ? 'Decreased' : 'Stable'} | |
| </p> | |
| </div> | |
| </div> | |
| `).join('')} | |
| <button onclick="app.requestLab()" class="w-full py-3 border-2 border-dashed border-gray-300 rounded-lg text-gray-500 hover:border-purple-400 hover:text-purple-600 transition-colors text-sm font-medium flex items-center justify-center gap-2"> | |
| <i data-lucide="plus" class="w-4 h-4"></i> | |
| Request New Lab | |
| </button> | |
| </div> | |
| `; | |
| } | |
| renderRecommendations() { | |
| const container = document.getElementById('recommendationList'); | |
| const recs = this.aiRecommendations.filter(r => r.patientId === this.currentPatient?.id); | |
| document.getElementById('pendingCount').textContent = recs.length; | |
| if(recs.length === 0) { | |
| container.innerHTML = ` | |
| <div class="p-4 text-center text-gray-500 bg-gray-50 rounded-lg border border-gray-200"> | |
| <i data-lucide="check-circle" class="w-8 h-8 mx-auto mb-2 text-green-500"></i> | |
| <p class="text-sm font-medium">All recommendations reviewed</p> | |
| <p class="text-xs mt-1">No pending AI suggestions</p> | |
| </div> | |
| `; | |
| lucide.createIcons(); | |
| return; | |
| } | |
| container.innerHTML = recs.map(rec => ` | |
| <div class="bg-white rounded-lg border border-blue-200 overflow-hidden shadow-sm hover:shadow-md transition-all slide-in" id="rec-${rec.id}"> | |
| <div class="p-3 border-b border-blue-50 bg-gradient-to-r from-blue-50 to-indigo-50 flex items-center justify-between"> | |
| <div class="flex items-center gap-2"> | |
| <i data-lucide="${rec.type === 'medication' ? 'pill' : rec.type === 'followup' ? 'calendar-clock' : 'activity'}" class="w-4 h-4 text-blue-600"></i> | |
| <span class="text-xs font-semibold text-blue-800 uppercase tracking-wide">${rec.type}</span> | |
| </div> | |
| <div class="flex items-center gap-1"> | |
| <div class="w-6 h-6 rounded-full bg-white border-2 border-blue-500 flex items-center justify-center text-xs font-bold text-blue-600"> | |
| ${rec.confidence} | |
| </div> | |
| </div> | |
| </div> | |
| <div class="p-3"> | |
| <h4 class="font-semibold text-gray-900 text-sm mb-1">${rec.title}</h4> | |
| <p class="text-xs text-gray-600 leading-relaxed mb-3">${rec.description}</p> | |
| <div class="bg-gray-50 p-2 rounded border border-gray-200 mb-3"> | |
| <p class="text-xs text-gray-500 mb-1 font-medium">AI Rationale:</p> | |
| <p class="text-xs text-gray-700 italic">${rec.rationale}</p> | |
| </div> | |
| <div class="flex gap-2"> | |
| <button onclick="app.approveRecommendation(${rec.id})" class="flex-1 py-1.5 bg-blue-600 text-white text-xs font-medium rounded hover:bg-blue-700 transition-colors flex items-center justify-center gap-1"> | |
| <i data-lucide="check" class="w-3 h-3"></i> | |
| Approve | |
| </button> | |
| <button onclick="app.rejectRecommendation(${rec.id})" class="flex-1 py-1.5 bg-white text-gray-700 border border-gray-300 text-xs font-medium rounded hover:bg-gray-50 transition-colors flex items-center justify-center gap-1"> | |
| <i data-lucide="x" class="w-3 h-3"></i> | |
| Reject | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| // Update confidence based on pending recommendations | |
| const avgConfidence = recs.reduce((acc, r) => acc + r.confidence, 0) / recs.length; | |
| this.updateConfidence(avgConfidence); | |
| lucide.createIcons(); | |
| } | |
| approveRecommendation(id) { | |
| const rec = this.aiRecommendations.find(r => r.id === id); | |
| if(!rec) return; | |
| // Add to history | |
| this.approvalHistory.unshift({ | |
| ...rec, | |
| status: 'approved', | |
| timestamp: new Date().toLocaleTimeString() | |
| }); | |
| // Remove from pending | |
| this.aiRecommendations = this.aiRecommendations.filter(r => r.id !== id); | |
| // Update UI | |
| const card = document.getElementById(`rec-${id}`); | |
| if(card) { | |
| card.style.transform = 'translateX(100%)'; | |
| card.style.opacity = '0'; | |
| setTimeout(() => this.renderRecommendations(), 300); | |
| } | |
| this.showToast('Recommendation approved and applied to care plan', 'success'); | |
| this.updateApprovalHistory(); | |
| } | |
| rejectRecommendation(id) { | |
| const rec = this.aiRecommendations.find(r => r.id === id); | |
| if(!rec) return; | |
| this.approvalHistory.unshift({ | |
| ...rec, | |
| status: 'rejected', | |
| timestamp: new Date().toLocaleTimeString() | |
| }); | |
| this.aiRecommendations = this.aiRecommendations.filter(r => r.id !== id); | |
| const card = document.getElementById(`rec-${id}`); | |
| if(card) { | |
| card.style.transform = 'translateX(100%)'; | |
| card.style.opacity = '0'; | |
| setTimeout(() => this.renderRecommendations(), 300); | |
| } | |
| this.showToast('Recommendation rejected', 'info'); | |
| this.updateApprovalHistory(); | |
| } | |
| updateApprovalHistory() { | |
| const container = document.getElementById('approvalHistory'); | |
| container.innerHTML = this.approvalHistory.slice(0, 3).map(h => ` | |
| <div class="p-2 bg-gray-50 rounded border border-gray-200 text-xs"> | |
| <div class="flex items-center justify-between mb-1"> | |
| <span class="font-medium text-gray-900">${h.title}</span> | |
| <span class="px-1.5 py-0.5 rounded ${h.status === 'approved' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'} text-xs font-bold uppercase">${h.status}</span> | |
| </div> | |
| <p class="text-gray-500">${h.timestamp}</p> | |
| </div> | |
| `).join(''); | |
| } | |
| updateConfidence(score) { | |
| document.getElementById('confidenceValue').textContent = Math.round(score) + '%'; | |
| document.getElementById('confidenceBar').style.width = score + '%'; | |
| document.getElementById('lastUpdated').textContent = 'Just now'; | |
| } | |
| regenerateAI() { | |
| const btn = document.getElementById('regenerateBtn'); | |
| const originalContent = btn.innerHTML; | |
| btn.innerHTML = '<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i><span>Analyzing...</span>'; | |
| btn.disabled = true; | |
| lucide.createIcons(); | |
| // Simulate AI processing | |
| setTimeout(() => { | |
| // Add new random recommendation | |
| const newRec = { | |
| id: Date.now(), | |
| patientId: this.currentPatient.id, | |
| type: 'monitoring', | |
| title: 'New Symptom Pattern Detected', | |
| description: 'AI detected irregular glucose patterns during sleep hours.', | |
| rationale: 'Continuous monitoring data analysis reveals dawn phenomenon requiring adjustment.', | |
| confidence: Math.floor(Math.random() * 15) + 85, | |
| action: 'Review data', | |
| timestamp: 'Just now' | |
| }; | |
| this.aiRecommendations.push(newRec); | |
| this.renderRecommendations(); | |
| btn.innerHTML = originalContent; | |
| btn.disabled = false; | |
| lucide.createIcons(); | |
| this.showToast('AI analysis complete. New recommendations available.', 'success'); | |
| }, 2000); | |
| } | |
| saveCarePlan() { | |
| this.showToast('Care plan saved successfully', 'success'); | |
| } | |
| addTimelineEvent() { | |
| const today = new Date().toISOString().split('T')[0]; | |
| this.timeline.unshift({ | |
| date: today, | |
| type: 'note', | |
| title: 'New Clinical Note', | |
| description: 'Added via care plan engine', | |
| icon: 'file-text' | |
| }); | |
| this.renderTabContent(); | |
| this.showToast('Timeline event added', 'success'); | |
| } | |
| requestLab() { | |
| this.showToast('Lab order submitted to LIS', 'info'); | |
| } | |
| openAIChat() { | |
| this.showToast('AI Assistant coming soon', 'info'); | |
| } | |
| showToast(message, type = 'info') { | |
| const container = document.getElementById('toastContainer'); | |
| const toast = document.createElement('div'); | |
| const colors = { | |
| success: 'bg-green-500', | |
| error: 'bg-red-500', | |
| info: 'bg-blue-500', | |
| warning: 'bg-amber-500' | |
| }; | |
| const icons = { | |
| success: 'check-circle', | |
| error: 'x-circle', | |
| info: 'info', | |
| warning: 'alert-triangle' | |
| }; | |
| toast.className = `${colors[type]} text-white px-4 py-3 rounded-lg shadow-lg flex items-center gap-3 min-w-[300px] toast-enter`; | |
| toast.innerHTML = ` | |
| <i data-lucide="${icons[type]}" class="w-5 h-5"></i> | |
| <div class="flex-1"> | |
| <p class="text-sm font-medium">${message}</p> | |
| </div> | |
| <button onclick="this.parentElement.remove()" class="hover:opacity-75"> | |
| <i data-lucide="x" class="w-4 h-4"></i> | |
| </button> | |
| `; | |
| container.appendChild(toast); | |
| lucide.createIcons(); | |
| setTimeout(() => toast.remove(), 5000); | |
| } | |
| } | |
| // Initialize app | |
| const app = new HealeoApp(); |