Spaces:
Running
Running
| // Initialize Lucide Icons | |
| lucide.createIcons(); | |
| // ========================================= | |
| // AI VOICE ACTIONS WORKFLOW ENGINE | |
| // ========================================= | |
| const VoiceActions = { | |
| isOpen: false, | |
| currentStep: 1, | |
| totalSteps: 5, | |
| providerData: { | |
| name: '', | |
| email: '' | |
| }, | |
| automationTasks: [ | |
| "Creating provider profile", | |
| "Registering in provider directory", | |
| "Preparing account access", | |
| "Assigning provider role & permissions", | |
| "Creating default scheduling profile", | |
| "Applying weekly availability (Mon-Fri, 9-5)", | |
| "Enabling appointment reminders", | |
| "Enabling notification preferences", | |
| "Preparing booking visibility", | |
| "Queuing invitation email", | |
| "Syncing to Scheduling OS", | |
| "Finalizing onboarding status" | |
| ], | |
| // Panel Management | |
| togglePanel: function() { | |
| if (this.isOpen) { | |
| this.closePanel(); | |
| } else { | |
| this.openPanel(); | |
| } | |
| }, | |
| openPanel: function() { | |
| this.isOpen = true; | |
| const panel = document.getElementById('voice-panel'); | |
| const overlay = document.getElementById('voice-overlay'); | |
| const overlayBg = document.getElementById('voice-overlay-bg'); | |
| overlay.classList.remove('hidden'); | |
| panel.classList.remove('translate-x-full'); | |
| // Trigger reflow for animation | |
| void overlay.offsetWidth; | |
| overlayBg.classList.remove('opacity-0'); | |
| // Add mic pulse effect to button in main bar | |
| const micBtn = document.getElementById('voice-input-btn'); | |
| if(micBtn) { | |
| const pulse = document.createElement('div'); | |
| pulse.className = 'mic-active-ring'; | |
| micBtn.appendChild(pulse); | |
| } | |
| }, | |
| closePanel: function() { | |
| this.isOpen = false; | |
| const panel = document.getElementById('voice-panel'); | |
| const overlay = document.getElementById('voice-overlay'); | |
| const overlayBg = document.getElementById('voice-overlay-bg'); | |
| panel.classList.add('translate-x-full'); | |
| overlayBg.classList.add('opacity-0'); | |
| setTimeout(() => { | |
| overlay.classList.add('hidden'); | |
| this.exitWorkflow(); // Reset to menu if closed | |
| }, 300); | |
| // Remove mic pulse | |
| const micBtn = document.getElementById('voice-input-btn'); | |
| if(micBtn) { | |
| const pulse = micBtn.querySelector('.mic-active-ring'); | |
| if(pulse) pulse.remove(); | |
| } | |
| }, | |
| // Workflow Navigation | |
| startWorkflow: function() { | |
| this.currentStep = 1; | |
| this.providerData = { name: '', email: '' }; | |
| document.getElementById('voice-menu-view').classList.add('hidden'); | |
| document.getElementById('voice-workflow-view').classList.remove('hidden'); | |
| document.getElementById('voice-workflow-view').classList.add('flex'); | |
| this.renderStep(); | |
| this.updateUIState(); | |
| }, | |
| exitWorkflow: function() { | |
| document.getElementById('voice-menu-view').classList.remove('hidden'); | |
| document.getElementById('voice-workflow-view').classList.add('hidden'); | |
| document.getElementById('voice-workflow-view').classList.remove('flex'); | |
| this.currentStep = 1; | |
| }, | |
| nextStep: function() { | |
| // Validation | |
| if (this.currentStep === 1 && !this.providerData.name.trim()) { | |
| this.shakeInput('provider-name'); | |
| return; | |
| } | |
| if (this.currentStep === 2 && !this.validateEmail(this.providerData.email)) { | |
| this.shakeInput('provider-email'); | |
| return; | |
| } | |
| if (this.currentStep < this.totalSteps) { | |
| this.currentStep++; | |
| this.renderStep(); | |
| this.updateUIState(); | |
| } | |
| }, | |
| prevStep: function() { | |
| if (this.currentStep > 1) { | |
| this.currentStep--; | |
| this.renderStep(); | |
| this.updateUIState(); | |
| } | |
| }, | |
| updateUIState: function() { | |
| // Update Progress | |
| const progressPct = (this.currentStep / this.totalSteps) * 100; | |
| document.getElementById('workflow-progress').style.width = `${progressPct}%`; | |
| document.getElementById('workflow-step-badge').textContent = `Step ${this.currentStep} of ${this.totalSteps}`; | |
| // Update Buttons | |
| const backBtn = document.getElementById('workflow-back-btn'); | |
| const nextBtn = document.getElementById('workflow-next-btn'); | |
| backBtn.disabled = this.currentStep === 1; | |
| nextBtn.textContent = this.currentStep === 4 ? 'Run Automation' : | |
| this.currentStep === 5 ? 'Done' : 'Continue'; | |
| if (this.currentStep === 5) { | |
| nextBtn.classList.add('hidden'); | |
| backBtn.classList.add('hidden'); | |
| } else { | |
| nextBtn.classList.remove('hidden'); | |
| backBtn.classList.remove('hidden'); | |
| } | |
| }, | |
| renderStep: function() { | |
| const container = document.getElementById('workflow-content'); | |
| let html = ''; | |
| switch(this.currentStep) { | |
| case 1: // Name | |
| html = ` | |
| <div class="mb-6"> | |
| <h3 class="text-xl font-bold text-gray-900 mb-2">Provider Name</h3> | |
| <p class="text-gray-500">Enter the provider's display name for profile creation and scheduling setup.</p> | |
| </div> | |
| <div> | |
| <label class="workflow-label">Full Name</label> | |
| <input type="text" id="provider-name" class="workflow-input" placeholder="e.g. Dr. Olivia Carter" | |
| value="${this.providerData.name}" oninput="VoiceActions.updateData('name', this.value)"> | |
| </div> | |
| `; | |
| break; | |
| case 2: // Email | |
| html = ` | |
| <div class="mb-6"> | |
| <h3 class="text-xl font-bold text-gray-900 mb-2">Provider Email</h3> | |
| <p class="text-gray-500">Enter the email address for invitations, login access, and notifications.</p> | |
| </div> | |
| <div> | |
| <label class="workflow-label">Email Address</label> | |
| <input type="email" id="provider-email" class="workflow-input" placeholder="provider@clinic.com" | |
| value="${this.providerData.email}" oninput="VoiceActions.updateData('email', this.value)"> | |
| <p class="text-xs text-red-500 mt-2 hidden" id="email-error">Please enter a valid email address.</p> | |
| </div> | |
| `; | |
| break; | |
| case 3: // Verify | |
| html = ` | |
| <div class="mb-6"> | |
| <h3 class="text-xl font-bold text-gray-900 mb-2">Verify Details</h3> | |
| <p class="text-gray-500">Review provider information before automation begins.</p> | |
| </div> | |
| <div class="verify-card mb-6"> | |
| <div class="verify-row"> | |
| <span class="verify-label">Name</span> | |
| <div class="flex items-center"> | |
| <span class="verify-value mr-2">${this.providerData.name}</span> | |
| <button onclick="VoiceActions.goToStep(1)" class="text-[#089e97] text-xs font-medium hover:underline">Change</button> | |
| </div> | |
| </div> | |
| <div class="verify-row"> | |
| <span class="verify-label">Email</span> | |
| <div class="flex items-center"> | |
| <span class="verify-value mr-2">${this.providerData.email}</span> | |
| <button onclick="VoiceActions.goToStep(2)" class="text-[#089e97] text-xs font-medium hover:underline">Change</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <h4 class="text-sm font-bold text-gray-900 mb-3 flex items-center"> | |
| <i data-lucide="check-circle" class="w-4 h-4 mr-1.5 text-green-500"></i> | |
| Recommended Defaults | |
| </h4> | |
| <div class="space-y-3"> | |
| <label class="flex items-start"> | |
| <input type="checkbox" checked class="mt-1 h-4 w-4 text-[#089e97] rounded border-gray-300"> | |
| <span class="ml-3 text-sm text-gray-600">Create provider account & invite</span> | |
| </label> | |
| <label class="flex items-start"> | |
| <input type="checkbox" checked class="mt-1 h-4 w-4 text-[#089e97] rounded border-gray-300"> | |
| <span class="ml-3 text-sm text-gray-600">Set default availability (Mon-Fri, 9-5)</span> | |
| </label> | |
| <label class="flex items-start"> | |
| <input type="checkbox" checked class="mt-1 h-4 w-4 text-[#089e97] rounded border-gray-300"> | |
| <span class="ml-3 text-sm text-gray-600">Enable appointment reminders</span> | |
| </label> | |
| <label class="flex items-start"> | |
| <input type="checkbox" checked class="mt-1 h-4 w-4 text-[#089e97] rounded border-gray-300"> | |
| <span class="ml-3 text-sm text-gray-600">Add to provider directory</span> | |
| </label> | |
| </div> | |
| </div> | |
| `; | |
| break; | |
| case 4: // Automation Simulation | |
| html = ` | |
| <div class="mb-6"> | |
| <h3 class="text-xl font-bold text-gray-900 mb-2">Setting Up Profile</h3> | |
| <p class="text-gray-500">AI is configuring your provider settings...</p> | |
| </div> | |
| <div class="bg-[#089e97]/5 rounded-xl p-4 mb-6 border border-[#089e97]/10"> | |
| <div class="flex justify-between text-sm mb-2"> | |
| <span class="font-medium text-gray-700">Progress</span> | |
| <span class="font-bold text-[#089e97]" id="automation-percent">0%</span> | |
| </div> | |
| <div class="h-2 bg-gray-200 rounded-full overflow-hidden"> | |
| <div id="automation-bar" class="h-full bg-[#089e97] transition-all duration-300" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div class="automation-timeline" id="automation-list"> | |
| ${this.automationTasks.map((task, i) => ` | |
| <div class="timeline-item" id="task-${i}"> | |
| <div class="timeline-marker"> | |
| <i data-lucide="check" class="w-3 h-3"></i> | |
| </div> | |
| <div class="text-sm text-gray-600">${task}</div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| `; | |
| // Trigger animation after render | |
| setTimeout(() => this.runAutomation(), 100); | |
| break; | |
| case 5: // Success | |
| html = ` | |
| <div class="text-center pt-4"> | |
| <div class="success-icon-circle"> | |
| <i data-lucide="check" class="w-10 h-10 text-white"></i> | |
| </div> | |
| <h3 class="text-2xl font-bold text-gray-900 mb-2">Provider Onboarding Completed</h3> | |
| <p class="text-gray-500 mb-8 max-w-xs mx-auto">The provider account and onboarding defaults have been prepared successfully.</p> | |
| <div class="verify-card text-left mb-6"> | |
| <div class="verify-row"> | |
| <span class="verify-label">Provider</span> | |
| <span class="verify-value">${this.providerData.name}</span> | |
| </div> | |
| <div class="verify-row"> | |
| <span class="verify-label">Account Status</span> | |
| <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700">Ready</span> | |
| </div> | |
| <div class="verify-row"> | |
| <span class="verify-label">Scheduling</span> | |
| <span class="verify-value">Mon-Fri, 9-5</span> | |
| </div> | |
| <div class="verify-row"> | |
| <span class="verify-label">Invitation</span> | |
| <span class="verify-value">Queued</span> | |
| </div> | |
| </div> | |
| <div class="space-y-3"> | |
| <button onclick="VoiceActions.closePanel()" class="w-full py-3 bg-[#089e97] text-white rounded-lg font-medium hover:bg-[#078b85] transition-colors"> | |
| Done | |
| </button> | |
| <button onclick="alert('Navigating to profile...')" class="w-full py-3 bg-white border border-gray-300 text-gray-700 rounded-lg font-medium hover:bg-gray-50 transition-colors"> | |
| View Profile | |
| </button> | |
| </div> | |
| <p class="text-xs text-gray-400 mt-6"> | |
| Recommended defaults were applied based on standard provider onboarding policies. | |
| </p> | |
| </div> | |
| `; | |
| break; | |
| } | |
| container.innerHTML = html; | |
| lucide.createIcons(); | |
| // Focus inputs on Steps 1 & 2 | |
| if(this.currentStep === 1) setTimeout(() => document.getElementById('provider-name')?.focus(), 50); | |
| if(this.currentStep === 2) setTimeout(() => document.getElementById('provider-email')?.focus(), 50); | |
| }, | |
| // Logic Helpers | |
| updateData: function(key, value) { | |
| this.providerData[key] = value; | |
| // Clear errors | |
| if(key === 'email') { | |
| const err = document.getElementById('email-error'); | |
| if(err) err.classList.add('hidden'); | |
| } | |
| }, | |
| goToStep: function(step) { | |
| this.currentStep = step; | |
| this.renderStep(); | |
| this.updateUIState(); | |
| }, | |
| validateEmail: function(email) { | |
| return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email); | |
| }, | |
| shakeInput: function(id) { | |
| const el = document.getElementById(id); | |
| if(el) { | |
| el.classList.add('border-red-500', 'animate-pulse'); | |
| setTimeout(() => el.classList.remove('border-red-500', 'animate-pulse'), 1000); | |
| } | |
| if(id === 'provider-email') { | |
| document.getElementById('email-error').classList.remove('hidden'); | |
| } | |
| }, | |
| runAutomation: async function() { | |
| const tasks = this.automationTasks; | |
| const bar = document.getElementById('automation-bar'); | |
| const text = document.getElementById('automation-percent'); | |
| for (let i = 0; i < tasks.length; i++) { | |
| // Update UI | |
| const item = document.getElementById(`task-${i}`); | |
| item.classList.add('active'); | |
| item.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
| // Wait random time | |
| await new Promise(r => setTimeout(r, 400 + Math.random() * 400)); | |
| // Complete Item | |
| item.classList.remove('active'); | |
| item.classList.add('completed'); | |
| // Update Progress Bar | |
| const pct = Math.round(((i + 1) / tasks.length) * 100); | |
| bar.style.width = `${pct}%`; | |
| text.textContent = `${pct}%`; | |
| } | |
| // Auto advance to success after short delay | |
| setTimeout(() => { | |
| this.nextStep(); | |
| }, 800); | |
| } | |
| }; | |
| // Attach to Voice Button (If exists in DOM, otherwise listener added below) | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const voiceBtn = document.getElementById('voice-input-btn'); | |
| if (voiceBtn) { | |
| // Remove any existing listeners to avoid duplicates if script re-runs | |
| const newBtn = voiceBtn.cloneNode(true); | |
| voiceBtn.parentNode.replaceChild(newBtn, voiceBtn); | |
| newBtn.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| VoiceActions.togglePanel(); | |
| }); | |
| } | |
| // Close panel on overlay click | |
| document.getElementById('voice-overlay').addEventListener('click', (e) => { | |
| if(e.target === document.getElementById('voice-overlay') || e.target.id === 'voice-overlay-bg') { | |
| VoiceActions.closePanel(); | |
| } | |
| }); | |
| // Close on ESC | |
| document.addEventListener('keydown', (e) => { | |
| if(e.key === 'Escape' && VoiceActions.isOpen) { | |
| VoiceActions.closePanel(); | |
| } | |
| }); | |
| }); | |
| // ========================================= | |
| // VOICE & CONVERSATIONAL MODE (VCM V1) | |
| // ========================================= | |
| const ChatContext = { | |
| conversationHistory: [], | |
| lastAction: null, | |
| userIntent: null | |
| }; | |
| // Voice Recognition Setup | |
| const VoiceRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; | |
| let recognition = null; | |
| let isRecording = false; | |
| if (VoiceRecognition) { | |
| recognition = new VoiceRecognition(); | |
| recognition.continuous = false; | |
| recognition.lang = 'en-US'; | |
| recognition.interimResults = true; | |
| recognition.onstart = () => { | |
| isRecording = true; | |
| const btn = document.getElementById('voice-input-btn'); | |
| const visualizer = document.getElementById('voice-visualizer'); | |
| if (btn) btn.classList.add('active'); | |
| if (visualizer) visualizer.classList.add('active'); | |
| }; | |
| recognition.onresult = (event) => { | |
| const transcript = Array.from(event.results) | |
| .map(result => result[0]) | |
| .map(result => result.transcript) | |
| .join(''); | |
| const input = document.getElementById('ai-command-input'); | |
| if (input && event.results[0].isFinal) { | |
| input.value = transcript; | |
| // Trigger search/command processing | |
| const event = new KeyboardEvent('keydown', { key: 'Enter' }); | |
| input.dispatchEvent(event); | |
| stopVoiceRecording(); | |
| } else if (input) { | |
| input.value = transcript; | |
| } | |
| }; | |
| recognition.onerror = (event) => { | |
| console.error('Voice recognition error', event.error); | |
| stopVoiceRecording(); | |
| }; | |
| recognition.onend = () => { | |
| stopVoiceRecording(); | |
| }; | |
| } | |
| function startVoiceRecording() { | |
| if (!recognition) { | |
| showToast('Voice recognition not supported in this browser'); | |
| return; | |
| } | |
| // Open Copilot Chat Panel | |
| openVCMCopilot(); | |
| try { | |
| recognition.start(); | |
| } catch (e) { | |
| // Already started | |
| } | |
| } | |
| function stopVoiceRecording() { | |
| if (recognition && isRecording) { | |
| recognition.stop(); | |
| } | |
| const btn = document.getElementById('voice-input-btn'); | |
| const visualizer = document.getElementById('voice-visualizer'); | |
| if (btn) btn.classList.remove('active'); | |
| if (visualizer) visualizer.classList.remove('active'); | |
| isRecording = false; | |
| } | |
| // Attach Voice Button Listener - UPDATED FOR AI VOICE ACTIONS | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const voiceBtn = document.getElementById('voice-input-btn'); | |
| if (voiceBtn) { | |
| voiceBtn.addEventListener('click', () => { | |
| // Open the AI Voice Actions Panel instead of just recording | |
| toggleAIVoicePanel(); | |
| }); | |
| } | |
| // Chat Input Enter Key | |
| const chatInput = document.getElementById('vcm-chat-input'); | |
| if (chatInput) { | |
| chatInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter') { | |
| sendVCMMessage(); | |
| } | |
| }); | |
| } | |
| }); | |
| // Copilot Panel Functions | |
| function openVCMCopilot() { | |
| const overlay = document.getElementById('vcm-copilot-overlay'); | |
| const panel = document.getElementById('vcm-copilot-panel'); | |
| if (!overlay || !panel) return; | |
| overlay.classList.remove('hidden'); | |
| panel.classList.remove('translate-x-full'); | |
| // Delay opacity for smooth fade | |
| setTimeout(() => overlay.classList.remove('opacity-0'), 10); | |
| // Focus input | |
| setTimeout(() => { | |
| const input = document.getElementById('vcm-chat-input'); | |
| if (input) input.focus(); | |
| }, 300); | |
| } | |
| function closeVCMCopilot() { | |
| const overlay = document.getElementById('vcm-copilot-overlay'); | |
| const panel = document.getElementById('vcm-copilot-panel'); | |
| if (!overlay || !panel) return; | |
| panel.classList.add('translate-x-full'); | |
| overlay.classList.add('opacity-0'); | |
| setTimeout(() => { | |
| overlay.classList.add('hidden'); | |
| }, 300); | |
| stopVoiceRecording(); | |
| } | |
| function minimizeVCMCopilot() { | |
| closeVCMCopilot(); | |
| } | |
| // Chat Message Logic | |
| function sendVCMMessage() { | |
| const input = document.getElementById('vcm-chat-input'); | |
| const text = input.value.trim(); | |
| if (!text) return; | |
| // Add User Message | |
| addVCMMessage('user', text); | |
| input.value = ''; | |
| // Show Typing Indicator | |
| showVCMTyping(); | |
| // Simulate AI Processing | |
| setTimeout(() => { | |
| hideVCMTyping(); | |
| processVCMIntent(text); | |
| }, 1000 + Math.random() * 1000); | |
| } | |
| function addVCMMessage(sender, text, actions = []) { | |
| const history = document.getElementById('vcm-chat-history'); | |
| if (!history) return; | |
| const messageDiv = document.createElement('div'); | |
| messageDiv.className = sender === 'user' ? 'vcm-message-user' : 'vcm-message-ai'; | |
| let contentHTML = ''; | |
| if (sender === 'user') { | |
| contentHTML = `<div class="vcm-bubble-user text-sm">${text}</div>`; | |
| } else { | |
| let avatarHTML = ` | |
| <div class="w-8 h-8 bg-gradient-to-br from-violet-600 to-purple-600 rounded-full flex items-center justify-center flex-shrink-0 mr-3 shadow-sm"> | |
| <i data-lucide="bot" class="w-4 h-4 text-white"></i> | |
| </div> | |
| `; | |
| let actionsHTML = ''; | |
| if (actions.length > 0) { | |
| actionsHTML = ` | |
| <div class="vcm-chat-actions"> | |
| ${actions.map(action => ` | |
| <button onclick="executeVCMAction('${action.id}', '${action.label}')" | |
| class="vcm-action-chip ${action.primary ? 'primary' : ''}"> | |
| ${action.label} | |
| </button> | |
| `).join('')} | |
| </div> | |
| `; | |
| } | |
| contentHTML = ` | |
| ${avatarHTML} | |
| <div> | |
| <div class="vcm-bubble-ai text-sm">${text}</div> | |
| ${actionsHTML} | |
| </div> | |
| `; | |
| } | |
| messageDiv.innerHTML = contentHTML; | |
| history.appendChild(messageDiv); | |
| // Re-init icons | |
| lucide.createIcons(); | |
| // Scroll to bottom | |
| history.scrollTop = history.scrollHeight; | |
| } | |
| function showVCMTyping() { | |
| const indicator = document.getElementById('vcm-typing-indicator'); | |
| if (indicator) indicator.classList.remove('hidden'); | |
| } | |
| function hideVCMTyping() { | |
| const indicator = document.getElementById('vcm-typing-indicator'); | |
| if (indicator) indicator.classList.add('hidden'); | |
| } | |
| // Contextual AI Response Logic | |
| function processVCMIntent(text) { | |
| const lowerText = text.toLowerCase(); | |
| // Pattern Matching for Intent | |
| if (lowerText.includes('add provider') || lowerText.includes('new doctor') || lowerText.includes('hire')) { | |
| ChatContext.userIntent = 'add-provider'; | |
| addVCMMessage('ai', "I can help you add a new provider. Should I create the profile using your standard template (Dr. [Name], Cardiology)?", [ | |
| { id: 'create-provider', label: 'Yes, Create Profile', primary: true }, | |
| { id: 'open-operator', label: 'Use Full Workflow' } | |
| ]); | |
| } | |
| else if (lowerText.includes('sms') || lowerText.includes('reminder') || lowerText.includes('text')) { | |
| addVCMMessage('ai', "I can connect Twilio for SMS reminders. This typically reduces no-shows by 70%. Shall I connect it now?", [ | |
| { id: 'enable-sms', label: 'Connect Twilio', primary: true }, | |
| { id: 'details', label: 'Show Details' } | |
| ]); | |
| } | |
| else if (lowerText.includes('payment') || lowerText.includes('stripe') || lowerText.includes('billing')) { | |
| addVCMMessage('ai', "Your payment processor isn't connected yet. I can set up Stripe for you. Do you want to enable online payments immediately?", [ | |
| { id: 'connect-payments', label: 'Setup Stripe', primary: true }, | |
| { id: 'test-payments', label: 'Test Transaction' } | |
| ]); | |
| } | |
| else if (lowerText.includes('schedule') || lowerText.includes('availability') || lowerText.includes('hours')) { | |
| addVCMMessage('ai', "I can set up availability rules. Would you like to apply the 'Standard 9-5' template again?", [ | |
| { id: 'apply-schedule', label: 'Apply Template', primary: true }, | |
| { id: 'custom-schedule', label: 'Customize' } | |
| ]); | |
| } | |
| else if (lowerText.includes('status') || lowerText.includes('health') || lowerText.includes('progress')) { | |
| const progress = document.getElementById('setup-progress-percentage').textContent; | |
| addVCMMessage('ai', `Your setup progress is currently at ${progress}. You have 2 critical alerts regarding Payments and SMS. Would you like me to fix them?`, [ | |
| { id: 'auto-fix', label: 'Auto-Fix All', primary: true } | |
| ]); | |
| } | |
| else { | |
| // Fallback / General | |
| addVCMMessage('ai', "I can help with providers, scheduling, payments, or notifications. What would you like to do?", [ | |
| { id: 'add-provider', label: 'Add Provider' }, | |
| { id: 'connect-payments', label: 'Setup Payments' }, | |
| { id: 'enable-sms', label: 'Enable SMS' } | |
| ]); | |
| } | |
| } | |
| // Action Execution from Chat | |
| function executeVCMAction(actionId, label) { | |
| // Add "Performing action" feedback message | |
| addVCMMessage('user', `I want to ${label.toLowerCase()}`); | |
| showVCMTyping(); | |
| // Simulate processing | |
| setTimeout(() => { | |
| hideVCMTyping(); | |
| // Execute based on action ID | |
| switch(actionId) { | |
| case 'create-provider': | |
| // Update Dashboard (Live Sync) | |
| updateDashboardState({ | |
| progress: 5, | |
| scores: { health: 2 } | |
| }); | |
| addVCMMessage('ai', "I've created a placeholder provider profile. I've assigned standard permissions. You can edit the specific details in the Providers tab.", [ | |
| { id: 'navigate-providers', label: 'Go to Providers', primary: true } | |
| ]); | |
| break; | |
| case 'enable-sms': | |
| // Update Alerts & Scores | |
| updateDashboardState({ | |
| progress: 4, | |
| alerts: ['sms'], | |
| scores: { automation: 10 } | |
| }); | |
| addVCMMessage('ai', "Twilio is now connected! SMS reminders are active. I've set the default timing to 24 hours before appointments.", [ | |
| { id: 'test-sms', label: 'Send Test SMS' } | |
| ]); | |
| break; | |
| case 'connect-payments': | |
| updateDashboardState({ | |
| progress: 8, | |
| alerts: ['payments'], | |
| scores: { revenue: 20 } | |
| }); | |
| addVCMMessage('ai', "Stripe is connected and online payments are enabled. I've configured the deposit requirement to None for now.", [ | |
| { id: 'navigate-payments', label: 'Configure Billing', primary: true } | |
| ]); | |
| break; | |
| case 'auto-fix': | |
| updateDashboardState({ | |
| progress: 12, | |
| alerts: ['sms', 'payments'], | |
| scores: { health: 5, automation: 10, revenue: 15 } | |
| }); | |
| addVCMMessage('ai', "Done! I've fixed both the Payments and SMS alerts. Your system health score has improved. Is there anything else?", [ | |
| { id: 'optimize', label: 'Optimize Further' } | |
| ]); | |
| break; | |
| case 'apply-schedule': | |
| addVCMMessage('ai', "Schedule template 'Standard 9-5' applied to all active providers. Buffer times set to 10 minutes."); | |
| updateDashboardState({ progress: 3 }); | |
| break; | |
| case 'open-operator': | |
| closeVCMCopilot(); | |
| setTimeout(() => triggerAIWorkflow('auto-configure'), 300); | |
| break; | |
| default: | |
| addVCMMessage('ai', "Action noted. I've updated the relevant settings in the background."); | |
| } | |
| // Trigger Toast for main UI feedback | |
| showToast(`AI executed: ${label}`); | |
| }, 800); | |
| } | |
| // ========================================= | |
| // AI OPERATOR MODE (AOM V1) - WORKFLOW ENGINE | |
| // ========================================= | |
| const AIContext = { | |
| lastScheduleTemplate: 'Standard 9-5', | |
| notificationDefaults: ['Email', 'SMS'], | |
| currency: 'USD' | |
| }; | |
| const Workflows = { | |
| 'add-provider': { | |
| title: 'Add New Provider', | |
| steps: [ | |
| { id: 'create-profile', title: 'Create Provider Profile', description: 'Generate unique ID and basic record', action: 'createProviderProfile' }, | |
| { id: 'assign-permissions', title: 'Assign Default Permissions', description: 'Apply role-based access controls', action: 'assignPermissions' }, | |
| { id: 'notify-admin', title: 'Notify Administrators', description: 'Send onboarding email to admin team', action: 'notifyAdmin' } | |
| ] | |
| }, | |
| 'enable-sms': { | |
| title: 'Enable SMS Reminders', | |
| steps: [ | |
| { id: 'check-twilio', title: 'Verify Twilio Connection', description: 'Ping API gateway for status', action: 'checkTwilio' }, | |
| { id: 'activate-reminders', title: 'Activate Reminder Engine', description: 'Enable automated SMS logic', action: 'activateReminders' }, | |
| { id: 'send-test', title: 'Send Test SMS', description: 'Verify delivery to admin phone', action: 'sendTestSMS' } | |
| ], | |
| impact: { progress: 4, alerts: ['sms'], scores: { automation: 5 } } | |
| }, | |
| 'auto-configure': { | |
| title: 'Auto-Configure Platform', | |
| steps: [ | |
| { id: 'audit-system', title: 'Audit System State', description: 'Analyze current configuration gaps', action: 'auditSystem' }, | |
| { id: 'fix-payments', title: 'Fix Payment Integration', description: 'Connect Stripe gateway with defaults', action: 'fixPayments' }, | |
| { id: 'enable-notifications', title: 'Enable Notifications', description: 'Turn on email and SMS channels', action: 'enableNotifications' }, | |
| { id: 'optimize-scheduling', title: 'Optimize Scheduling', description: 'Apply AI-optimized buffer times', action: 'optimizeScheduling' } | |
| ], | |
| impact: { progress: 20, alerts: ['payments', 'sms'], scores: { health: 5, revenue: 15, automation: 10 } } | |
| }, | |
| 'setup-onboarding': { | |
| title: 'Set Up Provider Onboarding', | |
| steps: [ | |
| { id: 'create-provider', title: 'Create Provider Profile', description: 'Dr. Sarah Johnson (Cardiology)', action: 'createProviderProfile' }, | |
| { id: 'assign-schedule', title: 'Assign Schedule', description: `Apply template: ${AIContext.lastScheduleTemplate}`, action: 'assignSchedule' }, | |
| { id: 'enable-reminders', title: 'Enable Patient Reminders', description: `Enable: ${AIContext.notificationDefaults.join(' & ')}`, action: 'enableReminders' }, | |
| { id: 'generate-link', title: 'Generate Booking Link', description: 'Create unique provider booking URL', action: 'generateLink' }, | |
| { id: 'send-welcome', title: 'Send Welcome Email', description: 'Dispatch credentials and handbook', action: 'sendWelcomeEmail' } | |
| ], | |
| impact: { progress: 8, alerts: [], scores: { automation: 5, health: 2 } } | |
| } | |
| }; | |
| // Command Parser | |
| function parseCommand(input) { | |
| const lowerInput = input.toLowerCase().trim(); | |
| if (lowerInput.includes('onboarding') || lowerInput.includes('set up new provider')) return 'setup-onboarding'; | |
| if (lowerInput.includes('add provider')) return 'add-provider'; | |
| if (lowerInput.includes('sms') || lowerInput.includes('reminders')) return 'enable-sms'; | |
| if (lowerInput.includes('payment') || lowerInput.includes('billing')) return 'enable-payments'; | |
| if (lowerInput.includes('auto') || lowerInput.includes('configure') || lowerInput.includes('optimize')) return 'auto-configure'; | |
| return null; | |
| } | |
| // Trigger workflow from UI | |
| function triggerAIWorkflow(workflowKey) { | |
| const workflow = Workflows[workflowKey]; | |
| if (!workflow) return; | |
| openAIOperator(workflow); | |
| } | |
| // Open AI Operator Panel | |
| function openAIOperator(workflow) { | |
| const overlay = document.getElementById('ai-operator-overlay'); | |
| const panel = document.getElementById('ai-operator-panel'); | |
| const content = document.getElementById('ai-operator-content'); | |
| const footer = document.getElementById('ai-operator-footer'); | |
| // Reset UI | |
| overlay.classList.remove('hidden'); | |
| panel.classList.remove('translate-x-full'); | |
| setTimeout(() => overlay.classList.remove('opacity-0'), 10); | |
| // Render Execution Plan | |
| renderExecutionPlan(workflow, content, footer); | |
| lucide.createIcons(); | |
| } | |
| function closeAIOperator() { | |
| const overlay = document.getElementById('ai-operator-overlay'); | |
| const panel = document.getElementById('ai-operator-panel'); | |
| panel.classList.add('translate-x-full'); | |
| overlay.classList.add('opacity-0'); | |
| setTimeout(() => { | |
| overlay.classList.add('hidden'); | |
| }, 300); | |
| } | |
| // Render Execution Plan | |
| function renderExecutionPlan(workflow, container, footer) { | |
| container.innerHTML = ` | |
| <div class="mb-6"> | |
| <div class="inline-flex items-center px-3 py-1 bg-blue-50 text-blue-700 rounded-full text-xs font-semibold mb-3"> | |
| <i data-lucide="zap" class="w-3 h-3 mr-1.5"></i> | |
| AI Plan Generated | |
| </div> | |
| <h2 class="text-xl font-bold text-gray-900 mb-2">${workflow.title}</h2> | |
| <p class="text-gray-600 text-sm">AI will perform the following ${workflow.steps.length} actions to complete this request.</p> | |
| </div> | |
| <div class="space-y-3 mb-6" id="workflow-steps-list"> | |
| ${workflow.steps.map((step, index) => ` | |
| <div class="workflow-step" id="step-${index}"> | |
| <div class="step-header"> | |
| <div class="flex items-center flex-1"> | |
| <div class="step-status-icon mr-3">${index + 1}</div> | |
| <div> | |
| <div class="step-title">${step.title}</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="step-description">${step.description}</div> | |
| <div class="step-progress"><div class="step-progress-bar"></div></div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| <div class="bg-amber-50 border border-amber-200 rounded-lg p-3 flex items-start"> | |
| <i data-lucide="alert-circle" class="w-4 h-4 text-amber-600 mt-0.5 mr-2 flex-shrink-0"></i> | |
| <p class="text-xs text-amber-800">This will modify system settings. Review the plan above before proceeding.</p> | |
| </div> | |
| `; | |
| footer.innerHTML = ` | |
| <button onclick="closeAIOperator()" class="px-4 py-2 text-gray-700 hover:bg-gray-100 rounded-lg font-medium text-sm transition-colors"> | |
| Cancel | |
| </button> | |
| <button onclick="runWorkflow('${workflow.title}')" class="px-6 py-2 bg-[#089e97] text-white rounded-lg font-medium text-sm hover:bg-[#078b85] transition-colors shadow-lg shadow-[#089e97]/20 flex items-center"> | |
| <i data-lucide="play" class="w-4 h-4 mr-2"></i> | |
| Run Plan | |
| </button> | |
| `; | |
| } | |
| // Execute Workflow | |
| async function runWorkflow(title) { | |
| const workflow = Object.values(Workflows).find(w => w.title === title); | |
| if (!workflow) return; | |
| const footer = document.getElementById('ai-operator-footer'); | |
| const status = document.getElementById('ai-operator-status'); | |
| // Update footer to show running state | |
| footer.innerHTML = ` | |
| <div class="flex items-center text-gray-500 text-sm"> | |
| <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-[#089e97] mr-2"></div> | |
| Executing... | |
| </div> | |
| `; | |
| status.textContent = 'Executing workflow...'; | |
| // Execute steps sequentially | |
| for (let i = 0; i < workflow.steps.length; i++) { | |
| await executeStep(i, workflow.steps[i]); | |
| } | |
| // Workflow Complete | |
| completeWorkflow(workflow); | |
| } | |
| async function executeStep(index, step) { | |
| const stepEl = document.getElementById(`step-${index}`); | |
| const progressBar = stepEl.querySelector('.step-progress-bar'); | |
| const statusIcon = stepEl.querySelector('.step-status-icon'); | |
| // Mark as Active | |
| stepEl.classList.add('active', 'running'); | |
| statusIcon.innerHTML = '<i data-lucide="loader-2" class="w-3 h-3 animate-spin"></i>'; | |
| lucide.createIcons(); | |
| // Simulate Progress | |
| progressBar.style.width = '30%'; | |
| await wait(600); | |
| progressBar.style.width = '70%'; | |
| await wait(600); | |
| progressBar.style.width = '100%'; | |
| // Mark as Completed | |
| await wait(400); | |
| stepEl.classList.remove('active', 'running'); | |
| stepEl.classList.add('completed'); | |
| statusIcon.innerHTML = '<i data-lucide="check" class="w-3 h-3"></i>'; | |
| lucide.createIcons(); | |
| } | |
| function completeWorkflow(workflow) { | |
| const status = document.getElementById('ai-operator-status'); | |
| const footer = document.getElementById('ai-operator-footer'); | |
| const content = document.getElementById('ai-operator-content'); | |
| status.textContent = 'Complete'; | |
| status.classList.add('text-green-600'); | |
| // Update Content to Success State | |
| content.innerHTML = ` | |
| <div class="text-center py-10"> | |
| <div class="w-20 h-20 bg-green-100 rounded-full flex items-center justify-center mx-auto mb-6 animate-bounce"> | |
| <i data-lucide="check" class="w-10 h-10 text-green-600"></i> | |
| </div> | |
| <h2 class="text-2xl font-bold text-gray-900 mb-2">Workflow Complete</h2> | |
| <p class="text-gray-600 mb-8">All ${workflow.steps.length} steps executed successfully.</p> | |
| <div class="bg-gray-50 rounded-xl p-4 max-w-xs mx-auto text-left"> | |
| <h4 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">Updates Applied</h4> | |
| <div class="space-y-2 text-sm text-gray-700"> | |
| ${workflow.impact ? ` | |
| ${workflow.impact.progress ? `<div class="flex justify-between"><span>Setup Progress</span><span class="text-green-600 font-medium">+${workflow.impact.progress}%</span></div>` : ''} | |
| ${workflow.impact.scores ? Object.entries(workflow.impact.scores).map(([k, v]) => ` | |
| <div class="flex justify-between"><span class="capitalize">${k} Score</span><span class="text-green-600 font-medium">+${v}%</span></div> | |
| `).join('') : ''} | |
| ` : '<div>System configuration updated</div>'} | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| footer.innerHTML = ` | |
| <button onclick="closeAIOperator()" class="w-full px-6 py-3 bg-[#089e97] text-white rounded-lg font-medium hover:bg-[#078b85] transition-colors"> | |
| Done | |
| </button> | |
| `; | |
| lucide.createIcons(); | |
| // Update Main Dashboard UI (No Reload) | |
| if (workflow.impact) updateDashboardState(workflow.impact); | |
| showToast(`${workflow.title} completed successfully`); | |
| } | |
| function updateDashboardState(impact) { | |
| // Update Progress Bar | |
| if (impact.progress) { | |
| const currentBar = document.getElementById('setup-progress-bar'); | |
| const currentText = document.getElementById('setup-progress-percentage'); | |
| if (currentBar && currentText) { | |
| let currentWidth = parseInt(currentBar.style.width); | |
| let newWidth = Math.min(100, currentWidth + impact.progress); | |
| currentBar.style.width = `${newWidth}%`; | |
| currentText.textContent = `${newWidth}%`; | |
| } | |
| } | |
| // Update System Scores | |
| if (impact.scores) { | |
| Object.entries(impact.scores).forEach(([key, value]) => { | |
| const scoreEl = document.getElementById(`${key}-score`); | |
| const barEl = document.getElementById(`${key}-bar`); | |
| if (scoreEl && barEl) { | |
| let currentScore = parseInt(scoreEl.textContent); | |
| let newScore = Math.min(100, currentScore + value); | |
| scoreEl.textContent = `${newScore}%`; | |
| barEl.style.width = `${newScore}%`; | |
| } | |
| }); | |
| } | |
| // Update Alerts | |
| if (impact.alerts && impact.alerts.length > 0) { | |
| impact.alerts.forEach(alertKey => { | |
| const alertEl = document.querySelector(`.alert-item[data-alert="${alertKey}"]`); | |
| if (alertEl) { | |
| // Simulate fixing alert by changing style | |
| alertEl.style.opacity = '0.5'; | |
| alertEl.querySelector('.status-amber-700').textContent = 'Resolved'; | |
| alertEl.querySelector('.status-amber-100').classList.replace('status-amber-100', 'status-green-100'); | |
| alertEl.querySelector('.status-amber-700').classList.replace('status-amber-700', 'status-green-700'); | |
| } | |
| }); | |
| } | |
| } | |
| function wait(ms) { | |
| return new Promise(resolve => setTimeout(resolve, ms)); | |
| } | |
| // Enhanced Command Bar Listener | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const commandInput = document.getElementById('ai-command-input'); | |
| if (commandInput) { | |
| commandInput.addEventListener('keydown', (e) => { | |
| if (e.key === 'Enter') { | |
| const workflowKey = parseCommand(commandInput.value); | |
| if (workflowKey) { | |
| commandInput.value = ''; | |
| triggerAIWorkflow(workflowKey); | |
| } else { | |
| showToast('Command not recognized. Try "Add provider" or "Auto-configure"'); | |
| } | |
| } | |
| }); | |
| } | |
| }); | |
| // App State | |
| const appState = { | |
| currentView: 'dashboard', | |
| settingsSidebarExpanded: true, | |
| aiPanelExpanded: false, | |
| sidebarSectionsState: { | |
| organization: true, // Expanded by default (contains dashboard) | |
| scheduling: false, | |
| payments: false, | |
| communication: false, | |
| automation: false, | |
| security: false | |
| }, | |
| progress: { | |
| total: 15, | |
| completed: 11 | |
| }, | |
| checklist: [ | |
| { id: 'org-details', completed: true, group: 'Organization Setup', description: 'Add organization details' }, | |
| { id: 'upload-logo', completed: false, group: 'Organization Setup', description: 'Upload logo & branding' }, | |
| { id: 'add-locations', completed: true, group: 'Organization Setup', description: 'Add clinic locations' }, | |
| { id: 'configure-providers', completed: true, group: 'Scheduling Setup', description: 'Configure providers' }, | |
| { id: 'set-availability', completed: false, group: 'Scheduling Setup', description: 'Set availability rules' }, | |
| { id: 'appointment-types', completed: true, group: 'Scheduling Setup', description: 'Create appointment types' }, | |
| { id: 'connect-payments', completed: false, group: 'Payments Setup', description: 'Connect payment processor' }, | |
| { id: 'billing-setup', completed: true, group: 'Payments Setup', description: 'Set up billing & invoicing' }, | |
| { id: 'insurance-setup', completed: false, group: 'Payments Setup', description: 'Configure insurance' }, | |
| { id: 'enable-notifications', completed: true, group: 'Communication Setup', description: 'Enable notifications' }, | |
| { id: 'setup-sms', completed: false, group: 'Communication Setup', description: 'Setup SMS & Email' }, | |
| { id: 'create-templates', completed: true, group: 'Communication Setup', description: 'Create communication templates' }, | |
| { id: 'compliance-setup', completed: true, group: 'Compliance Setup', description: 'Configure compliance settings' }, | |
| { id: 'security-audit', completed: true, group: 'Compliance Setup', description: 'Complete security audit' }, | |
| { id: 'ai-optimization', completed: false, group: 'AI Optimization', description: 'Enable AI scheduling' } | |
| ], | |
| settings: { | |
| organization: { | |
| name: 'MediCare Clinic', | |
| locations: 3, | |
| providers: 12 | |
| }, | |
| scheduling: { | |
| appointmentTypes: 8, | |
| weeklyHours: 45 | |
| }, | |
| payments: { | |
| processorConnected: false, | |
| insuranceNetworks: 2 | |
| }, | |
| notifications: { | |
| templates: 5, | |
| smsEnabled: true | |
| } | |
| } | |
| }; | |
| // Calculate progress percentage | |
| function calculateProgress() { | |
| const completed = appState.checklist.filter(item => item.completed).length; | |
| const total = appState.checklist.length; | |
| return Math.round((completed / total) * 100); | |
| } | |
| // Update progress display | |
| function updateProgressDisplay() { | |
| const percentage = calculateProgress(); | |
| const progressBar = document.querySelector('.progress-bar'); | |
| const progressText = document.querySelector('.text-2xl.font-bold.text-gray-900'); | |
| if (progressBar) { | |
| progressBar.style.background = `linear-gradient(90deg, #089e97 0%, #089e97 ${percentage}%, #e5e7eb ${percentage}%, #e5e7eb 100%)`; | |
| } | |
| if (progressText) { | |
| progressText.textContent = `${percentage}%`; | |
| } | |
| } | |
| // Toggle checklist item | |
| function toggleChecklistItem(itemId) { | |
| const item = appState.checklist.find(item => item.id === itemId); | |
| if (item) { | |
| item.completed = !item.completed; | |
| // Update UI | |
| const checklistItem = document.querySelector(`[data-item="${itemId}"]`); | |
| if (checklistItem) { | |
| checklistItem.classList.toggle('completed'); | |
| const checkIcon = checklistItem.querySelector('.check-circle i'); | |
| if (checkIcon) { | |
| checkIcon.classList.toggle('hidden'); | |
| } | |
| } | |
| updateProgressDisplay(); | |
| showToast('Checklist updated'); | |
| } | |
| } | |
| // ========================================= | |
| // GIS V1: ACTION FEEDBACK SYSTEM | |
| // ========================================= | |
| // Apply immediate loading state to any button | |
| function applyActionFeedback(buttonElement) { | |
| if (!buttonElement) return; | |
| // Store original text/content | |
| const originalHTML = buttonElement.innerHTML; | |
| const originalWidth = buttonElement.offsetWidth; | |
| // Apply loading state | |
| buttonElement.classList.add('btn-loading'); | |
| buttonElement.style.width = originalWidth + 'px'; // Prevent width collapse | |
| // Remove loading state after short delay (simulating action processing) | |
| setTimeout(() => { | |
| buttonElement.classList.remove('btn-loading'); | |
| buttonElement.style.width = ''; | |
| }, 600); | |
| } | |
| // Show toast notification | |
| function showToast(message) { | |
| const toast = document.getElementById('toast'); | |
| if (toast) { | |
| toast.querySelector('span').textContent = message; | |
| toast.classList.remove('hidden'); | |
| // Fade out smoothly | |
| setTimeout(() => { | |
| toast.style.opacity = '0'; | |
| toast.style.transition = 'opacity 0.3s ease'; | |
| setTimeout(() => { | |
| toast.classList.add('hidden'); | |
| toast.style.opacity = '1'; | |
| }, 300); | |
| }, 2500); | |
| } | |
| } | |
| // Show modal | |
| function showModal(title, content) { | |
| const modalOverlay = document.getElementById('modal-overlay'); | |
| const modalContent = document.getElementById('modal-content'); | |
| const modalHTML = ` | |
| <div class="p-6"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h3 class="text-lg font-semibold text-gray-900">${title}</h3> | |
| <button class="p-1 hover:bg-gray-100 rounded-md" id="close-modal"> | |
| <i data-lucide="x" class="w-5 h-5"></i> | |
| </button> | |
| </div> | |
| ${content} | |
| <div class="flex justify-end space-x-3 mt-8"> | |
| <button class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg hover:bg-gray-50 transition-colors" id="cancel-modal"> | |
| Cancel | |
| </button> | |
| <button class="px-4 py-2 bg-[#089e97] text-white rounded-lg hover:bg-[#078b85] transition-colors" id="save-modal"> | |
| Save Changes | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| modalContent.innerHTML = modalHTML; | |
| modalOverlay.classList.remove('hidden'); | |
| // Re-initialize icons | |
| lucide.createIcons(); | |
| // Add event listeners | |
| document.getElementById('close-modal').addEventListener('click', hideModal); | |
| document.getElementById('cancel-modal').addEventListener('click', hideModal); | |
| document.getElementById('save-modal').addEventListener('click', () => { | |
| showToast('Settings saved successfully'); | |
| hideModal(); | |
| }); | |
| } | |
| // Hide modal | |
| function hideModal() { | |
| document.getElementById('modal-overlay').classList.add('hidden'); | |
| } | |
| // Enterprise Configuration System - Settings Detail Page V3 | |
| const configSystem = { | |
| availability: { | |
| title: 'Availability Rules', | |
| description: 'Configure schedules, time blocks, and recurring availability patterns for optimal patient flow.', | |
| status: 'needs-setup', | |
| statusText: 'Needs Setup', | |
| modules: [ | |
| { | |
| id: 'working-hours', | |
| title: 'Working Hours', | |
| description: 'Set standard operating hours for each day of the week', | |
| icon: 'clock', | |
| fields: [ | |
| { type: 'toggle', id: 'monday-enabled', label: 'Monday', default: true }, | |
| { type: 'time-range', id: 'monday-hours', label: 'Hours', start: '09:00', end: '17:00' }, | |
| { type: 'toggle', id: 'tuesday-enabled', label: 'Tuesday', default: true }, | |
| { type: 'time-range', id: 'tuesday-hours', label: 'Hours', start: '09:00', end: '17:00' }, | |
| { type: 'toggle', id: 'wednesday-enabled', label: 'Wednesday', default: true }, | |
| { type: 'time-range', id: 'wednesday-hours', label: 'Hours', start: '09:00', end: '17:00' }, | |
| { type: 'toggle', id: 'thursday-enabled', label: 'Thursday', default: true }, | |
| { type: 'time-range', id: 'thursday-hours', label: 'Hours', start: '09:00', end: '17:00' }, | |
| { type: 'toggle', id: 'friday-enabled', label: 'Friday', default: true }, | |
| { type: 'time-range', id: 'friday-hours', label: 'Hours', start: '09:00', end: '17:00' }, | |
| { type: 'toggle', id: 'saturday-enabled', label: 'Saturday', default: false }, | |
| { type: 'toggle', id: 'sunday-enabled', label: 'Sunday', default: false } | |
| ], | |
| warning: 'Short lunch breaks may cause provider fatigue. Consider 60-minute midday breaks.' | |
| }, | |
| { | |
| id: 'time-blocks', | |
| title: 'Time Blocks & Buffers', | |
| description: 'Configure appointment duration defaults and buffer times between visits', | |
| icon: 'hourglass', | |
| fields: [ | |
| { type: 'select', id: 'default-duration', label: 'Default Appointment Duration', options: ['15 min', '30 min', '45 min', '60 min'], default: '30 min' }, | |
| { type: 'select', id: 'buffer-time', label: 'Buffer Between Appointments', options: ['0 min', '5 min', '10 min', '15 min', '30 min'], default: '10 min' }, | |
| { type: 'select', id: 'lunch-buffer', label: 'Lunch Break Duration', options: ['30 min', '45 min', '60 min', '90 min'], default: '60 min' }, | |
| { type: 'toggle', id: 'enforce-buffers', label: 'Strictly enforce buffer times', default: true } | |
| ], | |
| suggestion: 'Adding 15-minute buffers reduces scheduling conflicts by 45%.' | |
| }, | |
| { | |
| id: 'blackout-dates', | |
| title: 'Blackout Dates', | |
| description: 'Block specific dates for holidays, training, or facility closures', | |
| icon: 'calendar-x', | |
| type: 'blackout-list', | |
| fields: [ | |
| { type: 'date-list', id: 'blackout-dates', label: 'Blocked Dates', items: ['2024-12-25', '2024-12-26', '2025-01-01'] } | |
| ], | |
| action: { label: 'Add Blackout Date', icon: 'plus' } | |
| }, | |
| { | |
| id: 'recurring-rules', | |
| title: 'Recurring Availability Rules', | |
| description: 'Set up templates for provider schedules that repeat weekly', | |
| icon: 'repeat', | |
| fields: [ | |
| { type: 'toggle', id: 'auto-optimize', label: 'AI Auto-Optimization', description: 'Let AI adjust availability based on demand patterns', default: false }, | |
| { type: 'toggle', id: 'dynamic-scheduling', label: 'Dynamic Scheduling', description: 'Automatically extend hours during high-demand periods', default: false }, | |
| { type: 'number', id: 'max-daily-appointments', label: 'Max Daily Appointments per Provider', default: 16, min: 1, max: 40 } | |
| ], | |
| aiInsight: 'Enable AI Auto-Optimization to increase booking capacity by up to 23%.' | |
| } | |
| ], | |
| aiRecommendations: [ | |
| { id: 'buffer-optimization', title: 'Increase Buffer Times', description: 'Current 5-min buffers causing 12% overlap rate', impact: '+45% fewer conflicts', action: 'Apply' }, | |
| { id: 'extend-friday', title: 'Extend Friday Hours', description: 'High demand detected on Friday afternoons', impact: '+8 appointments/week', action: 'Apply' }, | |
| { id: 'add-saturday', title: 'Enable Saturday Mornings', description: 'Patient survey shows 34% want weekend slots', impact: '+15% patient satisfaction', action: 'Review' } | |
| ], | |
| tests: [ | |
| { id: 'booking-simulation', name: 'Test Booking Flow', description: 'Simulate patient booking experience' }, | |
| { id: 'conflict-check', name: 'Check Conflicts', description: 'Identify scheduling overlaps' }, | |
| { id: 'capacity-analysis', name: 'Capacity Analysis', description: 'Analyze max daily booking potential' } | |
| ] | |
| }, | |
| providers: { | |
| title: 'Provider Settings', | |
| description: 'Configure provider profiles, permissions, scheduling preferences, and workflow automation.', | |
| status: 'optimized', | |
| statusText: 'Optimized', | |
| modules: [ | |
| { | |
| id: 'provider-profiles', | |
| title: 'Provider Profiles', | |
| description: 'Manage provider information and default settings', | |
| icon: 'user-check', | |
| fields: [ | |
| { type: 'select', id: 'default-role', label: 'Default Provider Role', options: ['Primary Care Physician', 'Specialist', 'Nurse Practitioner', 'Physician Assistant'], default: 'Primary Care Physician' }, | |
| { type: 'toggle', id: 'auto-accept', label: 'Auto-accept routine appointments', default: true }, | |
| { type: 'toggle', id: 'require-approval', label: 'Require approval for new patient appointments', default: false } | |
| ] | |
| }, | |
| { | |
| id: 'permissions', | |
| title: 'Permissions & Access', | |
| description: 'Control what providers can see and modify', | |
| icon: 'shield', | |
| fields: [ | |
| { type: 'select', id: 'calendar-access', label: 'Calendar Access Level', options: ['Own Only', 'Department', 'All Providers'], default: 'Department' }, | |
| { type: 'select', id: 'patient-access', label: 'Patient Record Access', options: ['Assigned Only', 'All Patients', 'Read-Only'], default: 'Assigned Only' }, | |
| { type: 'toggle', id: 'can-modify-schedule', label: 'Can modify own schedule', default: true }, | |
| { type: 'toggle', id: 'can-view-revenue', label: 'Can view revenue reports', default: false } | |
| ] | |
| }, | |
| { | |
| id: 'notifications', | |
| title: 'Notification Preferences', | |
| description: 'Configure how providers receive alerts and updates', | |
| icon: 'bell', | |
| fields: [ | |
| { type: 'toggle', id: 'new-booking-sms', label: 'SMS for new bookings', default: true }, | |
| { type: 'toggle', id: 'cancellation-email', label: 'Email for cancellations', default: true }, | |
| { type: 'toggle', id: 'reminder-push', label: 'Push notifications for reminders', default: false }, | |
| { type: 'select', id: 'digest-frequency', label: 'Daily Digest Frequency', options: ['Never', 'Morning', 'Evening', 'Both'], default: 'Morning' } | |
| ] | |
| } | |
| ], | |
| aiRecommendations: [ | |
| { id: 'auto-scheduling', title: 'Enable Auto-Scheduling', description: 'Let AI optimize provider calendars automatically', impact: '+35% efficiency gain', action: 'Enable' }, | |
| { id: 'conflict-prevention', title: 'Conflict Prevention', description: 'Block double-booking and enforce buffer times', impact: 'Zero double bookings', action: 'Apply' } | |
| ], | |
| tests: [ | |
| { id: 'permission-check', name: 'Test Permissions', description: 'Verify access controls work correctly' }, | |
| { id: 'notification-test', name: 'Test Notifications', description: 'Send test alert to providers' } | |
| ] | |
| }, | |
| 'payment-processors': { | |
| title: 'Payment Processors', | |
| description: 'Connect and manage payment gateways for seamless revenue collection.', | |
| status: 'at-risk', | |
| statusText: 'At Risk', | |
| modules: [ | |
| { | |
| id: 'processor-connection', | |
| title: 'Payment Gateway', | |
| description: 'Connect your preferred payment processor', | |
| icon: 'credit-card', | |
| fields: [ | |
| { type: 'select', id: 'processor', label: 'Payment Processor', options: ['Stripe', 'PayPal', 'Square', 'Authorize.net'], default: '' }, | |
| { type: 'text', id: 'api-key', label: 'API Key', placeholder: 'sk_live_...', secure: true }, | |
| { type: 'text', id: 'webhook-url', label: 'Webhook URL', value: 'https://api.verifymc.com/webhooks/payments', readonly: true }, | |
| { type: 'toggle', id: 'test-mode', label: 'Test Mode (Sandbox)', default: true } | |
| ], | |
| warning: 'No payment processor connected. Revenue collection is currently disabled.' | |
| }, | |
| { | |
| id: 'payment-settings', | |
| title: 'Payment Settings', | |
| description: 'Configure payment behavior and options', | |
| icon: 'settings', | |
| fields: [ | |
| { type: 'toggle', id: 'online-payments', label: 'Accept online payments', default: false }, | |
| { type: 'toggle', id: 'auto-charge', label: 'Auto-charge on appointment completion', default: false }, | |
| { type: 'select', id: 'deposit-requirement', label: 'Require Deposit', options: ['None', '25%', '50%', 'Fixed Amount'], default: 'None' }, | |
| { type: 'number', id: 'fixed-deposit-amount', label: 'Fixed Deposit Amount ($)', default: 50, showIf: { field: 'deposit-requirement', value: 'Fixed Amount' } } | |
| ] | |
| }, | |
| { | |
| id: 'payouts', | |
| title: 'Payout Configuration', | |
| description: 'Manage when and how you receive funds', | |
| icon: 'dollar-sign', | |
| fields: [ | |
| { type: 'select', id: 'payout-schedule', label: 'Payout Schedule', options: ['Daily', 'Weekly', 'Monthly', 'Manual'], default: 'Weekly' }, | |
| { type: 'select', id: 'payout-day', label: 'Payout Day (if weekly)', options: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'], default: 'Monday' }, | |
| { type: 'toggle', id: 'instant-payout', label: 'Enable instant payouts (2% fee)', default: false } | |
| ] | |
| } | |
| ], | |
| aiRecommendations: [ | |
| { id: 'connect-stripe', title: 'Connect Stripe Immediately', description: 'Payment processor missing - revenue opportunity loss: $12,400/month', impact: 'Restore revenue flow', action: 'Connect' }, | |
| { id: 'enable-deposits', title: 'Enable 25% Deposits', description: 'Reduce no-shows and secure commitment', impact: '+40% show rate', action: 'Apply' } | |
| ], | |
| tests: [ | |
| { id: 'payment-test', name: 'Test Transaction', description: 'Process $1 test charge' }, | |
| { id: 'webhook-test', name: 'Test Webhooks', description: 'Verify payment notifications' }, | |
| { id: 'refund-test', name: 'Test Refund Flow', description: 'Process test refund' } | |
| ] | |
| }, | |
| notifications: { | |
| title: 'Notifications & Communication', | |
| description: 'Configure automated messages, reminders, and patient communication workflows.', | |
| status: 'optimized', | |
| statusText: 'Optimized', | |
| modules: [ | |
| { | |
| id: 'reminder-settings', | |
| title: 'Appointment Reminders', | |
| description: 'Automated reminders to reduce no-shows', | |
| icon: 'bell', | |
| fields: [ | |
| { type: 'toggle', id: 'sms-reminders', label: 'SMS Reminders', default: true }, | |
| { type: 'toggle', id: 'email-reminders', label: 'Email Reminders', default: true }, | |
| { type: 'select', id: 'reminder-timing', label: 'Reminder Timing', options: ['24 hours', '48 hours', '1 hour', '15 minutes'], default: '24 hours' }, | |
| { type: 'number', id: 'reminder-count', label: 'Number of reminders', default: 2, min: 0, max: 5 } | |
| ], | |
| suggestion: 'Multiple reminders (24h + 1h) reduce no-shows by 70%.' | |
| }, | |
| { | |
| id: 'templates', | |
| title: 'Message Templates', | |
| description: 'Customize communication content', | |
| icon: 'file-text', | |
| fields: [ | |
| { type: 'textarea', id: 'confirmation-template', label: 'Confirmation Message', default: 'Your appointment with {{provider}} is confirmed for {{date}} at {{time}}.' }, | |
| { type: 'textarea', id: 'reminder-template', label: 'Reminder Message', default: 'Reminder: You have an appointment tomorrow at {{time}} with {{provider}}.' }, | |
| { type: 'textarea', id: 'cancellation-template', label: 'Cancellation Message', default: 'Your appointment has been cancelled. Reply to reschedule or call {{phone}}.' } | |
| ] | |
| }, | |
| { | |
| id: 'channels', | |
| title: 'Communication Channels', | |
| description: 'Manage SMS, email, and push notification settings', | |
| icon: 'message-square', | |
| fields: [ | |
| { type: 'toggle', id: 'sms-enabled', label: 'SMS Enabled', default: true }, | |
| { type: 'text', id: 'sms-from', label: 'SMS From Number', value: '+1 (555) 123-4567' }, | |
| { type: 'toggle', id: 'email-enabled', label: 'Email Enabled', default: true }, | |
| { type: 'text', id: 'email-from', label: 'Email From Name', value: 'MediCare Clinic' } | |
| ] | |
| } | |
| ], | |
| aiRecommendations: [ | |
| { id: 'add-followup', title: 'Add Follow-up Messages', description: 'Send post-appointment care instructions automatically', impact: '+25% patient satisfaction', action: 'Enable' }, | |
| { id: 'personalize', title: 'Personalize Message Timing', description: 'AI adjusts reminder timing based on patient behavior', impact: '+15% open rate', action: 'Apply' } | |
| ], | |
| tests: [ | |
| { id: 'send-test-sms', name: 'Send Test SMS', description: 'Verify SMS delivery' }, | |
| { id: 'send-test-email', name: 'Send Test Email', description: 'Verify email delivery' }, | |
| { id: 'spam-check', name: 'Spam Score Check', description: 'Check message deliverability' } | |
| ] | |
| }, | |
| insurance: { | |
| title: 'Insurance Configuration', | |
| description: 'Manage payer networks, eligibility verification, and claims processing.', | |
| status: 'needs-setup', | |
| statusText: 'Needs Setup', | |
| modules: [ | |
| { | |
| id: 'payer-networks', | |
| title: 'Payer Networks', | |
| description: 'Configure accepted insurance providers', | |
| icon: 'shield', | |
| fields: [ | |
| { type: 'multiselect', id: 'insurance-providers', label: 'Accepted Insurance', options: ['Aetna', 'Blue Cross', 'Cigna', 'UnitedHealthcare', 'Medicare', 'Medicaid'], default: [] }, | |
| { type: 'toggle', id: 'verify-eligibility', label: 'Auto-verify eligibility', default: false }, | |
| { type: 'toggle', id: 'out-of-network', label: 'Accept out-of-network', default: true } | |
| ] | |
| }, | |
| { | |
| id: 'claims', | |
| title: 'Claims Processing', | |
| description: 'Configure automated claims submission', | |
| icon: 'file-text', | |
| fields: [ | |
| { type: 'toggle', id: 'auto-submit', label: 'Auto-submit claims', default: false }, | |
| { type: 'select', id: 'submission-timing', label: 'Submission Timing', options: ['Immediately', 'Daily Batch', 'Weekly'], default: 'Daily Batch' }, | |
| { type: 'toggle', id: 'denial-management', label: 'AI Denial Management', description: 'Automatically handle claim denials', default: false } | |
| ] | |
| } | |
| ], | |
| aiRecommendations: [ | |
| { id: 'enable-eligibility', title: 'Enable Real-time Eligibility', description: 'Check insurance before every appointment', impact: '-90% claim denials', action: 'Enable' }, | |
| { id: 'add-payers', title: 'Add Top 5 Local Payers', description: 'Expand network to capture 40% more patients', impact: '+40% patient base', action: 'Review' } | |
| ], | |
| tests: [ | |
| { id: 'eligibility-test', name: 'Test Eligibility Check', description: 'Verify insurance lookup' }, | |
| { id: 'claim-test', name: 'Submit Test Claim', description: 'Process test insurance claim' } | |
| ] | |
| } | |
| }; | |
| // Page header templates for different sections | |
| const pageHeaders = { | |
| providers: { | |
| title: 'Providers Settings', | |
| description: 'Configure provider-related configuration, profile behavior, permissions, scheduling defaults, and workflow preferences.', | |
| breadcrumb: [ | |
| { label: 'Settings', action: () => navigateToSection('dashboard'), clickable: true }, | |
| { label: 'Providers', action: () => navigateToSection('providers'), clickable: false } | |
| ], | |
| actions: ['help', 'ai', 'save'] | |
| }, | |
| 'appointment-types': { | |
| title: 'Appointment Types', | |
| description: 'Define visit categories, durations, color coding, and workflow behavior used across scheduling, booking links, and patient intake flows.', | |
| breadcrumb: [ | |
| { label: 'Settings', action: () => navigateToSection('dashboard'), clickable: true }, | |
| { label: 'Appointment Types', action: null, clickable: false } | |
| ], | |
| actions: ['help', 'ai', 'save'] | |
| }, | |
| organization: { | |
| title: 'Organization Settings', | |
| description: 'Manage your clinic details, branding, locations, and organizational structure.', | |
| breadcrumb: [ | |
| { label: 'Settings', action: () => navigateToSection('dashboard'), clickable: true }, | |
| { label: 'Organization', action: null, clickable: false } | |
| ], | |
| actions: ['help', 'ai', 'save'] | |
| }, | |
| locations: { | |
| title: 'Location Management', | |
| description: 'Configure clinic locations, operating hours, room assignments, and facility settings.', | |
| breadcrumb: [ | |
| { label: 'Settings', action: () => navigateToSection('dashboard'), clickable: true }, | |
| { label: 'Locations', action: null, clickable: false } | |
| ], | |
| actions: ['help', 'ai', 'save'] | |
| }, | |
| users: { | |
| title: 'Users & Permissions', | |
| description: 'Manage staff accounts, role-based access controls, and security permissions.', | |
| breadcrumb: [ | |
| { label: 'Settings', action: () => navigateToSection('dashboard'), clickable: true }, | |
| { label: 'Users & Permissions', action: null, clickable: false } | |
| ], | |
| actions: ['help', 'ai', 'save'] | |
| }, | |
| availability: { | |
| title: 'Availability Rules', | |
| description: 'Set provider schedules, time blocks, blackouts, and recurring availability patterns for optimal patient flow.', | |
| breadcrumb: [ | |
| { label: 'Settings', action: () => navigateToSection('dashboard'), clickable: true }, | |
| { label: 'Availability', action: null, clickable: false } | |
| ], | |
| actions: ['help', 'ai', 'save'], | |
| status: 'not-configured', | |
| statusText: 'Not Configured', | |
| secondaryActions: ['test', 'preview'] | |
| }, | |
| 'payment-processors': { | |
| title: 'Payment Processors', | |
| description: 'Connect and manage Stripe, PayPal, or other payment gateways for revenue collection.', | |
| breadcrumb: [ | |
| { label: 'Settings', action: () => navigateToSection('dashboard'), clickable: true }, | |
| { label: 'Payment Processors', action: null, clickable: false } | |
| ], | |
| actions: ['help', 'ai', 'save'] | |
| }, | |
| billing: { | |
| title: 'Billing & Invoicing', | |
| description: 'Configure invoice templates, payment terms, auto-billing, and financial workflows.', | |
| breadcrumb: [ | |
| { label: 'Settings', action: () => navigateToSection('dashboard'), clickable: true }, | |
| { label: 'Billing & Invoicing', action: null, clickable: false } | |
| ], | |
| actions: ['help', 'ai', 'save'] | |
| }, | |
| insurance: { | |
| title: 'Insurance Configuration', | |
| description: 'Manage payer networks, eligibility verification, and claims processing settings.', | |
| breadcrumb: [ | |
| { label: 'Settings', action: () => navigateToSection('dashboard'), clickable: true }, | |
| { label: 'Insurance', action: null, clickable: false } | |
| ], | |
| actions: ['help', 'ai', 'save'] | |
| }, | |
| notifications: { | |
| title: 'Notifications & Communication', | |
| description: 'Configure automated patient communications, reminders, and messaging workflows.', | |
| breadcrumb: [ | |
| { label: 'Settings', action: () => navigateToSection('dashboard'), clickable: true }, | |
| { label: 'Notifications', action: null, clickable: false } | |
| ], | |
| actions: ['help', 'ai', 'save'] | |
| } | |
| }; | |
| // Unsaved changes tracker | |
| let hasUnsavedChanges = false; | |
| let formChangeListeners = []; | |
| function markUnsaved() { | |
| hasUnsavedChanges = true; | |
| const badge = document.getElementById('unsaved-badge'); | |
| const saveBtn = document.getElementById('header-save-btn'); | |
| if (badge) badge.classList.remove('hidden'); | |
| if (saveBtn) { | |
| saveBtn.classList.add('ring-2', 'ring-[#089e97]', 'ring-offset-2'); | |
| saveBtn.innerHTML = `<i data-lucide="save" class="w-4 h-4 mr-2"></i>Save Changes`; | |
| lucide.createIcons(); | |
| } | |
| } | |
| // Sidebar Collapsible Sections V2 | |
| function toggleSidebarSection(sectionId, forceExpand = false) { | |
| const section = document.querySelector(`.sidebar-section[data-section="${sectionId}"]`); | |
| const content = section?.querySelector('.sidebar-section-content'); | |
| const icon = section?.querySelector('.sidebar-section-icon'); | |
| if (!section || !content || !icon) return; | |
| const isExpanded = content.classList.contains('expanded') || appState.sidebarSectionsState[sectionId]; | |
| if (forceExpand || !isExpanded) { | |
| // Expand this section, collapse others | |
| document.querySelectorAll('.sidebar-section-content').forEach(el => { | |
| el.classList.remove('expanded'); | |
| el.style.display = 'none'; | |
| }); | |
| document.querySelectorAll('.sidebar-section-icon').forEach(el => { | |
| el.classList.remove('expanded'); | |
| }); | |
| // Expand current section | |
| content.classList.add('expanded'); | |
| content.style.display = 'block'; | |
| icon.classList.add('expanded'); | |
| appState.sidebarSectionsState[sectionId] = true; | |
| // Update other sections to false | |
| Object.keys(appState.sidebarSectionsState).forEach(key => { | |
| if (key !== sectionId) { | |
| appState.sidebarSectionsState[key] = false; | |
| } | |
| }); | |
| } else { | |
| // Collapse this section | |
| content.classList.remove('expanded'); | |
| content.style.display = 'none'; | |
| icon.classList.remove('expanded'); | |
| appState.sidebarSectionsState[sectionId] = false; | |
| } | |
| // Save state to localStorage | |
| try { | |
| localStorage.setItem('sidebarSectionsState', JSON.stringify(appState.sidebarSectionsState)); | |
| } catch (e) { | |
| console.log('Could not save sidebar state to localStorage'); | |
| } | |
| } | |
| function initializeSidebarSections() { | |
| // Load saved state from localStorage | |
| try { | |
| const savedState = localStorage.getItem('sidebarSectionsState'); | |
| if (savedState) { | |
| const parsedState = JSON.parse(savedState); | |
| Object.keys(parsedState).forEach(key => { | |
| if (appState.sidebarSectionsState.hasOwnProperty(key)) { | |
| appState.sidebarSectionsState[key] = parsedState[key]; | |
| } | |
| }); | |
| } | |
| } catch (e) { | |
| console.log('Could not load sidebar state from localStorage'); | |
| } | |
| // Apply the state to the UI | |
| Object.keys(appState.sidebarSectionsState).forEach(sectionId => { | |
| if (appState.sidebarSectionsState[sectionId]) { | |
| const content = document.querySelector(`.sidebar-section-content[data-section="${sectionId}"]`); | |
| const icon = document.querySelector(`.sidebar-section-icon[data-section="${sectionId}"]`); | |
| if (content) { | |
| content.classList.add('expanded'); | |
| content.style.display = 'block'; | |
| } | |
| if (icon) { | |
| icon.classList.add('expanded'); | |
| } | |
| } | |
| }); | |
| // Add click handlers to section headers | |
| document.querySelectorAll('.sidebar-section-header').forEach(header => { | |
| header.addEventListener('click', (e) => { | |
| const sectionId = header.dataset.section; | |
| if (sectionId) { | |
| toggleSidebarSection(sectionId); | |
| } | |
| }); | |
| }); | |
| } | |
| function expandSectionForView(sectionId) { | |
| // Map view to section | |
| const viewToSectionMap = { | |
| 'dashboard': 'organization', | |
| 'organization': 'organization', | |
| 'locations': 'organization', | |
| 'providers': 'organization', | |
| 'users': 'organization', | |
| 'availability': 'scheduling', | |
| 'appointment-types': 'scheduling', | |
| 'buffer-time': 'scheduling', | |
| 'booking-rules': 'scheduling', | |
| 'payment-processors': 'payments', | |
| 'billing': 'payments', | |
| 'insurance': 'payments', | |
| 'notifications': 'communication', | |
| 'sms': 'communication', | |
| 'templates': 'communication', | |
| 'ai-scheduling': 'automation', | |
| 'workflows': 'automation', | |
| 'reminders': 'automation', | |
| 'security': 'security', | |
| 'compliance': 'security', | |
| 'audit': 'security' | |
| }; | |
| const targetSection = viewToSectionMap[sectionId]; | |
| if (targetSection) { | |
| toggleSidebarSection(targetSection, true); | |
| } | |
| } | |
| // Module Dependency System (MDS V1) | |
| const moduleData = { | |
| organization: { | |
| title: "Organization System", | |
| description: "Manage clinic details, locations, providers, and organizational structure", | |
| status: "optimized", | |
| completion: 85, | |
| missingDependencies: [ | |
| { | |
| id: "logo-branding", | |
| title: "Logo & Branding", | |
| subtitle: "Upload clinic logo and configure brand colors", | |
| icon: "image", | |
| action: () => navigateToSection('organization'), | |
| actionText: "Configure" | |
| } | |
| ], | |
| recommendedOptimizations: [ | |
| { | |
| id: "advanced-reporting", | |
| title: "Enable Advanced Reporting", | |
| subtitle: "Get detailed insights on provider performance", | |
| icon: "bar-chart-3", | |
| action: () => showToast('Advanced reporting enabled'), | |
| actionText: "Enable" | |
| }, | |
| { | |
| id: "provider-roles", | |
| title: "Define Provider Roles", | |
| subtitle: "Create custom roles for different provider types", | |
| icon: "users", | |
| action: () => navigateToSection('users'), | |
| actionText: "Setup" | |
| } | |
| ], | |
| configuredItems: [ | |
| { | |
| id: "clinic-details", | |
| title: "Clinic Details", | |
| subtitle: "Name, contact info, and tax ID configured", | |
| icon: "building" | |
| }, | |
| { | |
| id: "locations", | |
| title: "Clinic Locations", | |
| subtitle: "3 locations with addresses and hours", | |
| icon: "map-pin" | |
| }, | |
| { | |
| id: "providers", | |
| title: "Provider Directory", | |
| subtitle: "12 providers with profiles", | |
| icon: "user-check" | |
| } | |
| ] | |
| }, | |
| scheduling: { | |
| title: "Scheduling System", | |
| description: "Configure appointment types, availability rules, and booking workflows", | |
| status: "in-progress", | |
| completion: 65, | |
| missingDependencies: [ | |
| { | |
| id: "availability-rules", | |
| title: "Availability Rules", | |
| subtitle: "No availability rules configured for providers", | |
| icon: "clock", | |
| action: () => navigateToSection('availability'), | |
| actionText: "Fix" | |
| }, | |
| { | |
| id: "buffer-time", | |
| title: "Buffer Time", | |
| subtitle: "No buffer time set between appointments", | |
| icon: "shield", | |
| action: () => navigateToSection('buffer-time'), | |
| actionText: "Configure" | |
| } | |
| ], | |
| recommendedOptimizations: [ | |
| { | |
| id: "sms-reminders", | |
| title: "Enable SMS Reminders", | |
| subtitle: "Reduce no-shows by 70% with automated SMS", | |
| icon: "message-square", | |
| action: () => navigateToSection('sms'), | |
| actionText: "Enable" | |
| }, | |
| { | |
| id: "booking-rules", | |
| title: "Optimize Booking Rules", | |
| subtitle: "Set smart rules for patient booking limits", | |
| icon: "list-checks", | |
| action: () => navigateToSection('booking-rules'), | |
| actionText: "Optimize" | |
| } | |
| ], | |
| configuredItems: [ | |
| { | |
| id: "appointment-types", | |
| title: "Appointment Types", | |
| subtitle: "5 appointment types with durations", | |
| icon: "calendar" | |
| }, | |
| { | |
| id: "calendar-sync", | |
| title: "Calendar Sync", | |
| subtitle: "Google Calendar integration active", | |
| icon: "refresh-cw" | |
| } | |
| ] | |
| }, | |
| payments: { | |
| title: "Payments System", | |
| description: "Configure payment processors, billing, and insurance integration", | |
| status: "not-configured", | |
| completion: 25, | |
| missingDependencies: [ | |
| { | |
| id: "payment-processor", | |
| title: "Payment Processor", | |
| subtitle: "No payment processor connected", | |
| icon: "credit-card", | |
| action: () => navigateToSection('payment-processors'), | |
| actionText: "Setup" | |
| }, | |
| { | |
| id: "insurance-setup", | |
| title: "Insurance Setup", | |
| subtitle: "Insurance networks not configured", | |
| icon: "shield", | |
| action: () => navigateToSection('insurance'), | |
| actionText: "Configure" | |
| } | |
| ], | |
| recommendedOptimizations: [ | |
| { | |
| id: "auto-invoicing", | |
| title: "Enable Auto-Invoicing", | |
| subtitle: "Automatically generate and send invoices", | |
| icon: "file-text", | |
| action: () => showToast('Auto-invoicing enabled'), | |
| actionText: "Enable" | |
| }, | |
| { | |
| id: "payment-plans", | |
| title: "Set Up Payment Plans", | |
| subtitle: "Allow patients to pay in installments", | |
| icon: "dollar-sign", | |
| action: () => showToast('Payment plans configured'), | |
| actionText: "Setup" | |
| } | |
| ], | |
| configuredItems: [ | |
| { | |
| id: "billing-setup", | |
| title: "Billing Setup", | |
| subtitle: "Basic billing configuration complete", | |
| icon: "receipt" | |
| } | |
| ] | |
| }, | |
| notifications: { | |
| title: "Notifications System", | |
| description: "Configure automated communications, reminders, and messaging workflows", | |
| status: "optimized", | |
| completion: 92, | |
| missingDependencies: [], | |
| recommendedOptimizations: [ | |
| { | |
| id: "personalized-timing", | |
| title: "Personalized Timing", | |
| subtitle: "AI adjusts reminder timing per patient", | |
| icon: "zap", | |
| action: () => showToast('Personalized timing enabled'), | |
| actionText: "Apply" | |
| } | |
| ], | |
| configuredItems: [ | |
| { | |
| id: "sms-templates", | |
| title: "SMS Templates", | |
| subtitle: "5 SMS templates for different scenarios", | |
| icon: "message-square" | |
| }, | |
| { | |
| id: "email-templates", | |
| title: "Email Templates", | |
| subtitle: "8 email templates configured", | |
| icon: "mail" | |
| }, | |
| { | |
| id: "appointment-reminders", | |
| title: "Appointment Reminders", | |
| subtitle: "24h & 1h reminders active", | |
| icon: "bell" | |
| }, | |
| { | |
| id: "automated-followups", | |
| title: "Automated Follow-ups", | |
| subtitle: "Post-appointment follow-ups enabled", | |
| icon: "repeat" | |
| } | |
| ] | |
| }, | |
| security: { | |
| title: "Security & Compliance", | |
| description: "Configure HIPAA compliance, security settings, and audit logging", | |
| status: "in-progress", | |
| completion: 78, | |
| missingDependencies: [ | |
| { | |
| id: "audit-logs", | |
| title: "Audit Logs", | |
| subtitle: "Full audit logging not enabled", | |
| icon: "history", | |
| action: () => navigateToSection('audit'), | |
| actionText: "Enable" | |
| } | |
| ], | |
| recommendedOptimizations: [ | |
| { | |
| id: "two-factor-auth", | |
| title: "Enable Two-Factor Auth", | |
| subtitle: "Add extra security layer for all users", | |
| icon: "shield", | |
| action: () => showToast('2FA enabled'), | |
| actionText: "Enable" | |
| }, | |
| { | |
| id: "data-retention", | |
| title: "Data Retention Policy", | |
| subtitle: "Set automated data cleanup rules", | |
| icon: "trash-2", | |
| action: () => navigateToSection('compliance'), | |
| actionText: "Setup" | |
| } | |
| ], | |
| configuredItems: [ | |
| { | |
| id: "hipaa-compliance", | |
| title: "HIPAA Compliance", | |
| subtitle: "Basic HIPAA settings configured", | |
| icon: "clipboard-check" | |
| }, | |
| { | |
| id: "user-permissions", | |
| title: "User Permissions", | |
| subtitle: "Role-based access control active", | |
| icon: "lock" | |
| }, | |
| { | |
| id: "data-encryption", | |
| title: "Data Encryption", | |
| subtitle: "End-to-end encryption enabled", | |
| icon: "key" | |
| } | |
| ] | |
| } | |
| }; | |
| function openModulePanel(moduleId) { | |
| const module = moduleData[moduleId]; | |
| if (!module) return; | |
| // Update panel content | |
| document.getElementById('module-panel-title').textContent = module.title; | |
| document.getElementById('module-description').textContent = module.description; | |
| document.getElementById('module-completion').textContent = module.completion + '%'; | |
| document.getElementById('module-progress-bar').style.width = module.completion + '%'; | |
| // Update status badge | |
| const statusBadge = document.getElementById('module-status-badge'); | |
| statusBadge.className = 'status-badge'; | |
| statusBadge.textContent = module.status === 'optimized' ? 'Optimized' : | |
| module.status === 'in-progress' ? 'In Progress' : 'Not Configured'; | |
| statusBadge.classList.add(module.status === 'optimized' ? 'status-optimized' : | |
| module.status === 'in-progress' ? 'status-in-progress' : 'status-not-configured'); | |
| // Render missing dependencies | |
| const missingContainer = document.getElementById('missing-dependencies'); | |
| if (module.missingDependencies.length > 0) { | |
| missingContainer.innerHTML = module.missingDependencies.map(item => ` | |
| <div class="dependency-item missing"> | |
| <div class="dependency-item-header"> | |
| <div class="dependency-item-icon missing"> | |
| <i data-lucide="${item.icon}" class="w-4 h-4"></i> | |
| </div> | |
| <div class="dependency-item-content"> | |
| <div class="dependency-item-title">${item.title}</div> | |
| <div class="dependency-item-subtitle">${item.subtitle}</div> | |
| </div> | |
| </div> | |
| <div class="dependency-item-action"> | |
| <button onclick="${item.action.toString().replace('() => ', '')}" class="px-3 py-1.5 text-xs font-medium text-white bg-red-500 hover:bg-red-600 rounded-lg transition-colors"> | |
| ${item.actionText} | |
| </button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } else { | |
| missingContainer.innerHTML = ` | |
| <div class="empty-state"> | |
| <div class="empty-state-icon"> | |
| <i data-lucide="check-circle" class="w-6 h-6"></i> | |
| </div> | |
| <p>No missing dependencies! All critical items are configured.</p> | |
| </div> | |
| `; | |
| } | |
| // Render recommended optimizations | |
| const recommendationsContainer = document.getElementById('recommended-optimizations'); | |
| if (module.recommendedOptimizations.length > 0) { | |
| recommendationsContainer.innerHTML = module.recommendedOptimizations.map(item => ` | |
| <div class="dependency-item recommended"> | |
| <div class="dependency-item-header"> | |
| <div class="dependency-item-icon recommended"> | |
| <i data-lucide="${item.icon}" class="w-4 h-4"></i> | |
| </div> | |
| <div class="dependency-item-content"> | |
| <div class="dependency-item-title">${item.title}</div> | |
| <div class="dependency-item-subtitle">${item.subtitle}</div> | |
| </div> | |
| </div> | |
| <div class="dependency-item-action"> | |
| <button onclick="${item.action.toString().replace('() => ', '')}" class="px-3 py-1.5 text-xs font-medium text-white bg-blue-500 hover:bg-blue-600 rounded-lg transition-colors"> | |
| ${item.actionText} | |
| </button> | |
| <button onclick="autoApplyOptimization('${item.id}')" class="px-3 py-1.5 text-xs font-medium text-gray-700 bg-white border border-gray-300 hover:bg-gray-50 rounded-lg transition-colors flex items-center"> | |
| <i data-lucide="zap" class="w-3 h-3 mr-1"></i> | |
| Auto | |
| </button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } else { | |
| recommendationsContainer.innerHTML = ` | |
| <div class="empty-state"> | |
| <div class="empty-state-icon"> | |
| <i data-lucide="zap" class="w-6 h-6"></i> | |
| </div> | |
| <p>No optimizations recommended at this time.</p> | |
| </div> | |
| `; | |
| } | |
| // Render configured items | |
| const configuredContainer = document.getElementById('configured-items'); | |
| if (module.configuredItems.length > 0) { | |
| configuredContainer.innerHTML = module.configuredItems.map(item => ` | |
| <div class="dependency-item configured"> | |
| <div class="dependency-item-header"> | |
| <div class="dependency-item-icon configured"> | |
| <i data-lucide="${item.icon}" class="w-4 h-4"></i> | |
| </div> | |
| <div class="dependency-item-content"> | |
| <div class="dependency-item-title">${item.title}</div> | |
| <div class="dependency-item-subtitle">${item.subtitle}</div> | |
| </div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } else { | |
| configuredContainer.innerHTML = ` | |
| <div class="empty-state"> | |
| <div class="empty-state-icon"> | |
| <i data-lucide="settings" class="w-6 h-6"></i> | |
| </div> | |
| <p>No items configured yet.</p> | |
| </div> | |
| `; | |
| } | |
| // Show panel | |
| document.getElementById('module-panel-overlay').classList.add('active'); | |
| document.getElementById('module-panel').classList.add('active'); | |
| // Reinitialize icons | |
| setTimeout(() => lucide.createIcons(), 100); | |
| } | |
| function closeModulePanel() { | |
| document.getElementById('module-panel-overlay').classList.remove('active'); | |
| document.getElementById('module-panel').classList.remove('active'); | |
| } | |
| function fixAllModuleIssues() { | |
| showToast('Fixing all module issues...'); | |
| // In a real app, this would trigger automatic fixes | |
| setTimeout(() => { | |
| showToast('All issues fixed successfully!'); | |
| closeModulePanel(); | |
| }, 1500); | |
| } | |
| function autoConfigureModule() { | |
| showToast('AI is auto-configuring the module...'); | |
| // In a real app, this would use AI to configure optimal settings | |
| setTimeout(() => { | |
| showToast('Module auto-configured with AI recommendations!'); | |
| }, 2000); | |
| } | |
| function testModuleFlow() { | |
| showToast('Testing module flow...'); | |
| // In a real app, this would run integration tests | |
| setTimeout(() => { | |
| showToast('Module flow test completed successfully!'); | |
| }, 1500); | |
| } | |
| function autoApplyOptimization(optimizationId) { | |
| showToast(`AI is applying optimization: ${optimizationId}`); | |
| // In a real app, this would apply the optimization automatically | |
| setTimeout(() => { | |
| showToast('Optimization applied successfully!'); | |
| }, 1000); | |
| } | |
| function clearUnsaved() { | |
| hasUnsavedChanges = false; | |
| const badge = document.getElementById('unsaved-badge'); | |
| const saveBtn = document.getElementById('header-save-btn'); | |
| if (badge) badge.classList.add('hidden'); | |
| if (saveBtn) { | |
| saveBtn.classList.remove('ring-2', 'ring-[#089e97]', 'ring-offset-2'); | |
| } | |
| } | |
| function setupFormChangeDetection() { | |
| // Remove old listeners | |
| formChangeListeners.forEach(({ element, handler }) => { | |
| element.removeEventListener('change', handler); | |
| element.removeEventListener('input', handler); | |
| }); | |
| formChangeListeners = []; | |
| // Add new listeners | |
| const inputs = document.querySelectorAll('#settings-form input, #settings-form select, #settings-form textarea'); | |
| inputs.forEach(input => { | |
| const handler = () => markUnsaved(); | |
| input.addEventListener('change', handler); | |
| input.addEventListener('input', handler); | |
| formChangeListeners.push({ element: input, handler }); | |
| }); | |
| } | |
| function renderPremiumHeader(headerConfig) { | |
| const breadcrumbHTML = headerConfig.breadcrumb.map((crumb, index) => { | |
| const isLast = index === headerConfig.breadcrumb.length - 1; | |
| const arrow = index > 0 ? `<span class="mx-2 text-gray-300">/</span>` : ''; | |
| const baseClasses = "text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:ring-offset-1 rounded px-1 -mx-1"; | |
| if (isLast) { | |
| return `${arrow}<span class="${baseClasses} text-gray-900 cursor-default" aria-current="page">${crumb.label}</span>`; | |
| } else { | |
| return `${arrow}<button onclick="${crumb.clickable ? crumb.action.toString().replace('() => navigateToSection', 'navigateToSection') : ''}" | |
| class="${baseClasses} text-gray-500 hover:text-[#089e97]" | |
| ${!crumb.clickable ? 'disabled' : ''}>${crumb.label}</button>`; | |
| } | |
| }).join(''); | |
| const actionsHTML = headerConfig.actions.map(action => { | |
| switch(action) { | |
| case 'help': | |
| return `<button onclick="openContextHelp()" class="flex items-center px-3 py-2 text-sm font-medium text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:ring-offset-1" aria-label="Get help"> | |
| <i data-lucide="help-circle" class="w-4 h-4 mr-2"></i> | |
| Help | |
| </button>`; | |
| case 'ai': | |
| return `<button onclick="openAICopilot()" class="flex items-center px-3 py-2 text-sm font-medium text-[#089e97] hover:bg-[#089e97]/10 rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:ring-offset-1 group" aria-label="AI Assistant"> | |
| <i data-lucide="sparkles" class="w-4 h-4 mr-2 group-hover:animate-pulse"></i> | |
| AI Copilot | |
| </button>`; | |
| case 'save': | |
| return `<button id="header-save-btn" onclick="saveSettings()" class="flex items-center px-4 py-2 text-sm font-medium text-white bg-[#089e97] hover:bg-[#078b85] rounded-lg shadow-sm transition-all focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:ring-offset-2" aria-label="Save changes"> | |
| <i data-lucide="save" class="w-4 h-4 mr-2"></i> | |
| Save Changes | |
| </button>`; | |
| default: | |
| return ''; | |
| } | |
| }).join(''); | |
| return ` | |
| <div class="sticky top-0 z-30 bg-white/80 backdrop-blur-xl border-b border-gray-200/80 px-8 py-5 transition-all duration-300" id="page-header"> | |
| <div class="flex items-start justify-between"> | |
| <div class="flex-1 min-w-0"> | |
| <!-- Breadcrumb --> | |
| <nav aria-label="Breadcrumb" class="mb-2"> | |
| <ol class="flex items-center flex-wrap"> | |
| ${breadcrumbHTML} | |
| </ol> | |
| </nav> | |
| <!-- Title Block --> | |
| <div> | |
| <h1 class="text-2xl font-bold text-gray-900 tracking-tight">${headerConfig.title}</h1> | |
| <p class="mt-1 text-sm text-gray-500 max-w-2xl leading-relaxed">${headerConfig.description}</p> | |
| </div> | |
| <!-- Unsaved Changes Indicator --> | |
| <div id="unsaved-badge" class="hidden mt-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-50 text-amber-700 border border-amber-200"> | |
| <span class="w-1.5 h-1.5 bg-amber-500 rounded-full mr-1.5 animate-pulse"></span> | |
| Unsaved changes | |
| </div> | |
| </div> | |
| <!-- Action Cluster --> | |
| <div class="flex items-center space-x-2 ml-4 flex-shrink-0"> | |
| ${actionsHTML} | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function openContextHelp() { | |
| showModal('Help: Provider Settings', ` | |
| <div class="space-y-4"> | |
| <div class="p-4 bg-gray-50 rounded-lg"> | |
| <h4 class="font-medium text-gray-900 mb-2 flex items-center"> | |
| <i data-lucide="info" class="w-4 h-4 mr-2 text-[#089e97]"></i> | |
| About Provider Settings | |
| </h4> | |
| <p class="text-sm text-gray-600">Configure how providers interact with the system, their default availability, notification preferences, and scheduling permissions.</p> | |
| </div> | |
| <div class="space-y-2"> | |
| <h4 class="font-medium text-gray-900 text-sm">Quick Links</h4> | |
| <button onclick="hideModal(); navigateToSection('availability')" class="w-full text-left p-3 hover:bg-gray-50 rounded-lg flex items-center text-sm text-gray-700 transition-colors"> | |
| <i data-lucide="clock" class="w-4 h-4 mr-3 text-gray-400"></i> | |
| View Availability Rules | |
| </button> | |
| <button onclick="hideModal(); navigateToSection('appointment-types')" class="w-full text-left p-3 hover:bg-gray-50 rounded-lg flex items-center text-sm text-gray-700 transition-colors"> | |
| <i data-lucide="calendar" class="w-4 h-4 mr-3 text-gray-400"></i> | |
| Manage Appointment Types | |
| </button> | |
| </div> | |
| </div> | |
| `); | |
| lucide.createIcons(); | |
| } | |
| function openAICopilot() { | |
| const aiActions = [ | |
| { label: 'Review provider setup', icon: 'clipboard-check', action: () => { hideModal(); showToast('AI is reviewing provider configuration...'); } }, | |
| { label: 'Recommend best defaults', icon: 'zap', action: () => { hideModal(); applyAIRecommendations(); } }, | |
| { label: 'Audit scheduling settings', icon: 'search', action: () => { hideModal(); showToast('AI audit started...'); } }, | |
| { label: 'Explain this setting', icon: 'help-circle', action: () => { hideModal(); showToast('Select a setting to explain'); } } | |
| ]; | |
| const actionsHTML = aiActions.map((action, idx) => ` | |
| <button onclick="(${action.action.toString()})()" class="w-full text-left p-4 hover:bg-[#089e97]/5 rounded-xl flex items-center group transition-all border border-transparent hover:border-[#089e97]/20"> | |
| <div class="w-10 h-10 bg-[#089e97]/10 rounded-lg flex items-center justify-center mr-4 group-hover:bg-[#089e97] transition-colors"> | |
| <i data-lucide="${action.icon}" class="w-5 h-5 text-[#089e97] group-hover:text-white transition-colors"></i> | |
| </div> | |
| <div> | |
| <div class="font-medium text-gray-900 text-sm">${action.label}</div> | |
| <div class="text-xs text-gray-500 mt-0.5">AI-powered assistance</div> | |
| </div> | |
| <i data-lucide="arrow-right" class="w-4 h-4 text-gray-300 ml-auto group-hover:text-[#089e97] transition-colors"></i> | |
| </button> | |
| `).join(''); | |
| showModal('AI Copilot', ` | |
| <div class="space-y-2"> | |
| <p class="text-sm text-gray-600 mb-4">Select an AI-powered action to optimize your provider settings:</p> | |
| ${actionsHTML} | |
| </div> | |
| `); | |
| lucide.createIcons(); | |
| } | |
| function applyAIRecommendations() { | |
| const settings = { | |
| 'setting-provider-notifications': true, | |
| 'setting-auto-scheduling': true, | |
| 'setting-conflict-prevention': true | |
| }; | |
| Object.keys(settings).forEach(id => { | |
| const el = document.getElementById(id); | |
| if (el) el.checked = settings[id]; | |
| }); | |
| markUnsaved(); | |
| showToast('AI has applied recommended settings. Review and save.'); | |
| } | |
| function saveSettings() { | |
| // GIS V1: Immediate Action Feedback | |
| const btn = document.getElementById('header-save-btn'); | |
| if (btn) applyActionFeedback(btn); | |
| // Simulate processing | |
| setTimeout(() => { | |
| clearUnsaved(); | |
| showToast('Changes saved successfully'); | |
| // Success state on save button | |
| if (btn) { | |
| btn.innerHTML = `<i data-lucide="check" class="w-4 h-4 mr-2"></i>Saved`; | |
| btn.classList.add('bg-green-600'); | |
| lucide.createIcons(); | |
| setTimeout(() => { | |
| btn.innerHTML = `<i data-lucide="save" class="w-4 h-4 mr-2"></i>Save Changes`; | |
| btn.classList.remove('bg-green-600'); | |
| lucide.createIcons(); | |
| }, 2000); | |
| } | |
| }, 400); // 400ms simulated delay for feedback feel | |
| } | |
| // Appointment Types specific functions | |
| function saveAppointmentTypes() { | |
| clearUnsaved(); | |
| showToast('Appointment types configuration saved'); | |
| // Visual feedback on save button | |
| const btn = document.getElementById('header-save-btn'); | |
| if (btn) { | |
| const originalHTML = btn.innerHTML; | |
| btn.innerHTML = `<i data-lucide="check" class="w-4 h-4 mr-2"></i>Saved`; | |
| btn.classList.add('bg-green-600'); | |
| btn.classList.remove('bg-[#089e97]', 'hover:bg-[#078b85]'); | |
| lucide.createIcons(); | |
| setTimeout(() => { | |
| btn.innerHTML = `<i data-lucide="save" class="w-4 h-4 mr-2"></i>Save Changes`; | |
| btn.classList.remove('bg-green-600'); | |
| btn.classList.add('bg-[#089e97]', 'hover:bg-[#078b85]'); | |
| lucide.createIcons(); | |
| }, 2000); | |
| } | |
| } | |
| function openAddAppointmentTypeModal() { | |
| showModal('Add Appointment Type', ` | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Appointment Name *</label> | |
| <input type="text" id="new-type-name" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent" placeholder="e.g., Annual Physical"> | |
| </div> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Duration (minutes) *</label> | |
| <select id="new-type-duration" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent"> | |
| <option value="15">15 minutes</option> | |
| <option value="30" selected>30 minutes</option> | |
| <option value="45">45 minutes</option> | |
| <option value="60">60 minutes</option> | |
| <option value="90">90 minutes</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Color Code</label> | |
| <div class="flex items-center space-x-2"> | |
| <input type="color" id="new-type-color" value="#089e97" class="h-10 w-16 border border-gray-300 rounded-lg cursor-pointer"> | |
| <span class="text-sm text-gray-500">Used in calendar</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Description</label> | |
| <textarea id="new-type-description" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent resize-none" placeholder="Description shown to patients during booking..."></textarea> | |
| </div> | |
| <div> | |
| <label class="flex items-center"> | |
| <input type="checkbox" id="new-type-requires-intake" class="h-4 w-4 text-[#089e97] focus:ring-[#089e97] border-gray-300 rounded" checked> | |
| <span class="ml-2 text-sm text-gray-700">Require intake form</span> | |
| </label> | |
| </div> | |
| <div> | |
| <label class="flex items-center"> | |
| <input type="checkbox" id="new-type-allow-online" class="h-4 w-4 text-[#089e97] focus:ring-[#089e97] border-gray-300 rounded" checked> | |
| <span class="ml-2 text-sm text-gray-700">Allow online booking</span> | |
| </label> | |
| </div> | |
| </div> | |
| `); | |
| // Override the save button behavior for this modal | |
| setTimeout(() => { | |
| const saveBtn = document.getElementById('save-modal'); | |
| if (saveBtn) { | |
| saveBtn.onclick = () => { | |
| const name = document.getElementById('new-type-name')?.value; | |
| if (!name) { | |
| showToast('Please enter an appointment name'); | |
| return; | |
| } | |
| hideModal(); | |
| showToast(`Created appointment type: ${name}`); | |
| markUnsaved(); | |
| }; | |
| } | |
| }, 100); | |
| } | |
| function openAIOptimizeModal() { | |
| const optimizations = [ | |
| { id: 'buffer-optimization', name: 'Smart Buffer Times', description: 'Add 15min buffers between high-stress appointments', impact: '+28% provider satisfaction', selected: true }, | |
| { id: 'duration-optimization', name: 'Duration Calibration', description: 'Adjust durations based on historical data', impact: '+12% schedule efficiency', selected: true }, | |
| { id: 'color-optimization', name: 'Color Psychology', description: 'Optimize color coding for patient perception', impact: 'Better patient experience', selected: false }, | |
| { id: 'name-optimization', name: 'Patient-Friendly Names', description: 'Simplify clinical terminology in booking flows', impact: '+18% booking completion', selected: true } | |
| ]; | |
| const optimizationsHTML = optimizations.map(opt => ` | |
| <label class="flex items-start p-4 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors ${opt.selected ? 'border-[#089e97] bg-[#089e97]/5' : ''}"> | |
| <input type="checkbox" class="mt-1 h-4 w-4 text-[#089e97] focus:ring-[#089e97] border-gray-300 rounded" ${opt.selected ? 'checked' : ''}> | |
| <div class="ml-3 flex-1"> | |
| <div class="flex items-center justify-between"> | |
| <span class="font-medium text-gray-900">${opt.name}</span> | |
| <span class="text-xs font-medium text-green-600 bg-green-50 px-2 py-1 rounded-full">${opt.impact}</span> | |
| </div> | |
| <p class="text-sm text-gray-500 mt-1">${opt.description}</p> | |
| </div> | |
| </label> | |
| `).join(''); | |
| showModal('AI Optimization', ` | |
| <div class="space-y-4"> | |
| <div class="p-4 bg-gradient-to-r from-purple-50 to-blue-50 rounded-lg border border-purple-200"> | |
| <div class="flex items-center mb-2"> | |
| <i data-lucide="sparkles" class="w-5 h-5 text-purple-600 mr-2"></i> | |
| <span class="font-semibold text-gray-900">AI Analysis Complete</span> | |
| </div> | |
| <p class="text-sm text-gray-600">Based on your scheduling patterns and provider feedback, AI recommends these optimizations:</p> | |
| </div> | |
| <div class="space-y-3"> | |
| ${optimizationsHTML} | |
| </div> | |
| </div> | |
| `); | |
| setTimeout(() => { | |
| const saveBtn = document.getElementById('save-modal'); | |
| if (saveBtn) { | |
| saveBtn.textContent = 'Apply Optimizations'; | |
| saveBtn.onclick = () => { | |
| hideModal(); | |
| showToast('AI optimizations applied successfully'); | |
| markUnsaved(); | |
| }; | |
| } | |
| }, 100); | |
| } | |
| function editAppointmentType(index) { | |
| showToast(`Editing appointment type ${index + 1}`); | |
| } | |
| function duplicateAppointmentType(index) { | |
| showToast('Appointment type duplicated'); | |
| markUnsaved(); | |
| } | |
| function deleteAppointmentType(index) { | |
| if (confirm('Are you sure you want to delete this appointment type? This will affect existing bookings.')) { | |
| showToast('Appointment type deleted'); | |
| markUnsaved(); | |
| } | |
| } | |
| function applyBufferRecommendation() { | |
| const bufferSelect = document.getElementById('setting-buffer-time'); | |
| if (bufferSelect) { | |
| bufferSelect.value = '15 minutes'; | |
| markUnsaved(); | |
| showToast('AI recommendation applied: 15-minute buffers enabled'); | |
| } | |
| } | |
| // Availability Page Functions | |
| function initializeAvailabilityPage() { | |
| // Set up event listeners for all inputs to mark unsaved | |
| document.querySelectorAll('#settings-form input, #settings-form select').forEach(input => { | |
| input.addEventListener('change', () => markUnsaved()); | |
| }); | |
| } | |
| function toggleAllDays() { | |
| const checkboxes = document.querySelectorAll('.toggle-day'); | |
| const allEnabled = Array.from(checkboxes).every(cb => cb.checked); | |
| checkboxes.forEach(cb => { | |
| cb.checked = !allEnabled; | |
| toggleDayRow(cb); | |
| }); | |
| showToast(allEnabled ? 'All days disabled' : 'All days enabled'); | |
| markUnsaved(); | |
| } | |
| function toggleDayRow(checkbox) { | |
| const dayRow = checkbox.closest('.day-row'); | |
| const timeInputs = dayRow.querySelectorAll('input[type="time"]'); | |
| timeInputs.forEach(input => { | |
| input.disabled = !checkbox.checked; | |
| if (!checkbox.checked) { | |
| input.value = ''; | |
| } else if (input.getAttribute('data-day')) { | |
| // Restore default values for weekdays | |
| const day = checkbox.getAttribute('data-day'); | |
| if (['monday', 'tuesday', 'wednesday', 'thursday', 'friday'].includes(day)) { | |
| if (input.previousElementSibling.textContent.includes('Start')) { | |
| input.value = '09:00'; | |
| } else { | |
| input.value = '17:00'; | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| function copyDaySettings(day) { | |
| // In a real app, this would copy settings to other days | |
| showToast(`Copied ${day} settings to clipboard`); | |
| } | |
| function addTimeBlock() { | |
| const blocksList = document.getElementById('time-blocks-list'); | |
| if (!blocksList) return; | |
| const newBlock = document.createElement('div'); | |
| newBlock.className = 'time-block-item flex items-center justify-between p-3 border border-gray-200 rounded-lg test-result'; | |
| newBlock.innerHTML = ` | |
| <div class="flex items-center flex-1 space-x-4"> | |
| <div class="flex-1"> | |
| <label class="block text-xs font-medium text-gray-500 mb-1">Name</label> | |
| <input type="text" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent" value="New Block"> | |
| </div> | |
| <div class="flex-1"> | |
| <label class="block text-xs font-medium text-gray-500 mb-1">Start Time</label> | |
| <input type="time" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent" value="14:00"> | |
| </div> | |
| <div class="flex-1"> | |
| <label class="block text-xs font-medium text-gray-500 mb-1">End Time</label> | |
| <input type="time" class="w-full px-3 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent" value="15:00"> | |
| </div> | |
| </div> | |
| <button class="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors ml-3" onclick="removeTimeBlock(this)"> | |
| <i data-lucide="trash-2" class="w-4 h-4"></i> | |
| </button> | |
| `; | |
| blocksList.appendChild(newBlock); | |
| lucide.createIcons(); | |
| markUnsaved(); | |
| showToast('New time block added'); | |
| } | |
| function removeTimeBlock(button) { | |
| if (confirm('Are you sure you want to remove this time block?')) { | |
| const block = button.closest('.time-block-item'); | |
| block.style.opacity = '0'; | |
| block.style.transform = 'translateX(-10px)'; | |
| setTimeout(() => { | |
| block.remove(); | |
| markUnsaved(); | |
| showToast('Time block removed'); | |
| }, 300); | |
| } | |
| } | |
| function addBlackoutDate() { | |
| const today = new Date(); | |
| const nextWeek = new Date(today); | |
| nextWeek.setDate(today.getDate() + 7); | |
| const dateStr = nextWeek.toISOString().split('T')[0]; | |
| const formattedDate = new Date(dateStr).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); | |
| const datesList = document.getElementById('blackout-dates-list'); | |
| if (!datesList) return; | |
| const newDate = document.createElement('div'); | |
| newDate.className = 'blackout-item flex items-center justify-between p-3 border border-gray-200 rounded-lg test-result'; | |
| newDate.innerHTML = ` | |
| <div class="flex items-center flex-1"> | |
| <i data-lucide="calendar" class="w-4 h-4 text-gray-400 mr-3"></i> | |
| <span class="text-sm text-gray-700">${formattedDate}</span> | |
| <span class="ml-2 text-xs text-gray-500">(Custom blackout)</span> | |
| </div> | |
| <button class="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors" onclick="removeBlackoutDate(this)"> | |
| <i data-lucide="trash-2" class="w-4 h-4"></i> | |
| </button> | |
| `; | |
| datesList.appendChild(newDate); | |
| lucide.createIcons(); | |
| markUnsaved(); | |
| showToast('Blackout date added'); | |
| } | |
| function addBlackoutDateFromInput() { | |
| const dateInput = document.getElementById('new-blackout-date'); | |
| const descInput = document.getElementById('new-blackout-description'); | |
| if (!dateInput.value) { | |
| showToast('Please select a date'); | |
| dateInput.focus(); | |
| return; | |
| } | |
| const date = new Date(dateInput.value); | |
| const formattedDate = date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }); | |
| const datesList = document.getElementById('blackout-dates-list'); | |
| if (!datesList) return; | |
| const newDate = document.createElement('div'); | |
| newDate.className = 'blackout-item flex items-center justify-between p-3 border border-gray-200 rounded-lg test-result'; | |
| newDate.innerHTML = ` | |
| <div class="flex items-center flex-1"> | |
| <i data-lucide="calendar" class="w-4 h-4 text-gray-400 mr-3"></i> | |
| <span class="text-sm text-gray-700">${formattedDate}</span> | |
| <span class="ml-2 text-xs text-gray-500">${descInput.value ? '(' + descInput.value + ')' : ''}</span> | |
| </div> | |
| <button class="p-2 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors" onclick="removeBlackoutDate(this)"> | |
| <i data-lucide="trash-2" class="w-4 h-4"></i> | |
| </button> | |
| `; | |
| datesList.appendChild(newDate); | |
| lucide.createIcons(); | |
| // Clear inputs | |
| dateInput.value = ''; | |
| descInput.value = ''; | |
| markUnsaved(); | |
| showToast('Blackout date added'); | |
| } | |
| function removeBlackoutDate(button) { | |
| if (confirm('Are you sure you want to remove this blackout date?')) { | |
| const dateItem = button.closest('.blackout-item'); | |
| dateItem.style.opacity = '0'; | |
| dateItem.style.transform = 'translateX(-10px)'; | |
| setTimeout(() => { | |
| dateItem.remove(); | |
| markUnsaved(); | |
| showToast('Blackout date removed'); | |
| }, 300); | |
| } | |
| } | |
| function testAvailabilityConfig() { | |
| const testResults = document.getElementById('test-results'); | |
| if (!testResults) return; | |
| // Clear previous results | |
| testResults.innerHTML = ''; | |
| // Simulate tests | |
| const tests = [ | |
| { name: 'Working Hours Validation', status: 'passed', message: 'All weekdays configured correctly' }, | |
| { name: 'Time Block Overlap Check', status: 'passed', message: 'No overlapping blocks detected' }, | |
| { name: 'Blackout Date Validation', status: 'warning', message: 'Only 2 blackout dates configured' }, | |
| { name: 'Schedule Consistency', status: 'passed', message: 'No gaps in provider schedules' } | |
| ]; | |
| tests.forEach(test => { | |
| const testEl = document.createElement('div'); | |
| testEl.className = 'flex items-center justify-between py-2 border-b border-gray-100 last:border-0'; | |
| testEl.innerHTML = ` | |
| <div class="flex items-center"> | |
| <span class="w-2 h-2 rounded-full mr-3 ${test.status === 'passed' ? 'bg-green-500' : 'bg-amber-500'}"></span> | |
| <div> | |
| <div class="text-sm text-gray-700">${test.name}</div> | |
| <div class="text-xs text-gray-500">${test.message}</div> | |
| </div> | |
| </div> | |
| <span class="text-xs font-medium ${test.status === 'passed' ? 'text-green-600' : 'text-amber-600'}"> | |
| ${test.status === 'passed' ? '✓ Passed' : '⚠ Warning'} | |
| </span> | |
| `; | |
| testResults.appendChild(testEl); | |
| }); | |
| showToast('Availability configuration tested successfully'); | |
| } | |
| function saveAvailabilitySettings() { | |
| clearUnsaved(); | |
| // Visual feedback | |
| const btn = document.getElementById('header-save-btn'); | |
| if (btn) { | |
| const originalHTML = btn.innerHTML; | |
| btn.innerHTML = `<i data-lucide="check" class="w-4 h-4 mr-2"></i>Saved`; | |
| btn.classList.add('bg-green-600'); | |
| btn.classList.remove('bg-[#089e97]', 'hover:bg-[#078b85]'); | |
| lucide.createIcons(); | |
| setTimeout(() => { | |
| btn.innerHTML = `<i data-lucide="save" class="w-4 h-4 mr-2"></i>Save Changes`; | |
| btn.classList.remove('bg-green-600'); | |
| btn.classList.add('bg-[#089e97]', 'hover:bg-[#078b85]'); | |
| lucide.createIcons(); | |
| }, 2000); | |
| } | |
| showToast('Availability settings saved successfully'); | |
| // Update status pill if this is the first save | |
| const statusPill = document.querySelector('.status-pill'); | |
| if (statusPill && statusPill.classList.contains('status-needs-setup')) { | |
| statusPill.classList.remove('status-needs-setup'); | |
| statusPill.classList.add('status-optimized'); | |
| statusPill.innerHTML = `<span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span>Optimized`; | |
| statusPill.querySelector('span').textContent = 'Optimized'; | |
| } | |
| } | |
| function openAIOptimizeAvailability() { | |
| const optimizations = [ | |
| { id: 'extend-hours', name: 'Extend Friday Hours', description: 'High demand detected on Friday afternoons', impact: '+8 appointments/week', selected: true }, | |
| { id: 'add-saturday', name: 'Enable Saturday Mornings', description: 'Patient survey shows 34% want weekend slots', impact: '+15% patient satisfaction', selected: true }, | |
| { id: 'buffer-optimization', name: 'Smart Buffer Times', description: 'Add 15min buffers between appointments', impact: '+45% fewer overlaps', selected: true }, | |
| { id: 'lunch-optimization', name: 'Optimize Lunch Breaks', description: 'Extend to 90 minutes on busy days', impact: '+28% provider satisfaction', selected: false } | |
| ]; | |
| const optimizationsHTML = optimizations.map(opt => ` | |
| <label class="flex items-start p-4 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors ${opt.selected ? 'border-[#089e97] bg-[#089e97]/5' : ''}"> | |
| <input type="checkbox" class="mt-1 h-4 w-4 text-[#089e97] focus:ring-[#089e97] border-gray-300 rounded" ${opt.selected ? 'checked' : ''}> | |
| <div class="ml-3 flex-1"> | |
| <div class="flex items-center justify-between"> | |
| <span class="font-medium text-gray-900">${opt.name}</span> | |
| <span class="text-xs font-medium text-green-600 bg-green-50 px-2 py-1 rounded-full">${opt.impact}</span> | |
| </div> | |
| <p class="text-sm text-gray-500 mt-1">${opt.description}</p> | |
| </div> | |
| </label> | |
| `).join(''); | |
| showModal('AI Availability Optimization', ` | |
| <div class="space-y-4"> | |
| <div class="p-4 bg-gradient-to-r from-purple-50 to-blue-50 rounded-lg border border-purple-200"> | |
| <div class="flex items-center mb-2"> | |
| <i data-lucide="sparkles" class="w-5 h-5 text-purple-600 mr-2"></i> | |
| <span class="font-semibold text-gray-900">AI Analysis Complete</span> | |
| </div> | |
| <p class="text-sm text-gray-600">Based on your patient demand patterns and provider capacity, AI recommends these optimizations:</p> | |
| </div> | |
| <div class="space-y-3"> | |
| ${optimizationsHTML} | |
| </div> | |
| </div> | |
| `); | |
| setTimeout(() => { | |
| const saveBtn = document.getElementById('save-modal'); | |
| if (saveBtn) { | |
| saveBtn.textContent = 'Apply Optimizations'; | |
| saveBtn.onclick = () => { | |
| // Apply selected optimizations | |
| optimizations.forEach(opt => { | |
| if (opt.selected) { | |
| switch(opt.id) { | |
| case 'extend-hours': | |
| // Enable Friday and extend hours | |
| const fridayCheckbox = document.querySelector('.toggle-day[data-day="friday"]'); | |
| if (fridayCheckbox) { | |
| fridayCheckbox.checked = true; | |
| toggleDayRow(fridayCheckbox); | |
| const fridayEndInput = document.querySelector('.toggle-day[data-day="friday"]').closest('.day-row').querySelectorAll('input[type="time"]')[1]; | |
| if (fridayEndInput) fridayEndInput.value = '18:00'; | |
| } | |
| break; | |
| case 'add-saturday': | |
| const saturdayCheckbox = document.querySelector('.toggle-day[data-day="saturday"]'); | |
| if (saturdayCheckbox) { | |
| saturdayCheckbox.checked = true; | |
| toggleDayRow(saturdayCheckbox); | |
| const saturdayStartInput = saturdayCheckbox.closest('.day-row').querySelectorAll('input[type="time"]')[0]; | |
| const saturdayEndInput = saturdayCheckbox.closest('.day-row').querySelectorAll('input[type="time"]')[1]; | |
| if (saturdayStartInput) saturdayStartInput.value = '09:00'; | |
| if (saturdayEndInput) saturdayEndInput.value = '13:00'; | |
| } | |
| break; | |
| case 'buffer-optimization': | |
| addTimeBlock(); | |
| break; | |
| } | |
| } | |
| }); | |
| hideModal(); | |
| markUnsaved(); | |
| showToast('AI optimizations applied successfully'); | |
| }; | |
| } | |
| }, 100); | |
| } | |
| function applyAIRecommendation(type) { | |
| switch(type) { | |
| case 'buffer': | |
| addTimeBlock(); | |
| showToast('Buffer time recommendation applied'); | |
| break; | |
| case 'weekend': | |
| const saturdayCheckbox = document.querySelector('.toggle-day[data-day="saturday"]'); | |
| if (saturdayCheckbox) { | |
| saturdayCheckbox.checked = true; | |
| toggleDayRow(saturdayCheckbox); | |
| const saturdayStartInput = saturdayCheckbox.closest('.day-row').querySelectorAll('input[type="time"]')[0]; | |
| const saturdayEndInput = saturdayCheckbox.closest('.day-row').querySelectorAll('input[type="time"]')[1]; | |
| if (saturdayStartInput) saturdayStartInput.value = '09:00'; | |
| if (saturdayEndInput) saturdayEndInput.value = '13:00'; | |
| showToast('Saturday hours enabled (9 AM - 1 PM)'); | |
| } | |
| break; | |
| case 'lunch': | |
| const lunchBlock = document.querySelector('.time-block-item:first-child input[type="text"]'); | |
| if (lunchBlock && lunchBlock.value === 'Lunch Break') { | |
| const endInput = lunchBlock.closest('.time-block-item').querySelectorAll('input[type="time"]')[1]; | |
| if (endInput) endInput.value = '13:30'; | |
| showToast('Lunch break extended to 90 minutes'); | |
| } | |
| break; | |
| } | |
| markUnsaved(); | |
| } | |
| function autoApplyAIRecommendation(type) { | |
| applyAIRecommendation(type); | |
| showToast('AI automatically applied the recommendation'); | |
| } | |
| // Sticky header scroll behavior | |
| function initStickyHeader() { | |
| const scrollContainer = document.getElementById('main-content-scroll'); | |
| const header = document.getElementById('page-header'); | |
| if (!scrollContainer || !header) return; | |
| scrollContainer.addEventListener('scroll', () => { | |
| if (scrollContainer.scrollTop > 10) { | |
| header.classList.add('shadow-sm', 'bg-white/95'); | |
| header.classList.remove('bg-white/80'); | |
| } else { | |
| header.classList.remove('shadow-sm', 'bg-white/95'); | |
| header.classList.add('bg-white/80'); | |
| } | |
| }); | |
| } | |
| // Navigation to settings section | |
| function navigateToSection(sectionId) { | |
| // GIS V1: Immediate feedback on navigation trigger | |
| // (Only if triggered by a button click, not programmatic navigation) | |
| if (event && event.currentTarget && event.currentTarget.tagName === 'A') { | |
| event.currentTarget.style.transform = 'scale(0.97)'; | |
| setTimeout(() => { event.currentTarget.style.transform = ''; }, 150); | |
| } | |
| // Check for unsaved changes before navigating | |
| if (hasUnsavedChanges && sectionId !== appState.currentView) { | |
| if (!confirm('You have unsaved changes. Are you sure you want to leave this page?')) { | |
| return; | |
| } | |
| clearUnsaved(); | |
| } | |
| // Update active state in sidebar | |
| document.querySelectorAll('#settings-sidebar a').forEach(link => { | |
| link.classList.remove('sidebar-item-active'); | |
| if (link.dataset.section === sectionId) { | |
| link.classList.add('sidebar-item-active'); | |
| } | |
| }); | |
| // Update app state | |
| appState.currentView = sectionId; | |
| const mainContent = document.getElementById('main-content'); | |
| if (sectionId === 'dashboard') { | |
| location.reload(); | |
| return; | |
| } | |
| // Check if we have a premium header config for this section | |
| const headerConfig = pageHeaders[sectionId] || { | |
| title: sectionId.charAt(0).toUpperCase() + sectionId.slice(1).replace(/-/g, ' ') + ' Settings', | |
| description: `Configure your ${sectionId.replace(/-/g, ' ')} settings`, | |
| breadcrumb: [ | |
| { label: 'Settings', action: () => navigateToSection('dashboard'), clickable: true }, | |
| { label: sectionId.charAt(0).toUpperCase() + sectionId.slice(1).replace(/-/g, ' '), action: null, clickable: false } | |
| ], | |
| actions: ['help', 'ai', 'save'] | |
| }; | |
| // Premium Appointment Types Settings Page with Enterprise Command Header | |
| if (sectionId === 'appointment-types') { | |
| const appointmentTypesData = { | |
| count: 8, | |
| status: 'Active', | |
| lastUpdated: '2 hours ago', | |
| usageContexts: ['Scheduling OS', 'Booking Links', 'Patient Intake'], | |
| activeTypes: [ | |
| { name: 'Initial Consultation', duration: '60 min', color: '#089e97', usage: 'High' }, | |
| { name: 'Follow-up Visit', duration: '30 min', color: '#3b82f6', usage: 'High' }, | |
| { name: 'Urgent Care', duration: '20 min', color: '#f59e0b', usage: 'Medium' }, | |
| { name: 'Telehealth', duration: '45 min', color: '#8b5cf6', usage: 'High' } | |
| ] | |
| }; | |
| mainContent.innerHTML = ` | |
| <!-- Enterprise Command Header Layer --> | |
| <div class="sticky top-0 z-30 bg-white border-b border-gray-200/80 transition-all duration-300" id="page-header"> | |
| <div class="px-8 py-5"> | |
| <!-- Layer 1: Strong Back Navigation --> | |
| <div class="mb-3"> | |
| <button onclick="navigateToSection('dashboard')" | |
| class="inline-flex items-center text-sm font-medium text-gray-500 hover:text-[#089e97] transition-all group focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:ring-offset-2 rounded-md px-2 py-1 -ml-2"> | |
| <i data-lucide="arrow-left" class="w-4 h-4 mr-1.5 transition-transform group-hover:-translate-x-0.5"></i> | |
| Back to Settings | |
| </button> | |
| </div> | |
| <!-- Layer 2: Enhanced Breadcrumb --> | |
| <nav aria-label="Breadcrumb" class="mb-3"> | |
| <ol class="flex items-center flex-wrap text-sm"> | |
| <li> | |
| <button onclick="navigateToSection('dashboard')" | |
| class="font-medium text-gray-400 hover:text-gray-600 transition-colors focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:ring-offset-1 rounded px-1"> | |
| Settings | |
| </button> | |
| </li> | |
| <li class="mx-2 text-gray-300">/</li> | |
| <li> | |
| <span class="font-semibold text-gray-900" aria-current="page">Appointment Types</span> | |
| </li> | |
| </ol> | |
| </nav> | |
| <!-- Layer 3: Title Block with Actions --> | |
| <div class="flex items-start justify-between"> | |
| <div class="flex-1 min-w-0"> | |
| <h1 class="text-2xl font-bold text-gray-900 tracking-tight">Appointment Types</h1> | |
| <p class="mt-1.5 text-sm text-gray-500 max-w-2xl leading-relaxed"> | |
| Define visit categories, durations, color coding, and workflow behavior used across scheduling, booking links, and patient intake flows. | |
| </p> | |
| <!-- Layer 4: System Context Bar --> | |
| <div class="mt-4 flex flex-wrap items-center gap-4 text-sm"> | |
| <div class="flex items-center text-gray-600"> | |
| <i data-lucide="layers" class="w-4 h-4 mr-1.5 text-gray-400"></i> | |
| <span class="font-medium">${appointmentTypesData.count} active types</span> | |
| </div> | |
| <span class="text-gray-300">|</span> | |
| <div class="flex items-center text-gray-600"> | |
| <i data-lucide="zap" class="w-4 h-4 mr-1.5 text-gray-400"></i> | |
| <span>Used in: <span class="font-medium text-gray-700">${appointmentTypesData.usageContexts.join(', ')}</span></span> | |
| </div> | |
| <span class="text-gray-300">|</span> | |
| <div class="flex items-center"> | |
| <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-50 text-green-700 border border-green-200"> | |
| <span class="w-1.5 h-1.5 bg-green-500 rounded-full mr-1.5"></span> | |
| ${appointmentTypesData.status} | |
| </span> | |
| </div> | |
| <span class="text-gray-300">|</span> | |
| <div class="flex items-center text-gray-500 text-xs"> | |
| <i data-lucide="clock" class="w-3.5 h-3.5 mr-1.5"></i> | |
| Updated ${appointmentTypesData.lastUpdated} | |
| </div> | |
| </div> | |
| <!-- Unsaved Changes Indicator --> | |
| <div id="unsaved-badge" class="hidden mt-3 inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium bg-amber-50 text-amber-700 border border-amber-200"> | |
| <span class="w-1.5 h-1.5 bg-amber-500 rounded-full mr-1.5 animate-pulse"></span> | |
| Unsaved changes | |
| </div> | |
| </div> | |
| <!-- Top-Right Action Cluster --> | |
| <div class="flex items-center space-x-2 ml-4 flex-shrink-0"> | |
| <!-- Quick Actions --> | |
| <button onclick="openAddAppointmentTypeModal()" class="flex items-center px-3 py-2 text-sm font-medium text-[#089e97] bg-[#089e97]/10 hover:bg-[#089e97]/20 rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:ring-offset-1" aria-label="Add new appointment type"> | |
| <i data-lucide="plus" class="w-4 h-4 mr-2"></i> | |
| Add Type | |
| </button> | |
| <button onclick="openAIOptimizeModal()" class="flex items-center px-3 py-2 text-sm font-medium text-purple-700 bg-purple-50 hover:bg-purple-100 rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-offset-1 group" aria-label="AI optimization"> | |
| <i data-lucide="sparkles" class="w-4 h-4 mr-2 group-hover:animate-pulse"></i> | |
| AI Optimize | |
| </button> | |
| <div class="w-px h-6 bg-gray-200 mx-1"></div> | |
| <button onclick="openContextHelp()" class="flex items-center px-3 py-2 text-sm font-medium text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-all focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:ring-offset-1" aria-label="Get help"> | |
| <i data-lucide="help-circle" class="w-4 h-4 mr-2"></i> | |
| Help | |
| </button> | |
| <button id="header-save-btn" onclick="saveAppointmentTypes()" class="flex items-center px-4 py-2 text-sm font-medium text-white bg-[#089e97] hover:bg-[#078b85] rounded-lg shadow-sm transition-all focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed" aria-label="Save changes"> | |
| <i data-lucide="save" class="w-4 h-4 mr-2"></i> | |
| Save Changes | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Content Area --> | |
| <div class="px-8 pb-8 pt-6" id="settings-form"> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
| <!-- Main Configuration: Appointment Types List --> | |
| <div class="lg:col-span-2 space-y-6"> | |
| <div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden"> | |
| <div class="px-6 py-4 border-b border-gray-100 bg-gray-50/50 flex items-center justify-between"> | |
| <h3 class="font-semibold text-gray-900 flex items-center"> | |
| <i data-lucide="calendar" class="w-5 h-5 mr-2 text-[#089e97]"></i> | |
| Active Appointment Types | |
| </h3> | |
| <button onclick="openAddAppointmentTypeModal()" class="text-sm text-[#089e97] hover:text-[#078b85] font-medium flex items-center"> | |
| <i data-lucide="plus" class="w-4 h-4 mr-1"></i> | |
| Add New | |
| </button> | |
| </div> | |
| <div class="divide-y divide-gray-100"> | |
| ${appointmentTypesData.activeTypes.map((type, index) => ` | |
| <div class="p-6 hover:bg-gray-50/50 transition-colors group"> | |
| <div class="flex items-start justify-between"> | |
| <div class="flex items-start flex-1"> | |
| <div class="w-4 h-4 rounded-full mt-1 mr-4 flex-shrink-0" style="background-color: ${type.color}"></div> | |
| <div class="flex-1"> | |
| <div class="flex items-center mb-1"> | |
| <h4 class="font-semibold text-gray-900">${type.name}</h4> | |
| <span class="ml-3 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${type.usage === 'High' ? 'bg-green-50 text-green-700' : 'bg-amber-50 text-amber-700'}"> | |
| ${type.usage} Usage | |
| </span> | |
| </div> | |
| <div class="flex items-center text-sm text-gray-500 space-x-4"> | |
| <span class="flex items-center"> | |
| <i data-lucide="clock" class="w-3.5 h-3.5 mr-1.5"></i> | |
| ${type.duration} | |
| </span> | |
| <span class="flex items-center"> | |
| <i data-lucide="link" class="w-3.5 h-3.5 mr-1.5"></i> | |
| Used in scheduling & intake | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="flex items-center space-x-1 opacity-0 group-hover:opacity-100 transition-opacity"> | |
| <button onclick="editAppointmentType(${index})" class="p-2 hover:bg-gray-100 rounded-lg text-gray-500 hover:text-gray-700" title="Edit"> | |
| <i data-lucide="pencil" class="w-4 h-4"></i> | |
| </button> | |
| <button onclick="duplicateAppointmentType(${index})" class="p-2 hover:bg-gray-100 rounded-lg text-gray-500 hover:text-gray-700" title="Duplicate"> | |
| <i data-lucide="copy" class="w-4 h-4"></i> | |
| </button> | |
| <button onclick="deleteAppointmentType(${index})" class="p-2 hover:bg-red-50 rounded-lg text-gray-500 hover:text-red-600" title="Delete"> | |
| <i data-lucide="trash-2" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| </div> | |
| <!-- Global Settings --> | |
| <div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden"> | |
| <div class="px-6 py-4 border-b border-gray-100 bg-gray-50/50"> | |
| <h3 class="font-semibold text-gray-900 flex items-center"> | |
| <i data-lucide="settings-2" class="w-5 h-5 mr-2 text-[#089e97]"></i> | |
| Global Behavior | |
| </h3> | |
| </div> | |
| <div class="p-6 space-y-6"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Default Buffer Time</label> | |
| <select class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent bg-white" id="setting-buffer-time" onchange="markUnsaved()"> | |
| <option>0 minutes</option> | |
| <option selected>15 minutes</option> | |
| <option>30 minutes</option> | |
| <option>45 minutes</option> | |
| </select> | |
| <p class="text-xs text-gray-500 mt-1">Time between appointments</p> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Booking Notice Required</label> | |
| <select class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent bg-white" id="setting-notice" onchange="markUnsaved()"> | |
| <option>Same day</option> | |
| <option selected>24 hours</option> | |
| <option>48 hours</option> | |
| <option>1 week</option> | |
| </select> | |
| <p class="text-xs text-gray-500 mt-1">Minimum advance notice</p> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-3">Workflow Automation</label> | |
| <div class="space-y-3"> | |
| <label class="flex items-center p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors"> | |
| <input type="checkbox" class="h-4 w-4 text-[#089e97] focus:ring-[#089e97] border-gray-300 rounded" id="setting-auto-intake" onchange="markUnsaved()" checked> | |
| <div class="ml-3"> | |
| <div class="text-sm font-medium text-gray-900">Auto-send Intake Forms</div> | |
| <div class="text-xs text-gray-500">Send forms when appointment is booked</div> | |
| </div> | |
| </label> | |
| <label class="flex items-center p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors"> | |
| <input type="checkbox" class="h-4 w-4 text-[#089e97] focus:ring-[#089e97] border-gray-300 rounded" id="setting-auto-reminders" onchange="markUnsaved()" checked> | |
| <div class="ml-3"> | |
| <div class="text-sm font-medium text-gray-900">Type-specific Reminders</div> | |
| <div class="text-xs text-gray-500">Customize reminder timing per appointment type</div> | |
| </div> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Sidebar: Intelligence & Tips --> | |
| <div class="space-y-6"> | |
| <div class="bg-gradient-to-br from-[#089e97]/5 to-blue-50 rounded-xl border border-[#089e97]/20 p-5"> | |
| <div class="flex items-center mb-3"> | |
| <div class="w-8 h-8 bg-[#089e97]/10 rounded-lg flex items-center justify-center mr-3"> | |
| <i data-lucide="lightbulb" class="w-4 h-4 text-[#089e97]"></i> | |
| </div> | |
| <h4 class="font-semibold text-gray-900 text-sm">AI Insight</h4> | |
| </div> | |
| <p class="text-sm text-gray-600 leading-relaxed mb-3">Adding 15-minute buffer times between appointments could reduce provider stress by 28% and prevent scheduling conflicts.</p> | |
| <button onclick="applyBufferRecommendation()" class="w-full py-2 bg-[#089e97] text-white text-sm rounded-lg hover:bg-[#078b85] transition-colors"> | |
| Apply Recommendation | |
| </button> | |
| </div> | |
| <div class="bg-white rounded-xl border border-gray-100 shadow-sm p-5"> | |
| <h4 class="font-semibold text-gray-900 text-sm mb-4 flex items-center"> | |
| <i data-lucide="activity" class="w-4 h-4 mr-2 text-gray-400"></i> | |
| Usage Analytics | |
| </h4> | |
| <div class="space-y-4"> | |
| <div> | |
| <div class="flex justify-between items-center mb-1"> | |
| <span class="text-sm text-gray-500">Most Booked</span> | |
| <span class="text-sm font-medium text-gray-900">Follow-up Visit</span> | |
| </div> | |
| <div class="h-2 bg-gray-100 rounded-full overflow-hidden"> | |
| <div class="h-full bg-[#089e97] rounded-full" style="width: 78%"></div> | |
| </div> | |
| </div> | |
| <div> | |
| <div class="flex justify-between items-center mb-1"> | |
| <span class="text-sm text-gray-500">Growth Trend</span> | |
| <span class="text-sm font-medium text-green-600">+12% this month</span> | |
| </div> | |
| <div class="h-2 bg-gray-100 rounded-full overflow-hidden"> | |
| <div class="h-full bg-green-500 rounded-full" style="width: 62%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-xl border border-gray-100 shadow-sm p-5"> | |
| <h4 class="font-semibold text-gray-900 text-sm mb-3">Integration Status</h4> | |
| <div class="space-y-2"> | |
| <div class="flex items-center justify-between py-2 border-b border-gray-50"> | |
| <div class="flex items-center text-sm text-gray-600"> | |
| <i data-lucide="calendar" class="w-4 h-4 mr-2 text-gray-400"></i> | |
| Scheduling OS | |
| </div> | |
| <span class="text-xs font-medium text-green-600">Connected</span> | |
| </div> | |
| <div class="flex items-center justify-between py-2 border-b border-gray-50"> | |
| <div class="flex items-center text-sm text-gray-600"> | |
| <i data-lucide="link" class="w-4 h-4 mr-2 text-gray-400"></i> | |
| Booking Links | |
| </div> | |
| <span class="text-xs font-medium text-green-600">Active</span> | |
| </div> | |
| <div class="flex items-center justify-between py-2"> | |
| <div class="flex items-center text-sm text-gray-600"> | |
| <i data-lucide="file-text" class="w-4 h-4 mr-2 text-gray-400"></i> | |
| Intake Forms | |
| </div> | |
| <span class="text-xs font-medium text-amber-600">4 of 8 linked</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| lucide.createIcons(); | |
| setupFormChangeDetection(); | |
| initStickyHeader(); | |
| } else if (sectionId === 'providers') { | |
| mainContent.innerHTML = ` | |
| ${renderPremiumHeader(headerConfig)} | |
| <div class="px-8 pb-8 pt-6" id="settings-form"> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
| <!-- Main Configuration --> | |
| <div class="lg:col-span-2 space-y-6"> | |
| <div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden"> | |
| <div class="px-6 py-4 border-b border-gray-100 bg-gray-50/50"> | |
| <h3 class="font-semibold text-gray-900 flex items-center"> | |
| <i data-lucide="user-check" class="w-5 h-5 mr-2 text-[#089e97]"></i> | |
| Provider Configuration | |
| </h3> | |
| </div> | |
| <div class="p-6 space-y-6"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Default Provider Role</label> | |
| <select class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent bg-white" id="setting-provider-role"> | |
| <option>Primary Care Physician</option> | |
| <option selected>Specialist</option> | |
| <option>Nurse Practitioner</option> | |
| <option>Physician Assistant</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Default Appointment Duration</label> | |
| <select class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent bg-white" id="setting-duration"> | |
| <option>15 minutes</option> | |
| <option selected>30 minutes</option> | |
| <option>45 minutes</option> | |
| <option>60 minutes</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-3">Provider Capabilities</label> | |
| <div class="space-y-3"> | |
| <label class="flex items-center p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors"> | |
| <input type="checkbox" class="h-4 w-4 text-[#089e97] focus:ring-[#089e97] border-gray-300 rounded" id="setting-provider-notifications" checked> | |
| <div class="ml-3"> | |
| <div class="text-sm font-medium text-gray-900">Enable Notifications</div> | |
| <div class="text-xs text-gray-500">Send email/SMS for new bookings and cancellations</div> | |
| </div> | |
| </label> | |
| <label class="flex items-center p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors"> | |
| <input type="checkbox" class="h-4 w-4 text-[#089e97] focus:ring-[#089e97] border-gray-300 rounded" id="setting-auto-scheduling"> | |
| <div class="ml-3"> | |
| <div class="text-sm font-medium text-gray-900">Auto-Scheduling</div> | |
| <div class="text-xs text-gray-500">Allow AI to optimize appointment placement</div> | |
| </div> | |
| </label> | |
| <label class="flex items-center p-3 border border-gray-200 rounded-lg hover:bg-gray-50 cursor-pointer transition-colors"> | |
| <input type="checkbox" class="h-4 w-4 text-[#089e97] focus:ring-[#089e97] border-gray-300 rounded" id="setting-conflict-prevention" checked> | |
| <div class="ml-3"> | |
| <div class="text-sm font-medium text-gray-900">Conflict Prevention</div> | |
| <div class="text-xs text-gray-500">Block double-booking and enforce buffer times</div> | |
| </div> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white rounded-xl border border-gray-100 shadow-sm overflow-hidden"> | |
| <div class="px-6 py-4 border-b border-gray-100 bg-gray-50/50"> | |
| <h3 class="font-semibold text-gray-900 flex items-center"> | |
| <i data-lucide="shield" class="w-5 h-5 mr-2 text-[#089e97]"></i> | |
| Permissions & Access | |
| </h3> | |
| </div> | |
| <div class="p-6 space-y-6"> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Calendar Access Level</label> | |
| <select class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent bg-white" id="setting-calendar-access"> | |
| <option>View Own Only</option> | |
| <option selected>View Department</option> | |
| <option>View All</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Patient Data Access</label> | |
| <select class="w-full px-4 py-2.5 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent bg-white" id="setting-patient-access"> | |
| <option>Assigned Patients Only</option> | |
| <option selected>All Patients</option> | |
| <option>Read-Only</option> | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Sidebar Info --> | |
| <div class="space-y-6"> | |
| <div class="bg-gradient-to-br from-[#089e97]/5 to-blue-50 rounded-xl border border-[#089e97]/20 p-5"> | |
| <div class="flex items-center mb-3"> | |
| <div class="w-8 h-8 bg-[#089e97]/10 rounded-lg flex items-center justify-center mr-3"> | |
| <i data-lucide="lightbulb" class="w-4 h-4 text-[#089e97]"></i> | |
| </div> | |
| <h4 class="font-semibold text-gray-900 text-sm">Pro Tip</h4> | |
| </div> | |
| <p class="text-sm text-gray-600 leading-relaxed">Enable Auto-Scheduling to let the AI optimize your provider calendars, reducing gaps by up to 35%.</p> | |
| </div> | |
| <div class="bg-white rounded-xl border border-gray-100 shadow-sm p-5"> | |
| <h4 class="font-semibold text-gray-900 text-sm mb-4 flex items-center"> | |
| <i data-lucide="activity" class="w-4 h-4 mr-2 text-gray-400"></i> | |
| Current Stats | |
| </h4> | |
| <div class="space-y-3"> | |
| <div class="flex justify-between items-center"> | |
| <span class="text-sm text-gray-500">Active Providers</span> | |
| <span class="font-semibold text-gray-900">12</span> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <span class="text-sm text-gray-500">Avg. Daily Appointments</span> | |
| <span class="font-semibold text-gray-900">48</span> | |
| </div> | |
| <div class="flex justify-between items-center"> | |
| <span class="text-sm text-gray-500">Utilization Rate</span> | |
| <span class="font-semibold text-green-600">87%</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| lucide.createIcons(); | |
| setupFormChangeDetection(); | |
| initStickyHeader(); | |
| } else { | |
| // Generic premium page for other sections | |
| mainContent.innerHTML = ` | |
| ${renderPremiumHeader(headerConfig)} | |
| <div class="px-8 pb-8 pt-6" id="settings-form"> | |
| <div class="bg-white rounded-xl border border-gray-100 shadow-sm p-6"> | |
| <div class="max-w-2xl"> | |
| <h2 class="text-lg font-semibold text-gray-900 mb-6">Configuration Options</h2> | |
| <div class="space-y-6"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Setting 1</label> | |
| <input type="text" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent" value="Current value"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-2">Setting 2</label> | |
| <select class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent"> | |
| <option>Option 1</option> | |
| <option selected>Option 2</option> | |
| <option>Option 3</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="flex items-center"> | |
| <input type="checkbox" class="h-4 w-4 text-[#089e97] focus:ring-[#089e97] border-gray-300 rounded"> | |
| <span class="ml-2 text-sm text-gray-700">Enable this feature</span> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| lucide.createIcons(); | |
| setupFormChangeDetection(); | |
| initStickyHeader(); | |
| } | |
| } | |
| // Quick action handlers | |
| function handleQuickAction(action) { | |
| const actions = { | |
| 'add-provider': { | |
| title: 'Add New Provider', | |
| content: ` | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Provider Name</label> | |
| <input type="text" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent" placeholder="Dr. Jane Smith"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Specialty</label> | |
| <input type="text" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent" placeholder="Cardiology"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Email</label> | |
| <input type="email" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent" placeholder="jane.smith@clinic.com"> | |
| </div> | |
| </div> | |
| ` | |
| }, | |
| 'set-availability': { | |
| title: 'Set Availability Rules', | |
| content: ` | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Default Hours</label> | |
| <div class="flex space-x-2"> | |
| <input type="time" class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent" value="09:00"> | |
| <span class="self-center">to</span> | |
| <input type="time" class="flex-1 px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent" value="17:00"> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Days Available</label> | |
| <div class="flex flex-wrap gap-2"> | |
| <label class="inline-flex items-center"> | |
| <input type="checkbox" class="h-4 w-4 text-[#089e97] focus:ring-[#089e97] border-gray-300 rounded" checked> | |
| <span class="ml-2 text-sm text-gray-700">Mon</span> | |
| </label> | |
| <label class="inline-flex items-center"> | |
| <input type="checkbox" class="h-4 w-4 text-[#089e97] focus:ring-[#089e97] border-gray-300 rounded" checked> | |
| <span class="ml-2 text-sm text-gray-700">Tue</span> | |
| </label> | |
| <label class="inline-flex items-center"> | |
| <input type="checkbox" class="h-4 w-4 text-[#089e97] focus:ring-[#089e97] border-gray-300 rounded" checked> | |
| <span class="ml-2 text-sm text-gray-700">Wed</span> | |
| </label> | |
| <label class="inline-flex items-center"> | |
| <input type="checkbox" class="h-4 w-4 text-[#089e97] focus:ring-[#089e97] border-gray-300 rounded" checked> | |
| <span class="ml-2 text-sm text-gray-700">Thu</span> | |
| </label> | |
| <label class="inline-flex items-center"> | |
| <input type="checkbox" class="h-4 w-4 text-[#089e97] focus:ring-[#089e97] border-gray-300 rounded" checked> | |
| <span class="ml-2 text-sm text-gray-700">Fri</span> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| ` | |
| }, | |
| 'create-appointment': { | |
| title: 'Create Appointment Type', | |
| content: ` | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Appointment Name</label> | |
| <input type="text" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent" placeholder="Follow-up Consultation"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Duration (minutes)</label> | |
| <select class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent"> | |
| <option>15</option> | |
| <option selected>30</option> | |
| <option>45</option> | |
| <option>60</option> | |
| </select> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">Color</label> | |
| <input type="color" class="w-full h-10 border border-gray-300 rounded-lg" value="#089e97"> | |
| </div> | |
| </div> | |
| ` | |
| }, | |
| 'enable-payments': { | |
| title: 'Enable Payments', | |
| content: ` | |
| <div class="space-y-4"> | |
| <div> | |
| <h4 class="font-medium text-gray-900 mb-2">Select Payment Processor</h4> | |
| <div class="space-y-2"> | |
| <label class="flex items-center p-3 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-50"> | |
| <input type="radio" name="processor" class="h-4 w-4 text-[#089e97] focus:ring-[#089e97]" checked> | |
| <div class="ml-3"> | |
| <div class="font-medium text-gray-900">Stripe</div> | |
| <div class="text-sm text-gray-500">Recommended for healthcare</div> | |
| </div> | |
| </label> | |
| <label class="flex items-center p-3 border border-gray-200 rounded-lg cursor-pointer hover:bg-gray-50"> | |
| <input type="radio" name="processor" class="h-4 w-4 text-[#089e97] focus:ring-[#089e97]"> | |
| <div class="ml-3"> | |
| <div class="font-medium text-gray-900">PayPal</div> | |
| <div class="text-sm text-gray-500">Widely used</div> | |
| </div> | |
| </label> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-gray-700 mb-1">API Key</label> | |
| <input type="text" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-[#089e97] focus:border-transparent" placeholder="sk_live_..."> | |
| </div> | |
| </div> | |
| ` | |
| } | |
| }; | |
| if (actions[action]) { | |
| showModal(actions[action].title, actions[action].content); | |
| } else { | |
| showModal('Quick Action', `<p class="text-gray-700">This action would open the ${action} configuration panel.</p>`); | |
| } | |
| } | |
| // Initialize event listeners | |
| function initializeEventListeners() { | |
| // Checklist item toggles | |
| document.querySelectorAll('.checklist-item').forEach(item => { | |
| item.addEventListener('click', (e) => { | |
| if (e.target.tagName === 'BUTTON') { | |
| const itemId = e.currentTarget.dataset.item; | |
| navigateToSection(itemId.split('-')[0]); | |
| } else { | |
| const itemId = e.currentTarget.dataset.item; | |
| toggleChecklistItem(itemId); | |
| } | |
| }); | |
| }); | |
| // Quick actions | |
| document.querySelectorAll('.quick-action').forEach(button => { | |
| button.addEventListener('click', () => { | |
| const action = button.dataset.action; | |
| handleQuickAction(action); | |
| }); | |
| }); | |
| // Settings sidebar navigation - Updated for V2 | |
| document.querySelectorAll('#settings-sidebar a').forEach(link => { | |
| link.addEventListener('click', (e) => { | |
| e.preventDefault(); | |
| const section = link.dataset.section; | |
| navigateToSection(section); | |
| }); | |
| }); | |
| // Settings category cards | |
| document.querySelectorAll('[data-section]').forEach(card => { | |
| if (card.classList.contains('card-hover')) { | |
| card.addEventListener('click', () => { | |
| const section = card.dataset.section; | |
| navigateToSection(section); | |
| }); | |
| } | |
| }); | |
| // Adaptive quick actions buttons | |
| document.querySelectorAll('#adaptive-actions button').forEach(button => { | |
| button.addEventListener('click', (e) => { | |
| // Let the onclick attribute handle navigation | |
| }); | |
| }); | |
| // Settings module cards | |
| document.querySelectorAll('#settings-cards > div').forEach(card => { | |
| card.addEventListener('click', (e) => { | |
| // Let the onclick attribute handle navigation | |
| }); | |
| }); | |
| // Collapse sidebar button | |
| document.getElementById('collapse-sidebar').addEventListener('click', () => { | |
| const sidebar = document.getElementById('settings-sidebar'); | |
| sidebar.classList.toggle('hidden'); | |
| appState.settingsSidebarExpanded = !appState.settingsSidebarExpanded; | |
| }); | |
| // AI Assistant toggle | |
| document.getElementById('ai-assistant-toggle').addEventListener('click', () => { | |
| const panel = document.getElementById('ai-assistant-panel'); | |
| if (appState.aiPanelExpanded) { | |
| panel.style.width = '0'; | |
| } else { | |
| panel.style.width = '20rem'; | |
| } | |
| appState.aiPanelExpanded = !appState.aiPanelExpanded; | |
| }); | |
| // Close AI panel | |
| document.getElementById('close-ai-panel').addEventListener('click', () => { | |
| const panel = document.getElementById('ai-assistant-panel'); | |
| panel.style.width = '0'; | |
| appState.aiPanelExpanded = false; | |
| }); | |
| // Save all changes button | |
| document.querySelector('button.bg-\\[\\#089e97\\]').addEventListener('click', () => { | |
| showToast('All settings saved successfully'); | |
| }); | |
| // Modal overlay click to close | |
| document.getElementById('modal-overlay').addEventListener('click', (e) => { | |
| if (e.target === document.getElementById('modal-overlay')) { | |
| hideModal(); | |
| } | |
| }); | |
| // System health fix buttons | |
| document.querySelectorAll('.health-warning button').forEach(button => { | |
| button.addEventListener('click', () => { | |
| navigateToSection('payments'); | |
| }); | |
| }); | |
| // AI recommendation buttons | |
| document.querySelectorAll('.bg-gray-50 button').forEach(button => { | |
| button.addEventListener('click', () => { | |
| showToast('Navigating to recommended setting'); | |
| }); | |
| }); | |
| } | |
| // Account Setup Progress Functions | |
| function toggleSetupItem(element) { | |
| // GIS V1: Add immediate scale feedback | |
| element.style.transform = 'scale(0.98)'; | |
| element.style.transition = 'transform 80ms ease'; | |
| setTimeout(() => { | |
| element.style.transform = ''; | |
| }, 100); | |
| const circle = element.querySelector('.check-circle'); | |
| const icon = circle.querySelector('i'); | |
| const text = element.querySelector('span'); | |
| const isCompleted = circle.classList.contains('bg-[#089e97]'); | |
| if (isCompleted) { | |
| // Uncheck | |
| circle.classList.remove('bg-[#089e97]', 'text-white'); | |
| circle.classList.add('bg-white', 'border-2', 'border-gray-300'); | |
| icon.classList.add('hidden'); | |
| text.classList.remove('line-through', 'text-gray-400'); | |
| text.classList.add('text-gray-700'); | |
| } else { | |
| // Check | |
| circle.classList.remove('bg-white', 'border-2', 'border-gray-300'); | |
| circle.classList.add('bg-[#089e97]', 'text-white'); | |
| icon.classList.remove('hidden'); | |
| text.classList.add('line-through', 'text-gray-400'); | |
| text.classList.remove('text-gray-700'); | |
| } | |
| updateSetupProgress(); | |
| showToast(isCompleted ? 'Task marked incomplete' : 'Task completed'); | |
| } | |
| function updateSetupProgress() { | |
| const totalItems = document.querySelectorAll('#settings-dashboard-top-module .checklist-item').length; | |
| const completedItems = document.querySelectorAll('#settings-dashboard-top-module .check-circle.bg-\\[\\#089e97\\]').length; | |
| const percentage = Math.round((completedItems / totalItems) * 100) || 56; | |
| const percentageEl = document.getElementById('setup-progress-percentage'); | |
| const barEl = document.getElementById('setup-progress-bar'); | |
| if (percentageEl) percentageEl.textContent = percentage + '%'; | |
| if (barEl) barEl.style.width = percentage + '%'; | |
| } | |
| // Initialize the app | |
| document.addEventListener('DOMContentLoaded', () => { | |
| initializeEventListeners(); | |
| updateProgressDisplay(); | |
| updateSetupProgress(); // Initialize setup progress on load | |
| // Show welcome toast | |
| setTimeout(() => { | |
| showToast('Welcome to Verify MC Settings Dashboard'); | |
| }, 1000); | |
| // Ensure settings sidebar is expanded on load | |
| const sidebar = document.getElementById('settings-sidebar'); | |
| sidebar.classList.remove('hidden'); | |
| // Initialize module panel event listeners | |
| const modulePanelOverlay = document.getElementById('module-panel-overlay'); | |
| if (modulePanelOverlay) { | |
| modulePanelOverlay.addEventListener('click', (e) => { | |
| if (e.target === modulePanelOverlay) { | |
| closeModulePanel(); | |
| } | |
| }); | |
| } | |
| // Initialize collapsible sidebar sections | |
| setTimeout(() => { | |
| initializeSidebarSections(); | |
| // Auto-expand the section for current view | |
| if (appState.currentView && appState.currentView !== 'dashboard') { | |
| expandSectionForView(appState.currentView); | |
| } | |
| }, 100); | |
| }); |