Spaces:
Running
Running
| // Initialize Lucide icons | |
| lucide.createIcons(); | |
| // State management | |
| let isSidebarCollapsed = false; | |
| let isBillingExpanded = true; | |
| let lineItemCount = 0; | |
| let currentPatient = null; | |
| // Mock patient data for search | |
| const mockPatients = [ | |
| { id: 'PT-1001', name: 'John Smith', phone: '+1 (555) 123-4567', image: 'https://static.photos/people/100x100/10' }, | |
| { id: 'PT-1002', name: 'Emma Wilson', phone: '+1 (555) 234-5678', image: 'https://static.photos/people/100x100/20' }, | |
| { id: 'PT-1003', name: 'Michael Brown', phone: '+1 (555) 345-6789', image: 'https://static.photos/people/100x100/30' }, | |
| { id: 'PT-1004', name: 'Sarah Johnson', phone: '+1 (555) 456-7890', image: 'https://static.photos/people/100x100/40' }, | |
| { id: 'PT-1005', name: 'David Lee', phone: '+1 (555) 567-8901', image: 'https://static.photos/people/100x100/50' } | |
| ]; | |
| // Initialize on load | |
| document.addEventListener('DOMContentLoaded', () => { | |
| // Set default dates | |
| const today = new Date().toISOString().split('T')[0]; | |
| document.getElementById('invoiceDate').value = today; | |
| const dueDate = new Date(); | |
| dueDate.setDate(dueDate.getDate() + 30); | |
| document.getElementById('dueDate').value = dueDate.toISOString().split('T')[0]; | |
| // Add initial line item | |
| addLineItem(); | |
| }); | |
| // Sidebar toggle for mobile | |
| function toggleSidebar() { | |
| const sidebar = document.getElementById('sidebar'); | |
| const overlay = document.getElementById('mobileOverlay'); | |
| const isOpen = !sidebar.classList.contains('-translate-x-full'); | |
| if (isOpen) { | |
| sidebar.classList.add('-translate-x-full'); | |
| overlay.classList.add('hidden'); | |
| document.body.style.overflow = ''; | |
| } else { | |
| sidebar.classList.remove('-translate-x-full'); | |
| overlay.classList.remove('hidden'); | |
| document.body.style.overflow = 'hidden'; | |
| } | |
| } | |
| // Sidebar toggle for desktop | |
| function toggleSidebarDesktop() { | |
| const sidebar = document.getElementById('sidebar'); | |
| const mainWrapper = document.getElementById('mainWrapper'); | |
| const desktopToggle = document.getElementById('desktopToggle'); | |
| isSidebarCollapsed = !isSidebarCollapsed; | |
| if (isSidebarCollapsed) { | |
| sidebar.classList.add('sidebar-collapsed'); | |
| sidebar.style.width = '5rem'; | |
| mainWrapper.style.marginLeft = '5rem'; | |
| desktopToggle.innerHTML = '<i data-lucide="panel-right" class="w-5 h-5"></i>'; | |
| } else { | |
| sidebar.classList.remove('sidebar-collapsed'); | |
| sidebar.style.width = '16rem'; | |
| mainWrapper.style.marginLeft = '16rem'; | |
| desktopToggle.innerHTML = '<i data-lucide="panel-left" class="w-5 h-5"></i>'; | |
| } | |
| lucide.createIcons(); | |
| // Handle billing submenu visibility | |
| const submenu = document.getElementById('billingSubmenu'); | |
| if (isSidebarCollapsed) { | |
| submenu.style.display = 'none'; | |
| } else if (isBillingExpanded) { | |
| submenu.style.display = 'block'; | |
| } | |
| } | |
| // Billing submenu toggle | |
| function toggleBillingSubmenu() { | |
| if (isSidebarCollapsed) return; | |
| const submenu = document.getElementById('billingSubmenu'); | |
| const chevron = document.getElementById('billingChevron'); | |
| isBillingExpanded = !isBillingExpanded; | |
| if (isBillingExpanded) { | |
| submenu.style.display = 'block'; | |
| chevron.style.transform = 'rotate(0deg)'; | |
| } else { | |
| submenu.style.display = 'none'; | |
| chevron.style.transform = 'rotate(-90deg)'; | |
| } | |
| } | |
| // Patient type toggle | |
| function setPatientType(type) { | |
| const btnExisting = document.getElementById('btnExisting'); | |
| const btnNew = document.getElementById('btnNew'); | |
| const existingForm = document.getElementById('existingPatientForm'); | |
| const newForm = document.getElementById('newPatientForm'); | |
| if (type === 'existing') { | |
| btnExisting.classList.add('bg-white', 'text-gray-900', 'shadow-sm'); | |
| btnExisting.classList.remove('text-gray-600'); | |
| btnNew.classList.remove('bg-white', 'text-gray-900', 'shadow-sm'); | |
| btnNew.classList.add('text-gray-600'); | |
| existingForm.classList.remove('hidden'); | |
| newForm.classList.add('hidden'); | |
| } else { | |
| btnNew.classList.add('bg-white', 'text-gray-900', 'shadow-sm'); | |
| btnNew.classList.remove('text-gray-600'); | |
| btnExisting.classList.remove('bg-white', 'text-gray-900', 'shadow-sm'); | |
| btnExisting.classList.add('text-gray-600'); | |
| newForm.classList.remove('hidden'); | |
| existingForm.classList.add('hidden'); | |
| } | |
| } | |
| // Patient search functionality | |
| function searchPatients(query) { | |
| const resultsDiv = document.getElementById('searchResults'); | |
| if (query.length < 2) { | |
| resultsDiv.classList.add('hidden'); | |
| return; | |
| } | |
| const filtered = mockPatients.filter(p => | |
| p.name.toLowerCase().includes(query.toLowerCase()) || | |
| p.id.toLowerCase().includes(query.toLowerCase()) || | |
| p.phone.includes(query) | |
| ); | |
| if (filtered.length === 0) { | |
| resultsDiv.innerHTML = '<div class="p-3 text-sm text-gray-500">No patients found</div>'; | |
| } else { | |
| resultsDiv.innerHTML = filtered.map(patient => ` | |
| <div onclick="selectPatient('${patient.id}')" class="p-3 hover:bg-gray-50 cursor-pointer border-b border-gray-100 last:border-0 transition-colors"> | |
| <div class="flex items-center gap-3"> | |
| <img src="${patient.image}" class="w-8 h-8 rounded-full object-cover"> | |
| <div> | |
| <p class="font-medium text-gray-900 text-sm">${patient.name}</p> | |
| <p class="text-xs text-gray-500">${patient.id} • ${patient.phone}</p> | |
| </div> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| resultsDiv.classList.remove('hidden'); | |
| } | |
| // Select patient from search | |
| function selectPatient(patientId) { | |
| const patient = mockPatients.find(p => p.id === patientId); | |
| if (!patient) return; | |
| currentPatient = patient; | |
| document.getElementById('searchResults').classList.add('hidden'); | |
| document.getElementById('patientSearch').value = ''; | |
| // Show selected patient card | |
| document.getElementById('spImage').src = patient.image; | |
| document.getElementById('spName').textContent = patient.name; | |
| document.getElementById('spId').textContent = patient.id; | |
| document.getElementById('spPhone').textContent = patient.phone; | |
| document.getElementById('selectedPatientCard').classList.remove('hidden'); | |
| } | |
| // Clear selected patient | |
| function clearPatient() { | |
| currentPatient = null; | |
| document.getElementById('selectedPatientCard').classList.add('hidden'); | |
| } | |
| // Add line item to invoice | |
| function addLineItem() { | |
| const tbody = document.getElementById('lineItemsBody'); | |
| const row = document.createElement('tr'); | |
| row.className = 'line-item border-b border-gray-100 hover:bg-gray-50 transition-colors'; | |
| row.id = `lineItem-${lineItemCount}`; | |
| row.innerHTML = ` | |
| <td class="py-3 pl-2"> | |
| <select class="w-full px-3 py-2 border border-gray-200 rounded-md text-sm focus-verify-teal outline-none bg-white" onchange="updateCalculation('${lineItemCount}')"> | |
| <option value="">Select Service</option> | |
| <option value="consultation">Consultation</option> | |
| <option value="lab_test">Laboratory Test</option> | |
| <option value="xray">X-Ray</option> | |
| <option value="surgery">Surgery</option> | |
| <option value="medication">Medication</option> | |
| <option value="therapy">Physical Therapy</option> | |
| <option value="vaccination">Vaccination</option> | |
| </select> | |
| </td> | |
| <td class="py-3"> | |
| <input type="text" class="w-full px-3 py-2 border border-gray-200 rounded-md text-sm focus-verify-teal outline-none" placeholder="Description"> | |
| </td> | |
| <td class="py-3"> | |
| <input type="number" id="qty-${lineItemCount}" value="1" min="1" class="w-full px-3 py-2 border border-gray-200 rounded-md text-sm focus-verify-teal outline-none text-center" onchange="updateCalculation('${lineItemCount}')"> | |
| </td> | |
| <td class="py-3"> | |
| <input type="number" id="rate-${lineItemCount}" value="0.00" min="0" step="0.01" class="w-full px-3 py-2 border border-gray-200 rounded-md text-sm focus-verify-teal outline-none" onchange="updateCalculation('${lineItemCount}')"> | |
| </td> | |
| <td class="py-3"> | |
| <input type="text" id="amount-${lineItemCount}" value="$0.00" readonly class="w-full px-3 py-2 bg-gray-50 border border-gray-200 rounded-md text-sm text-gray-700 font-medium cursor-not-allowed font-variant-numeric"> | |
| </td> | |
| <td class="py-3 text-right"> | |
| <button onclick="removeLineItem('${lineItemCount}')" class="text-gray-400 hover:text-red-500 transition-colors p-1 rounded hover:bg-red-50/50"> | |
| <i data-lucide="trash-2" class="w-4 h-4"></i> | |
| </button> | |
| </td> | |
| `; | |
| tbody.appendChild(row); | |
| lineItemCount++; | |
| // Refresh icons | |
| lucide.createIcons(); | |
| } | |
| // Remove line item | |
| function removeLineItem(id) { | |
| const row = document.getElementById(`lineItem-${id}`); | |
| if (row) { | |
| row.style.opacity = '0'; | |
| row.style.transform = 'translateX(-20px)'; | |
| setTimeout(() => { | |
| row.remove(); | |
| calculateTotals(); | |
| }, 200); | |
| } | |
| } | |
| // Update line item calculation | |
| function updateCalculation(id) { | |
| const qty = parseFloat(document.getElementById(`qty-${id}`).value) || 0; | |
| const rate = parseFloat(document.getElementById(`rate-${id}`).value) || 0; | |
| const amount = qty * rate; | |
| document.getElementById(`amount-${id}`).value = '$' + amount.toFixed(2); | |
| calculateTotals(); | |
| } | |
| // Calculate invoice totals | |
| function calculateTotals() { | |
| let subtotal = 0; | |
| // Sum all line items | |
| for (let i = 0; i < lineItemCount; i++) { | |
| const amountField = document.getElementById(`amount-${i}`); | |
| if (amountField && amountField.parentElement.parentElement.style.display !== 'none') { | |
| const value = parseFloat(amountField.value.replace('$', '')) || 0; | |
| subtotal += value; | |
| } | |
| } | |
| const tax = subtotal * 0.08; // 8% tax | |
| const total = subtotal + tax; | |
| // Update summary displays | |
| document.getElementById('summarySubtotal').textContent = '$' + subtotal.toFixed(2); | |
| document.getElementById('summaryTax').textContent = '$' + tax.toFixed(2); | |
| document.getElementById('summaryTotal').textContent = '$' + total.toFixed(2); | |
| document.getElementById('mobileTotal').textContent = '$' + total.toFixed(2); | |
| } | |
| // Save invoice | |
| function saveInvoice() { | |
| showToast('Invoice saved successfully!'); | |
| // In real app, would send to backend | |
| } | |
| // Print invoice | |
| function printInvoice() { | |
| window.print(); | |
| } | |
| // Send invoice | |
| function sendInvoice() { | |
| showToast('Invoice sent to patient email!'); | |
| // In real app, would trigger email/SMS | |
| } | |
| // Toast notification | |
| function showToast(message) { | |
| const toast = document.getElementById('toast'); | |
| const toastMessage = document.getElementById('toastMessage'); | |
| toastMessage.textContent = message; | |
| toast.classList.remove('translate-y-20', 'opacity-0'); | |
| setTimeout(() => { | |
| toast.classList.add('translate-y-20', 'opacity-0'); | |
| }, 3000); | |
| } | |
| // Close search results when clicking outside | |
| document.addEventListener('click', (e) => { | |
| const searchContainer = document.getElementById('patientSearch'); | |
| const resultsDiv = document.getElementById('searchResults'); | |
| if (!searchContainer.contains(e.target) && !resultsDiv.contains(e.target)) { | |
| resultsDiv.classList.add('hidden'); | |
| } | |
| }); | |
| // Handle window resize | |
| window.addEventListener('resize', () => { | |
| if (window.innerWidth >= 1024) { | |
| document.getElementById('mobileOverlay').classList.add('hidden'); | |
| document.body.style.overflow = ''; | |
| } else { | |
| const sidebar = document.getElementById('sidebar'); | |
| sidebar.classList.add('-translate-x-full'); | |
| } | |
| }); |