Spaces:
Running
Running
| // Verify MC - Add Encounter Application | |
| // Enterprise-grade medical marijuana SaaS workflow | |
| class VerifyMCApp { | |
| constructor() { | |
| this.currentStep = 1; | |
| this.totalSteps = 5; | |
| this.patients = []; | |
| this.selectedPatient = null; | |
| this.encounterData = { | |
| conditions: [], | |
| therapies: [], | |
| productForms: [], | |
| compliance: {}, | |
| details: {} | |
| }; | |
| this.transcript = []; | |
| this.isRecording = false; | |
| this.isPaused = false; | |
| this.recordingTime = 0; | |
| this.recordingInterval = null; | |
| this.currentFilter = 'all'; | |
| this.copilotOpen = false; | |
| this.init(); | |
| } | |
| init() { | |
| this.generateMockPatients(); | |
| this.renderStepper(); | |
| this.renderPatients(); | |
| this.initWaveform(); | |
| this.updateProgress(); | |
| this.startAutoSave(); | |
| // Keyboard shortcuts | |
| document.addEventListener('keydown', (e) => { | |
| if (e.metaKey && e.key === 'k') { | |
| e.preventDefault(); | |
| this.toggleCopilot(); | |
| } | |
| if (e.key === 'Escape') { | |
| this.closeWalkInModal(); | |
| this.closePreview(); | |
| } | |
| }); | |
| } | |
| // Data Generation | |
| generateMockPatients() { | |
| const conditions = ['Chronic Pain', 'PTSD', 'Neuropathy', 'MS Spasticity', 'Cancer-related Nausea']; | |
| const statuses = ['checked-in', 'in-room', 'waiting', 'renewal', 'high-risk']; | |
| this.patients = [ | |
| { | |
| id: 1, | |
| firstName: 'James', | |
| lastName: 'Wilson', | |
| dob: '1985-03-15', | |
| mrn: 'MRN-2024-001', | |
| registryId: 'CA-MMJ-789456', | |
| status: 'checked-in', | |
| lastVisit: '2024-01-15', | |
| nextRenewal: '2025-01-15', | |
| conditions: ['Chronic Pain', 'PTSD'], | |
| avatar: 'JW', | |
| aiSuggested: true, | |
| reason: 'Follow-up appointment scheduled' | |
| }, | |
| { | |
| id: 2, | |
| firstName: 'Maria', | |
| lastName: 'Garcia', | |
| dob: '1978-11-22', | |
| mrn: 'MRN-2024-002', | |
| registryId: 'CA-MMJ-123789', | |
| status: 'in-room', | |
| lastVisit: '2024-11-20', | |
| nextRenewal: '2024-12-20', | |
| conditions: ['Neuropathy'], | |
| avatar: 'MG', | |
| aiSuggested: true, | |
| reason: 'Renewal due in 30 days' | |
| }, | |
| { | |
| id: 3, | |
| firstName: 'Robert', | |
| lastName: 'Chen', | |
| dob: '1990-07-08', | |
| mrn: 'MRN-2024-003', | |
| registryId: null, | |
| status: 'waiting', | |
| lastVisit: null, | |
| nextRenewal: null, | |
| conditions: [], | |
| avatar: 'RC', | |
| aiSuggested: false, | |
| reason: 'New patient - initial certification' | |
| }, | |
| { | |
| id: 4, | |
| firstName: 'Sarah', | |
| lastName: 'Johnson', | |
| dob: '1982-04-30', | |
| mrn: 'MRN-2024-004', | |
| registryId: 'CA-MMJ-456123', | |
| status: 'renewal', | |
| lastVisit: '2024-10-01', | |
| nextRenewal: '2024-12-01', | |
| conditions: ['MS Spasticity', 'Chronic Pain'], | |
| avatar: 'SJ', | |
| aiSuggested: true, | |
| reason: 'Renewal overdue' | |
| }, | |
| { | |
| id: 5, | |
| firstName: 'David', | |
| lastName: 'Smith', | |
| dob: '1975-09-12', | |
| mrn: 'MRN-2024-005', | |
| registryId: 'CA-MMJ-789123', | |
| status: 'high-risk', | |
| lastVisit: '2024-11-10', | |
| nextRenewal: '2025-02-10', | |
| conditions: ['PTSD'], | |
| avatar: 'DS', | |
| aiSuggested: false, | |
| reason: 'Multiple medications - review required' | |
| }, | |
| { | |
| id: 6, | |
| firstName: 'Emily', | |
| lastName: 'Davis', | |
| dob: '1988-12-05', | |
| mrn: 'MRN-2024-006', | |
| registryId: 'CA-MMJ-321654', | |
| status: 'checked-in', | |
| lastVisit: '2024-09-20', | |
| nextRenewal: '2025-09-20', | |
| conditions: ['Cancer-related Nausea'], | |
| avatar: 'ED', | |
| aiSuggested: false, | |
| reason: 'Regular follow-up' | |
| } | |
| ]; | |
| } | |
| // Stepper Navigation | |
| renderStepper() { | |
| const steps = [ | |
| { num: 1, label: 'Patient' }, | |
| { num: 2, label: 'Details' }, | |
| { num: 3, label: 'Scribe' }, | |
| { num: 4, label: 'Notes' }, | |
| { num: 5, label: 'Review' } | |
| ]; | |
| const container = document.getElementById('stepper-container'); | |
| if (!container) return; | |
| container.innerHTML = steps.map((step, index) => { | |
| const isActive = step.num === this.currentStep; | |
| const isCompleted = step.num < this.currentStep; | |
| const isLast = index === steps.length - 1; | |
| let classes = 'w-10 h-10 rounded-full flex items-center justify-center text-sm font-bold transition-all '; | |
| if (isActive) { | |
| classes += 'bg-emerald-600 text-white shadow-lg ring-4 ring-emerald-100'; | |
| } else if (isCompleted) { | |
| classes += 'bg-emerald-100 text-emerald-700'; | |
| } else { | |
| classes += 'bg-gray-100 text-gray-500'; | |
| } | |
| return ` | |
| <div class="flex items-center"> | |
| <button onclick="app.goToStep(${step.num})" class="${classes}"> | |
| ${isCompleted ? '<i data-lucide="check" class="w-5 h-5"></i>' : step.num} | |
| </button> | |
| <span class="ml-2 text-xs font-medium ${isActive ? 'text-emerald-700' : 'text-gray-500'} hidden lg:block">${step.label}</span> | |
| ${!isLast ? '<div class="w-8 h-0.5 bg-gray-200 mx-2"></div>' : ''} | |
| </div> | |
| `; | |
| }).join(''); | |
| // Update footer | |
| const stepNames = ['Patient Selection', 'Encounter Details', 'AI Scribe', 'AI Documentation', 'Review & Finalize']; | |
| document.getElementById('footer-step-current').textContent = this.currentStep; | |
| document.getElementById('footer-step-total').textContent = this.totalSteps; | |
| document.getElementById('footer-step-name').textContent = stepNames[this.currentStep - 1]; | |
| // Update buttons | |
| document.getElementById('btn-prev').disabled = this.currentStep === 1; | |
| if (this.currentStep === this.totalSteps) { | |
| document.getElementById('btn-next').classList.add('hidden'); | |
| document.getElementById('btn-submit-final').classList.remove('hidden'); | |
| } else { | |
| document.getElementById('btn-next').classList.remove('hidden'); | |
| document.getElementById('btn-submit-final').classList.add('hidden'); | |
| } | |
| lucide.createIcons(); | |
| } | |
| goToStep(stepNum) { | |
| if (stepNum < 1 || stepNum > this.totalSteps) return; | |
| // Validation before moving forward | |
| if (stepNum > this.currentStep) { | |
| if (this.currentStep === 1 && !this.selectedPatient) { | |
| this.showToast('Please select a patient first', 'error'); | |
| return; | |
| } | |
| } | |
| this.currentStep = stepNum; | |
| this.renderStepper(); | |
| // Hide all steps | |
| document.querySelectorAll('.step-content').forEach(el => { | |
| el.classList.remove('active'); | |
| }); | |
| // Show current step | |
| document.getElementById(`step-${this.currentStep}`).classList.add('active'); | |
| // Scroll to top | |
| document.getElementById('main-container').scrollTop = 0; | |
| } | |
| nextStep() { | |
| if (this.currentStep < this.totalSteps) { | |
| this.goToStep(this.currentStep + 1); | |
| } | |
| } | |
| prevStep() { | |
| if (this.currentStep > 1) { | |
| this.goToStep(this.currentStep - 1); | |
| } | |
| } | |
| // Patient Selection | |
| renderPatients() { | |
| const grid = document.getElementById('patient-grid'); | |
| const aiContainer = document.getElementById('ai-suggested-container'); | |
| if (!grid) return; | |
| // Filter patients | |
| let filtered = this.patients; | |
| if (this.currentFilter !== 'all') { | |
| filtered = this.patients.filter(p => p.status === this.currentFilter); | |
| } | |
| // Render AI suggested | |
| const aiSuggested = this.patients.filter(p => p.aiSuggested); | |
| aiContainer.innerHTML = aiSuggested.map(p => this.createPatientCard(p, true)).join(''); | |
| // Render main grid | |
| grid.innerHTML = filtered.map(p => this.createPatientCard(p, false)).join(''); | |
| lucide.createIcons(); | |
| } | |
| createPatientCard(patient, isCompact) { | |
| const isSelected = this.selectedPatient?.id === patient.id; | |
| const statusColors = { | |
| 'checked-in': 'bg-blue-100 text-blue-700', | |
| 'in-room': 'bg-emerald-100 text-emerald-700', | |
| 'waiting': 'bg-amber-100 text-amber-700', | |
| 'renewal': 'bg-purple-100 text-purple-700', | |
| 'high-risk': 'bg-red-100 text-red-700' | |
| }; | |
| if (isCompact) { | |
| return ` | |
| <div onclick="app.selectPatient(${patient.id})" class="patient-card cursor-pointer border-2 ${isSelected ? 'border-emerald-500 bg-emerald-50' : 'border-gray-200 bg-white'} rounded-xl p-4 hover:border-emerald-300 hover:shadow-md transition-all"> | |
| <div class="flex items-center gap-3 mb-2"> | |
| <div class="w-10 h-10 rounded-full bg-emerald-600 text-white flex items-center justify-center font-bold text-sm">${patient.avatar}</div> | |
| <div class="flex-1 min-w-0"> | |
| <div class="font-semibold text-gray-900 truncate">${patient.firstName} ${patient.lastName}</div> | |
| <div class="text-xs text-gray-500">MRN: ${patient.mrn}</div> | |
| </div> | |
| </div> | |
| <div class="flex items-center justify-between"> | |
| <span class="text-xs px-2 py-0.5 rounded-full ${statusColors[patient.status]}">${patient.status.replace('-', ' ')}</span> | |
| <span class="text-xs text-gray-400">${patient.reason}</span> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| return ` | |
| <div onclick="app.selectPatient(${patient.id})" class="patient-card cursor-pointer border-2 ${isSelected ? 'border-emerald-500 bg-emerald-50' : 'border-gray-200 bg-white'} rounded-xl p-5 hover:border-emerald-300 hover:shadow-md transition-all"> | |
| <div class="flex items-start justify-between mb-3"> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-12 h-12 rounded-full bg-emerald-600 text-white flex items-center justify-center font-bold">${patient.avatar}</div> | |
| <div> | |
| <div class="font-semibold text-gray-900">${patient.firstName} ${patient.lastName}</div> | |
| <div class="text-sm text-gray-500">DOB: ${patient.dob}</div> | |
| </div> | |
| </div> | |
| <span class="text-xs px-2 py-1 rounded-full ${statusColors[patient.status]}">${patient.status.replace('-', ' ')}</span> | |
| </div> | |
| <div class="space-y-2 mb-3"> | |
| <div class="text-xs text-gray-500">MRN: ${patient.mrn}</div> | |
| <div class="text-xs text-gray-500">Registry: ${patient.registryId || 'Pending'}</div> | |
| ${patient.conditions.length ? ` | |
| <div class="flex flex-wrap gap-1 mt-2"> | |
| ${patient.conditions.map(c => `<span class="text-xs px-2 py-0.5 bg-gray-100 text-gray-700 rounded">${c}</span>`).join('')} | |
| </div> | |
| ` : ''} | |
| </div> | |
| <div class="flex items-center justify-between text-xs text-gray-400 border-t pt-3"> | |
| <span>Last visit: ${patient.lastVisit || 'New patient'}</span> | |
| <span>Renewal: ${patient.nextRenewal || 'N/A'}</span> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| selectPatient(patientId) { | |
| this.selectedPatient = this.patients.find(p => p.id === patientId); | |
| this.renderPatients(); | |
| this.updateProgress(); | |
| this.showToast(`Selected ${this.selectedPatient.firstName} ${this.selectedPatient.lastName}`, 'success'); | |
| } | |
| filterPatients(query) { | |
| const grid = document.getElementById('patient-grid'); | |
| const filtered = this.patients.filter(p => | |
| p.firstName.toLowerCase().includes(query.toLowerCase()) || | |
| p.lastName.toLowerCase().includes(query.toLowerCase()) || | |
| p.mrn.toLowerCase().includes(query.toLowerCase()) | |
| ); | |
| grid.innerHTML = filtered.map(p => this.createPatientCard(p, false)).join(''); | |
| lucide.createIcons(); | |
| } | |
| sortPatients(method) { | |
| // Implementation for sorting | |
| this.renderPatients(); | |
| } | |
| setFilter(filter) { | |
| this.currentFilter = filter; | |
| // Update chip styles | |
| document.querySelectorAll('.filter-chip').forEach(chip => { | |
| if (chip.dataset.filter === filter) { | |
| chip.className = 'filter-chip px-3 py-1.5 rounded-full text-xs font-medium bg-emerald-100 text-emerald-700 border border-emerald-200 transition-all'; | |
| } else { | |
| chip.className = 'filter-chip px-3 py-1.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600 border border-gray-200 hover:bg-gray-200 transition-all'; | |
| } | |
| }); | |
| this.renderPatients(); | |
| } | |
| // Walk-in Modal | |
| openWalkInModal() { | |
| document.getElementById('walkin-modal').classList.remove('hidden'); | |
| } | |
| closeWalkInModal() { | |
| document.getElementById('walkin-modal').classList.add('hidden'); | |
| } | |
| createWalkIn() { | |
| const first = document.getElementById('walkin-first').value; | |
| const last = document.getElementById('walkin-last').value; | |
| const dob = document.getElementById('walkin-dob').value; | |
| const phone = document.getElementById('walkin-phone').value; | |
| if (!first || !last || !dob) { | |
| this.showToast('Please fill in all required fields', 'error'); | |
| return; | |
| } | |
| const newPatient = { | |
| id: Date.now(), | |
| firstName: first, | |
| lastName: last, | |
| dob: dob, | |
| mrn: `MRN-${Date.now()}`, | |
| registryId: null, | |
| status: 'waiting', | |
| lastVisit: null, | |
| nextRenewal: null, | |
| conditions: [], | |
| avatar: `${first[0]}${last[0]}`.toUpperCase(), | |
| aiSuggested: false, | |
| reason: 'Walk-in patient' | |
| }; | |
| this.patients.unshift(newPatient); | |
| this.selectPatient(newPatient.id); | |
| this.closeWalkInModal(); | |
| this.showToast('New walk-in patient created', 'success'); | |
| } | |
| // Encounter Details | |
| updateEncounterDetail(field, value) { | |
| this.encounterData.details[field] = value; | |
| if (field === 'severity') { | |
| document.getElementById('severity-value').textContent = value; | |
| } | |
| this.updateProgress(); | |
| this.updatePreSummary(); | |
| } | |
| addCondition(condition) { | |
| if (!condition || this.encounterData.conditions.includes(condition)) return; | |
| this.encounterData.conditions.push(condition); | |
| this.renderConditionTags(); | |
| this.updatePreSummary(); | |
| document.getElementById('condition-select').value = ''; | |
| } | |
| renderConditionTags() { | |
| const container = document.getElementById('condition-tags'); | |
| container.innerHTML = this.encounterData.conditions.map(c => ` | |
| <span class="px-3 py-1 bg-emerald-100 text-emerald-700 rounded-full text-sm flex items-center gap-1"> | |
| ${c} | |
| <button onclick="app.removeCondition('${c}')" class="hover:text-emerald-900"> | |
| <i data-lucide="x" class="w-3 h-3"></i> | |
| </button> | |
| </span> | |
| `).join(''); | |
| lucide.createIcons(); | |
| } | |
| removeCondition(condition) { | |
| this.encounterData.conditions = this.encounterData.conditions.filter(c => c !== condition); | |
| this.renderConditionTags(); | |
| this.updatePreSummary(); | |
| } | |
| toggleTherapy(therapy) { | |
| if (this.encounterData.therapies.includes(therapy)) { | |
| this.encounterData.therapies = this.encounterData.therapies.filter(t => t !== therapy); | |
| } else { | |
| this.encounterData.therapies.push(therapy); | |
| } | |
| this.updatePreSummary(); | |
| } | |
| toggleProductForm(btn, form) { | |
| btn.classList.toggle('bg-emerald-100'); | |
| btn.classList.toggle('text-emerald-700'); | |
| btn.classList.toggle('border-emerald-500'); | |
| if (this.encounterData.productForms.includes(form)) { | |
| this.encounterData.productForms = this.encounterData.productForms.filter(f => f !== form); | |
| } else { | |
| this.encounterData.productForms.push(form); | |
| } | |
| } | |
| updateCompliance(type, value) { | |
| this.encounterData.compliance[type] = value; | |
| this.updatePreSummary(); | |
| } | |
| updatePreSummary() { | |
| const qualification = document.getElementById('summary-qualification'); | |
| const missing = document.getElementById('summary-missing'); | |
| const complexityBar = document.getElementById('complexity-bar'); | |
| const complexityText = document.getElementById('complexity-text'); | |
| // Update qualification | |
| if (this.encounterData.conditions.length > 0) { | |
| qualification.textContent = 'Likely Eligible - ' + this.encounterData.conditions.join(', '); | |
| qualification.className = 'text-sm font-semibold text-emerald-700'; | |
| } else { | |
| qualification.textContent = 'Select conditions to assess'; | |
| qualification.className = 'text-sm font-semibold text-gray-500'; | |
| } | |
| // Update missing items | |
| const missingItems = []; | |
| if (!this.encounterData.compliance.residency) missingItems.push('Residency verification'); | |
| if (!this.encounterData.compliance.identity) missingItems.push('Identity check'); | |
| if (!this.encounterData.compliance.consent) missingItems.push('Consent forms'); | |
| missing.innerHTML = missingItems.length ? missingItems.map(item => ` | |
| <li class="flex items-center gap-1 text-amber-600"> | |
| <i data-lucide="alert-circle" class="w-3 h-3"></i> | |
| <span>${item}</span> | |
| </li> | |
| `).join('') : '<li class="flex items-center gap-1 text-emerald-600"><i data-lucide="check" class="w-3 h-3"></i><span>All required items complete</span></li>'; | |
| // Update complexity | |
| const complexity = Math.min(100, (this.encounterData.conditions.length * 15) + (this.encounterData.therapies.length * 5) + 10); | |
| complexityBar.style.width = `${complexity}%`; | |
| if (complexity < 30) { | |
| complexityText.textContent = 'Low'; | |
| complexityBar.className = 'h-full bg-emerald-500 rounded-full'; | |
| } else if (complexity < 70) { | |
| complexityText.textContent = 'Moderate'; | |
| complexityBar.className = 'h-full bg-amber-500 rounded-full'; | |
| } else { | |
| complexityText.textContent = 'High'; | |
| complexityBar.className = 'h-full bg-red-500 rounded-full'; | |
| } | |
| lucide.createIcons(); | |
| } | |
| simulateUpload() { | |
| this.showToast('Uploading document...', 'info'); | |
| setTimeout(() => { | |
| this.showToast('Document uploaded successfully', 'success'); | |
| }, 1500); | |
| } | |
| // AI Scribe | |
| initWaveform() { | |
| const container = document.getElementById('waveform-container'); | |
| if (!container) return; | |
| container.innerHTML = Array(40).fill(0).map(() => | |
| `<div class="w-1 bg-emerald-400 rounded-full waveform-bar" style="height: 20%; animation-delay: ${Math.random() * 0.5}s"></div>` | |
| ).join(''); | |
| } | |
| startRecording() { | |
| this.isRecording = true; | |
| this.isPaused = false; | |
| document.getElementById('recording-status').textContent = 'Recording'; | |
| document.getElementById('recording-status').className = 'px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-600 recording-pulse'; | |
| document.getElementById('btn-start').classList.add('hidden'); | |
| document.getElementById('btn-pause').classList.remove('hidden'); | |
| document.getElementById('btn-stop').classList.remove('hidden'); | |
| document.getElementById('waveform-container').classList.remove('opacity-30'); | |
| this.recordingInterval = setInterval(() => { | |
| this.recordingTime++; | |
| const mins = Math.floor(this.recordingTime / 60).toString().padStart(2, '0'); | |
| const secs = (this.recordingTime % 60).toString().padStart(2, '0'); | |
| document.getElementById('recording-timer').textContent = `${mins}:${secs}`; | |
| }, 1000); | |
| // Simulate transcript generation | |
| this.simulateTranscriptGeneration(); | |
| } | |
| pauseRecording() { | |
| this.isPaused = true; | |
| clearInterval(this.recordingInterval); | |
| document.getElementById('recording-status').textContent = 'Paused'; | |
| document.getElementById('recording-status').className = 'px-2 py-1 rounded-full text-xs font-medium bg-amber-100 text-amber-600'; | |
| document.getElementById('btn-pause').classList.add('hidden'); | |
| document.getElementById('btn-resume').classList.remove('hidden'); | |
| document.getElementById('recording-overlay').classList.remove('hidden'); | |
| document.getElementById('waveform-container').classList.add('opacity-30'); | |
| } | |
| resumeRecording() { | |
| this.isPaused = false; | |
| document.getElementById('recording-status').textContent = 'Recording'; | |
| document.getElementById('recording-status').className = 'px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-600 recording-pulse'; | |
| document.getElementById('btn-resume').classList.add('hidden'); | |
| document.getElementById('btn-pause').classList.remove('hidden'); | |
| document.getElementById('recording-overlay').classList.add('hidden'); | |
| document.getElementById('waveform-container').classList.remove('opacity-30'); | |
| this.recordingInterval = setInterval(() => { | |
| this.recordingTime++; | |
| const mins = Math.floor(this.recordingTime / 60).toString().padStart(2, '0'); | |
| const secs = (this.recordingTime % 60).toString().padStart(2, '0'); | |
| document.getElementById('recording-timer').textContent = `${mins}:${secs}`; | |
| }, 1000); | |
| } | |
| stopRecording() { | |
| this.isRecording = false; | |
| clearInterval(this.recordingInterval); | |
| document.getElementById('recording-status').textContent = 'Completed'; | |
| document.getElementById('recording-status').className = 'px-2 py-1 rounded-full text-xs font-medium bg-emerald-100 text-emerald-600'; | |
| document.getElementById('btn-pause').classList.add('hidden'); | |
| document.getElementById('btn-resume').classList.add('hidden'); | |
| document.getElementById('btn-stop').classList.add('hidden'); | |
| document.getElementById('btn-start').classList.remove('hidden'); | |
| document.getElementById('btn-start').innerHTML = '<i data-lucide="mic" class="w-5 h-5"></i> New Recording'; | |
| document.getElementById('waveform-container').classList.add('opacity-30'); | |
| document.getElementById('recording-overlay').classList.add('hidden'); | |
| this.showToast('Recording saved', 'success'); | |
| this.updateProgress(); | |
| } | |
| simulateTranscriptGeneration() { | |
| const lines = [ | |
| { speaker: 'Dr', text: 'Good morning, James. How are you feeling today?' }, | |
| { speaker: 'Pt', text: 'Good morning, Doctor. The back pain is still there, maybe a 7 out of 10.' }, | |
| { speaker: 'Dr', text: 'I see. And how about the sleep issues we discussed last time?' }, | |
| { speaker: 'Pt', text: 'Still having nightmares. Maybe 3 or 4 times a week.' }, | |
| { speaker: 'Dr', text: 'Have you tried the physical therapy exercises?' }, | |
| { speaker: 'Pt', text: 'Yes, but they only help for an hour or so, then the pain returns.' } | |
| ]; | |
| let delay = 0; | |
| lines.forEach((line, index) => { | |
| delay += 2000 + Math.random() * 1000; | |
| setTimeout(() => { | |
| if (this.isRecording && !this.isPaused) { | |
| this.addTranscriptLine(line.speaker, line.text); | |
| } | |
| }, delay); | |
| }); | |
| } | |
| addTranscriptLine(speaker, text) { | |
| const container = document.getElementById('transcript-lines'); | |
| // Remove placeholder if exists | |
| if (container.querySelector('.italic')) { | |
| container.innerHTML = ''; | |
| } | |
| const line = document.createElement('div'); | |
| line.className = 'transcript-line flex gap-3 p-2 rounded-lg hover:bg-gray-50'; | |
| line.innerHTML = ` | |
| <div class="w-16 shrink-0 text-xs font-semibold ${speaker === 'Dr' ? 'text-emerald-600' : 'text-blue-600'}">${speaker}:</div> | |
| <div class="flex-1 text-sm text-gray-700">${text}</div> | |
| <div class="text-xs text-gray-400">${new Date().toLocaleTimeString()}</div> | |
| `; | |
| container.appendChild(line); | |
| container.scrollTop = container.scrollHeight; | |
| // Update word count | |
| const words = container.innerText.split(/\s+/).length; | |
| document.getElementById('word-count').textContent = `${words} words`; | |
| } | |
| simulateConversation() { | |
| this.startRecording(); | |
| this.showToast('Simulating conversation...', 'info'); | |
| } | |
| uploadAudio() { | |
| this.showToast('Audio upload feature would open file picker', 'info'); | |
| } | |
| useSampleVisit() { | |
| document.getElementById('transcript-lines').innerHTML = ''; | |
| const sampleLines = [ | |
| { speaker: 'Dr', text: 'Hello James, I see you\'re here for your initial certification visit.' }, | |
| { speaker: 'Pt', text: 'Yes, I\'ve been dealing with chronic pain from an old injury.' }, | |
| { speaker: 'Dr', text: 'Can you describe the pain for me?' }, | |
| { speaker: 'Pt', text: 'It\'s a sharp burning sensation in my lower back, radiating down my left leg.' }, | |
| { speaker: 'Dr', text: 'Rate the pain on a scale of 1-10.' }, | |
| { speaker: 'Pt', text: 'Usually around a 7, sometimes 8 in the evenings.' }, | |
| { speaker: 'Dr', text: 'What treatments have you tried?' }, | |
| { speaker: 'Pt', text: 'Physical therapy, ibuprofen, gabapentin... nothing really helps long-term.' }, | |
| { speaker: 'Dr', text: 'I see. And you mentioned PTSD in your intake form?' }, | |
| { speaker: 'Pt', text: 'Yes, from my time in the service. The nightmares are the worst part.' } | |
| ]; | |
| sampleLines.forEach((line, index) => { | |
| setTimeout(() => { | |
| this.addTranscriptLine(line.speaker, line.text); | |
| }, index * 500); | |
| }); | |
| this.showToast('Sample visit loaded', 'success'); | |
| } | |
| switchTab(tabName) { | |
| // Hide all tabs | |
| document.querySelectorAll('.tab-panel').forEach(p => p.classList.add('hidden')); | |
| document.getElementById(`tab-${tabName}`).classList.remove('hidden'); | |
| // Update button styles | |
| document.querySelectorAll('.tab-btn').forEach(btn => { | |
| if (btn.dataset.tab === tabName) { | |
| btn.className = 'tab-btn active px-3 py-1.5 text-xs font-medium rounded-md bg-emerald-100 text-emerald-700'; | |
| } else { | |
| btn.className = 'tab-btn px-3 py-1.5 text-xs font-medium rounded-md text-gray-600 hover:bg-gray-100'; | |
| } | |
| }); | |
| } | |
| clearTranscript() { | |
| document.getElementById('transcript-lines').innerHTML = '<div class="text-sm text-gray-400 italic text-center py-8">Recording not started. Click "Start Recording" or "Simulate Conversation" to begin.</div>'; | |
| document.getElementById('word-count').textContent = '0 words'; | |
| } | |
| copyTranscript() { | |
| const text = document.getElementById('transcript-lines').innerText; | |
| navigator.clipboard.writeText(text); | |
| this.showToast('Transcript copied to clipboard', 'success'); | |
| } | |
| expandTranscript() { | |
| this.showToast('Expand view would open full-screen transcript', 'info'); | |
| } | |
| insertCommand(command) { | |
| this.showToast(`Voice command: "${command}"`, 'info'); | |
| if (this.isRecording && !this.isPaused) { | |
| setTimeout(() => { | |
| this.addTranscriptLine('System', `[AI Command: ${command}]`); | |
| }, 500); | |
| } | |
| } | |
| // AI Notes | |
| generateAINotes() { | |
| const btn = document.getElementById('btn-generate-notes'); | |
| btn.innerHTML = '<i data-lucide="loader-2" class="w-5 h-5 animate-spin"></i> Generating...'; | |
| lucide.createIcons(); | |
| setTimeout(() => { | |
| document.getElementById('ai-notes-container').style.opacity = '1'; | |
| document.getElementById('ai-notes-container').style.pointerEvents = 'auto'; | |
| document.getElementById('quality-score-card').classList.remove('hidden'); | |
| btn.innerHTML = '<i data-lucide="refresh-cw" class="w-5 h-5"></i> Regenerate'; | |
| lucide.createIcons(); | |
| this.showToast('AI notes generated successfully', 'success'); | |
| this.updateProgress(); | |
| }, 2000); | |
| } | |
| regenerateSection(section) { | |
| this.showToast(`Regenerating ${section} section...`, 'info'); | |
| setTimeout(() => { | |
| this.showToast(`${section} section updated`, 'success'); | |
| }, 1000); | |
| } | |
| lockSection(section) { | |
| this.showToast(`${section} section locked`, 'success'); | |
| } | |
| selectCode(element) { | |
| document.querySelectorAll('#coding-suggestions > div').forEach(el => { | |
| el.classList.remove('bg-purple-50', 'border-purple-300'); | |
| }); | |
| element.classList.add('bg-purple-50', 'border-purple-300'); | |
| } | |
| showMoreCodes() { | |
| this.showToast('Additional coding suggestions would appear here', 'info'); | |
| } | |
| jumpToSection(section) { | |
| this.showToast(`Navigating to ${section}...`, 'info'); | |
| } | |
| generateCertPDF() { | |
| this.showToast('Generating certification PDF...', 'info'); | |
| setTimeout(() => { | |
| this.showToast('PDF generated and ready for download', 'success'); | |
| }, 1500); | |
| } | |
| generateRegistryDoc() { | |
| this.showToast('Preparing registry documents...', 'info'); | |
| } | |
| sendToRegistry() { | |
| this.showToast('Submitting to state registry...', 'info'); | |
| setTimeout(() => { | |
| this.showToast('Registry submission successful', 'success'); | |
| }, 2000); | |
| } | |
| // Review & Finalize | |
| toggleComplianceCheck(checkbox, type) { | |
| this.showToast(`${type} compliance ${checkbox.checked ? 'verified' : 'pending'}`, 'info'); | |
| } | |
| submitEncounter() { | |
| this.showToast('Submitting encounter...', 'info'); | |
| setTimeout(() => { | |
| this.showToast('Encounter submitted successfully!', 'success'); | |
| setTimeout(() => { | |
| if (confirm('Encounter complete! Return to dashboard?')) { | |
| this.showToast('Returning to dashboard...', 'info'); | |
| } | |
| }, 500); | |
| }, 2000); | |
| } | |
| saveDraft() { | |
| this.showToast('Draft saved successfully', 'success'); | |
| } | |
| scheduleFollowUp() { | |
| this.showToast('Opening scheduling calendar...', 'info'); | |
| } | |
| previewDoc(type) { | |
| const titles = { | |
| soap: 'SOAP Note Preview', | |
| cert: 'Certification Letter Preview', | |
| registry: 'Registry Form Preview', | |
| bill: 'Encounter Bill Preview' | |
| }; | |
| document.getElementById('preview-title').textContent = titles[type]; | |
| document.getElementById('preview-content').innerHTML = ` | |
| <div class="bg-white p-8 rounded-lg shadow-sm border"> | |
| <div class="text-center mb-6"> | |
| <h2 class="text-2xl font-bold text-gray-900">${titles[type]}</h2> | |
| <p class="text-gray-500">Verify MC Medical Marijuana Platform</p> | |
| </div> | |
| <div class="space-y-4 text-sm text-gray-700"> | |
| <div class="border-b pb-2"><strong>Patient:</strong> ${this.selectedPatient?.firstName} ${this.selectedPatient?.lastName}</div> | |
| <div class="border-b pb-2"><strong>Date:</strong> ${new Date().toLocaleDateString()}</div> | |
| <div class="border-b pb-2"><strong>Provider:</strong> Dr. Sarah Chen, MD</div> | |
| <div class="h-32 bg-gray-100 rounded flex items-center justify-center text-gray-400">Document content would appear here</div> | |
| </div> | |
| </div> | |
| `; | |
| document.getElementById('preview-modal').classList.remove('hidden'); | |
| } | |
| closePreview() { | |
| document.getElementById('preview-modal').classList.add('hidden'); | |
| } | |
| downloadPreview() { | |
| this.showToast('Downloading PDF...', 'success'); | |
| } | |
| // AI Copilot | |
| toggleCopilot() { | |
| this.copilotOpen = !this.copilotOpen; | |
| const panel = document.getElementById('copilot-panel'); | |
| if (this.copilotOpen) { | |
| panel.classList.remove('hidden'); | |
| document.getElementById('copilot-badge').classList.add('hidden'); | |
| } else { | |
| panel.classList.add('hidden'); | |
| } | |
| } | |
| sendCopilotMessage() { | |
| const input = document.getElementById('copilot-input'); | |
| const message = input.value.trim(); | |
| if (!message) return; | |
| this.addCopilotMessage('user', message); | |
| input.value = ''; | |
| // Simulate AI response | |
| setTimeout(() => { | |
| const responses = [ | |
| 'Based on the patient profile, I recommend documenting chronic pain with severity rating.', | |
| 'Would you like me to suggest appropriate dosing guidelines for this condition?', | |
| 'I can help generate the assessment section. Should I focus on prior treatment failures?', | |
| 'Reminder: This patient needs registry ID verification before submission.' | |
| ]; | |
| const randomResponse = responses[Math.floor(Math.random() * responses.length)]; | |
| this.addCopilotMessage('ai', randomResponse); | |
| }, 1000); | |
| } | |
| quickAsk(query) { | |
| document.getElementById('copilot-input').value = query; | |
| this.sendCopilotMessage(); | |
| } | |
| addCopilotMessage(sender, text) { | |
| const container = document.getElementById('copilot-messages'); | |
| const messageDiv = document.createElement('div'); | |
| if (sender === 'ai') { | |
| messageDiv.className = 'p-3 bg-blue-50 rounded-lg border border-blue-200'; | |
| messageDiv.innerHTML = ` | |
| <div class="flex items-start gap-2"> | |
| <i data-lucide="sparkles" class="w-4 h-4 text-blue-600 mt-0.5"></i> | |
| <div class="text-sm text-blue-900">${text}</div> | |
| </div> | |
| `; | |
| } else { | |
| messageDiv.className = 'p-3 bg-gray-100 rounded-lg border border-gray-200 ml-8'; | |
| messageDiv.innerHTML = `<div class="text-sm text-gray-900">${text}</div>`; | |
| } | |
| container.appendChild(messageDiv); | |
| container.scrollTop = container.scrollHeight; | |
| lucide.createIcons(); | |
| } | |
| // Progress & Status | |
| updateProgress() { | |
| let completed = 0; | |
| const total = 5; | |
| if (this.selectedPatient) completed++; | |
| if (this.encounterData.conditions.length > 0) completed++; | |
| if (this.recordingTime > 0) completed++; | |
| if (document.getElementById('ai-notes-container')?.style.opacity === '1') completed++; | |
| if (this.currentStep === 5) completed = 5; | |
| const percentage = Math.round((completed / total) * 100); | |
| // Update circle progress | |
| const circle = document.getElementById('progress-circle'); | |
| const radius = 20; | |
| const circumference = radius * 2 * Math.PI; | |
| const offset = circumference - (percentage / 100) * circumference; | |
| if (circle) { | |
| circle.style.strokeDashoffset = offset; | |
| } | |
| document.getElementById('progress-text').textContent = `${percentage}%`; | |
| // Update title and badges | |
| const titles = ['Needs Data', 'Getting Started', 'In Progress', 'Almost Complete', 'Ready to Submit']; | |
| const titleIndex = Math.floor((completed / total) * (titles.length - 1)); | |
| document.getElementById('readiness-title').textContent = titles[titleIndex]; | |
| const missingItems = []; | |
| if (!this.selectedPatient) missingItems.push('Select patient'); | |
| if (this.encounterData.conditions.length === 0) missingItems.push('Add conditions'); | |
| if (this.recordingTime === 0) missingItems.push('Record encounter'); | |
| document.getElementById('missing-items').textContent = missingItems.length > 0 | |
| ? `Missing: ${missingItems.join(', ')}` | |
| : 'All requirements met'; | |
| // Update status badges | |
| const badgesContainer = document.getElementById('status-badges'); | |
| if (percentage === 100) { | |
| badgesContainer.innerHTML = '<span class="px-3 py-1 rounded-full text-xs font-medium bg-emerald-100 text-emerald-700">Ready to Submit</span>'; | |
| } else if (percentage > 50) { | |
| badgesContainer.innerHTML = '<span class="px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-700">In Progress</span>'; | |
| } else { | |
| badgesContainer.innerHTML = '<span class="px-3 py-1 rounded-full text-xs font-medium bg-gray-100 text-gray-600">Draft</span>'; | |
| } | |
| } | |
| // Auto-save | |
| startAutoSave() { | |
| setInterval(() => { | |
| const now = new Date(); | |
| const timeStr = now.toLocaleTimeString(); | |
| const statusEl = document.getElementById('auto-save-status'); | |
| if (statusEl) { | |
| statusEl.textContent = `Auto-saved ${Math.floor(Math.random() * 10) + 1}s ago`; | |
| } | |
| }, 10000); | |
| } | |
| // Footer Actions | |
| quickAction(action) { | |
| switch(action) { | |
| case 'help': | |
| this.toggleCopilot(); | |
| break; | |
| case 'calculator': | |
| this.showToast('Dosage calculator would open', 'info'); | |
| break; | |
| case 'print': | |
| window.print(); | |
| break; | |
| } | |
| } | |
| saveAndExit() { | |
| this.showToast('Saving encounter...', 'info'); | |
| setTimeout(() => { | |
| this.showToast('Saved! You can safely exit.', 'success'); | |
| }, 1000); | |
| } | |
| submitFinal() { | |
| this.submitEncounter(); | |
| } | |
| // Utilities | |
| showToast(message, type = 'info') { | |
| const container = document.getElementById('toast-container'); | |
| const toast = document.createElement('div'); | |
| const colors = { | |
| success: 'bg-emerald-50 border-emerald-200 text-emerald-800', | |
| error: 'bg-red-50 border-red-200 text-red-800', | |
| info: 'bg-blue-50 border-blue-200 text-blue-800', | |
| warning: 'bg-amber-50 border-amber-200 text-amber-800' | |
| }; | |
| const icons = { | |
| success: 'check-circle', | |
| error: 'x-circle', | |
| info: 'info', | |
| warning: 'alert-triangle' | |
| }; | |
| toast.className = `flex items-center gap-2 px-4 py-3 rounded-lg border shadow-lg ${colors[type]} fade-in`; | |
| toast.innerHTML = ` | |
| <i data-lucide="${icons[type]}" class="w-5 h-5"></i> | |
| <span class="text-sm font-medium">${message}</span> | |
| `; | |
| container.appendChild(toast); | |
| lucide.createIcons(); | |
| setTimeout(() => { | |
| toast.style.opacity = '0'; | |
| toast.style.transform = 'translateY(10px)'; | |
| setTimeout(() => toast.remove(), 300); | |
| }, 4000); | |
| } | |
| } | |
| // Initialize app | |
| const app = new VerifyMCApp(); |