Spaces:
Running
Running
| // Demo Patient Data | |
| const patients = [ | |
| { | |
| id: 1, | |
| firstName: "John", | |
| lastName: "Smith", | |
| mrn: "839214", | |
| status: "active", | |
| insurance: "BlueCross", | |
| insuranceProvider: "BlueCross", | |
| nextAppointment: "2026-04-02", | |
| expirationDate: "2026-05-12", | |
| clinic: "Los Angeles", | |
| lastVisit: "2026-03-01", | |
| phoneVerified: true, | |
| emailVerified: true, | |
| provider: "Dr. Smith", | |
| tags: ["new"] | |
| }, | |
| { | |
| id: 2, | |
| firstName: "Maria", | |
| lastName: "Gonzalez", | |
| mrn: "481992", | |
| status: "pending", | |
| insurance: "UnitedHealth", | |
| insuranceProvider: "UnitedHealth", | |
| nextAppointment: "2026-03-18", | |
| expirationDate: "2026-04-10", | |
| clinic: "San Diego", | |
| lastVisit: "2026-02-15", | |
| phoneVerified: true, | |
| emailVerified: false, | |
| provider: "Dr. Johnson", | |
| tags: ["followup"] | |
| }, | |
| { | |
| id: 3, | |
| firstName: "David", | |
| lastName: "Chen", | |
| mrn: "673204", | |
| status: "active", | |
| insurance: "Aetna", | |
| insuranceProvider: "Aetna", | |
| nextAppointment: "2026-04-06", | |
| expirationDate: "2026-06-02", | |
| clinic: "San Francisco", | |
| lastVisit: "2026-03-05", | |
| phoneVerified: true, | |
| emailVerified: true, | |
| provider: "Dr. Smith", | |
| tags: ["telehealth"] | |
| }, | |
| { | |
| id: 4, | |
| firstName: "Sarah", | |
| lastName: "Johnson", | |
| mrn: "904211", | |
| status: "approved", | |
| insurance: "Kaiser Permanente", | |
| insuranceProvider: "Kaiser", | |
| nextAppointment: "2026-04-01", | |
| expirationDate: "2026-05-20", | |
| clinic: "Los Angeles", | |
| lastVisit: "2026-03-10", | |
| phoneVerified: true, | |
| emailVerified: true, | |
| provider: "Dr. Williams", | |
| tags: [] | |
| }, | |
| { | |
| id: 5, | |
| firstName: "Michael", | |
| lastName: "Patel", | |
| mrn: "510992", | |
| status: "expired", | |
| insurance: "Medicare", | |
| insuranceProvider: "Medicare", | |
| nextAppointment: null, | |
| expirationDate: "2026-02-10", | |
| clinic: "San Jose", | |
| lastVisit: "2026-01-20", | |
| phoneVerified: false, | |
| emailVerified: true, | |
| provider: "Dr. Brown", | |
| tags: ["billing"] | |
| }, | |
| { | |
| id: 6, | |
| firstName: "Lisa", | |
| lastName: "Nguyen", | |
| mrn: "482311", | |
| status: "active", | |
| insurance: "BlueShield", | |
| insuranceProvider: "BlueShield", | |
| nextAppointment: "2026-03-29", | |
| expirationDate: "2026-07-08", | |
| clinic: "Sacramento", | |
| lastVisit: "2026-02-28", | |
| phoneVerified: true, | |
| emailVerified: true, | |
| provider: "Dr. Johnson", | |
| tags: [] | |
| }, | |
| { | |
| id: 7, | |
| firstName: "Robert", | |
| lastName: "Walker", | |
| mrn: "881420", | |
| status: "active", | |
| insurance: "UnitedHealth", | |
| insuranceProvider: "UnitedHealth", | |
| nextAppointment: "2026-04-05", | |
| expirationDate: "2026-06-14", | |
| clinic: "Los Angeles", | |
| lastVisit: "2026-03-08", | |
| phoneVerified: true, | |
| emailVerified: false, | |
| provider: "Dr. Smith", | |
| tags: ["allergy"] | |
| }, | |
| { | |
| id: 8, | |
| firstName: "Emily", | |
| lastName: "Carter", | |
| mrn: "640199", | |
| status: "pending", | |
| insurance: "Aetna", | |
| insuranceProvider: "Aetna", | |
| nextAppointment: "2026-03-20", | |
| expirationDate: "2026-04-22", | |
| clinic: "San Diego", | |
| lastVisit: "2026-02-25", | |
| phoneVerified: true, | |
| emailVerified: true, | |
| provider: "Dr. Williams", | |
| tags: ["new"] | |
| }, | |
| { | |
| id: 9, | |
| firstName: "Daniel", | |
| lastName: "Kim", | |
| mrn: "777200", | |
| status: "approved", | |
| insurance: "BlueCross", | |
| insuranceProvider: "BlueCross", | |
| nextAppointment: "2026-04-03", | |
| expirationDate: "2026-05-30", | |
| clinic: "San Francisco", | |
| lastVisit: "2026-03-12", | |
| phoneVerified: true, | |
| emailVerified: true, | |
| provider: "Dr. Brown", | |
| tags: ["telehealth"] | |
| }, | |
| { | |
| id: 10, | |
| firstName: "Jessica", | |
| lastName: "Brown", | |
| mrn: "223901", | |
| status: "archived", | |
| insurance: "Kaiser Permanente", | |
| insuranceProvider: "Kaiser", | |
| nextAppointment: null, | |
| expirationDate: null, | |
| clinic: "Fresno", | |
| lastVisit: "2025-12-15", | |
| phoneVerified: false, | |
| emailVerified: false, | |
| provider: "Dr. Johnson", | |
| tags: [] | |
| }, | |
| { | |
| id: 11, | |
| firstName: "Carlos", | |
| lastName: "Ramirez", | |
| mrn: "100239", | |
| status: "active", | |
| insurance: "Medicare", | |
| insuranceProvider: "Medicare", | |
| nextAppointment: "2026-03-25", | |
| expirationDate: "2026-05-05", | |
| clinic: "San Jose", | |
| lastVisit: "2026-02-20", | |
| phoneVerified: true, | |
| emailVerified: true, | |
| provider: "Dr. Smith", | |
| tags: [] | |
| }, | |
| { | |
| id: 12, | |
| firstName: "Olivia", | |
| lastName: "Taylor", | |
| mrn: "564009", | |
| status: "active", | |
| insurance: "BlueShield", | |
| insuranceProvider: "BlueShield", | |
| nextAppointment: "2026-04-04", | |
| expirationDate: "2026-06-18", | |
| clinic: "Los Angeles", | |
| lastVisit: "2026-03-15", | |
| phoneVerified: true, | |
| emailVerified: true, | |
| provider: "Dr. Williams", | |
| tags: ["new", "telehealth"] | |
| } | |
| ]; | |
| // State Management | |
| let currentPage = 1; | |
| let pageSize = 12; | |
| let selectedRows = new Set(); | |
| let filteredPatients = [...patients]; | |
| let currentFilters = { | |
| search: '', | |
| status: 'all', | |
| tag: 'all', | |
| insurance: 'all', | |
| provider: 'all', | |
| date: 'all' | |
| }; | |
| // Avatar Color Generator | |
| function getAvatarColor(id) { | |
| const colors = [ | |
| { bg: '#DBEAFE', text: '#1D4ED8' }, | |
| { bg: '#EDE9FE', text: '#6D28D9' }, | |
| { bg: '#FCE7F3', text: '#BE185D' }, | |
| { bg: '#D1FAE5', text: '#047857' }, | |
| { bg: '#FEF3C7', text: '#B45309' }, | |
| { bg: '#E0E7FF', text: '#4338CA' } | |
| ]; | |
| return colors[id % colors.length]; | |
| } | |
| // Date Formatting | |
| function formatDate(dateString) { | |
| if (!dateString) return '—'; | |
| const date = new Date(dateString); | |
| return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); | |
| } | |
| // Check if date is within 30 days or past due | |
| function getExpirationClass(dateString) { | |
| if (!dateString) return ''; | |
| const expDate = new Date(dateString); | |
| const today = new Date(); | |
| const diffTime = expDate - today; | |
| const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); | |
| if (diffDays < 0) return 'expiration-danger'; | |
| if (diffDays <= 30) return 'expiration-warning'; | |
| return ''; | |
| } | |
| function getExpirationText(dateString) { | |
| if (!dateString) return '—'; | |
| const expDate = new Date(dateString); | |
| const today = new Date(); | |
| const diffTime = expDate - today; | |
| const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); | |
| if (diffDays < 0) return 'Expired'; | |
| return formatDate(dateString); | |
| } | |
| // Render Patient Row | |
| function renderPatientRow(patient) { | |
| const avatarColors = getAvatarColor(patient.id); | |
| const initials = `${patient.firstName[0]}${patient.lastName[0]}`; | |
| const isSelected = selectedRows.has(patient.id); | |
| const expirationClass = getExpirationClass(patient.expirationDate); | |
| const expirationText = getExpirationText(patient.expirationDate); | |
| return ` | |
| <div class="patient-row grid grid-cols-12 gap-4 px-6 py-4 items-center bg-white border-l-3 border-transparent cursor-pointer ${isSelected ? 'selected' : ''}" | |
| onclick="handleRowClick(event, ${patient.id})" | |
| data-id="${patient.id}"> | |
| <!-- Patient Column --> | |
| <div class="col-span-4 flex items-center gap-3"> | |
| <input type="checkbox" | |
| class="row-checkbox w-4 h-4 rounded border-gray-300 text-brand-600 focus:ring-brand-600 cursor-pointer" | |
| ${isSelected ? 'checked' : ''} | |
| onclick="event.stopPropagation(); toggleRowSelection(${patient.id})"> | |
| <div class="relative flex-shrink-0"> | |
| <div class="w-10 h-10 rounded-full flex items-center justify-center text-sm font-semibold" | |
| style="background-color: ${avatarColors.bg}; color: ${avatarColors.text};"> | |
| ${initials} | |
| </div> | |
| <div class="avatar-status ${patient.status}"></div> | |
| </div> | |
| <div class="min-w-0 flex-1"> | |
| <div class="flex items-center gap-2"> | |
| <span class="text-sm font-semibold text-gray-900 truncate"> | |
| ${patient.firstName} ${patient.lastName} | |
| </span> | |
| ${patient.phoneVerified ? '<i data-lucide="phone" class="w-3 h-3 text-green-500"></i>' : ''} | |
| ${patient.emailVerified ? '<i data-lucide="mail" class="w-3 h-3 text-blue-500"></i>' : ''} | |
| </div> | |
| <div class="text-xs text-gray-500 mt-0.5">MRN ${patient.mrn}</div> | |
| <div class="text-xs text-gray-400 mt-0.5 truncate"> | |
| Last visit ${formatDate(patient.lastVisit)} • ${patient.clinic} Clinic | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Status Column --> | |
| <div class="col-span-1"> | |
| <span class="status-chip ${patient.status}"> | |
| ${patient.status.charAt(0).toUpperCase() + patient.status.slice(1)} | |
| </span> | |
| </div> | |
| <!-- Insurance Column --> | |
| <div class="col-span-2 flex items-center gap-2 text-sm text-gray-700"> | |
| <i data-lucide="shield" class="w-4 h-4 text-gray-400"></i> | |
| <span class="truncate">${patient.insurance}</span> | |
| </div> | |
| <!-- Next Appointment Column --> | |
| <div class="col-span-2 text-sm text-gray-700"> | |
| ${formatDate(patient.nextAppointment)} | |
| </div> | |
| <!-- Expires Column --> | |
| <div class="col-span-1 text-sm ${expirationClass}"> | |
| ${expirationText} | |
| </div> | |
| <!-- Clinic Column --> | |
| <div class="col-span-1 text-sm text-gray-700"> | |
| ${patient.clinic} | |
| </div> | |
| <!-- Actions Column --> | |
| <div class="col-span-1 flex justify-end relative"> | |
| <button onclick="event.stopPropagation(); toggleDropdown(event, ${patient.id})" | |
| class="p-2 hover:bg-gray-100 rounded-lg transition-colors"> | |
| <i data-lucide="more-vertical" class="w-4 h-4 text-gray-500"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| } | |
| // Filter Logic | |
| function applyFilters() { | |
| filteredPatients = patients.filter(patient => { | |
| // Search filter | |
| if (currentFilters.search) { | |
| const searchLower = currentFilters.search.toLowerCase(); | |
| const fullName = `${patient.firstName} ${patient.lastName}`.toLowerCase(); | |
| const match = fullName.includes(searchLower) || | |
| patient.mrn.includes(searchLower) || | |
| patient.clinic.toLowerCase().includes(searchLower); | |
| if (!match) return false; | |
| } | |
| // Status filter | |
| if (currentFilters.status !== 'all' && patient.status !== currentFilters.status) { | |
| return false; | |
| } | |
| // Insurance filter | |
| if (currentFilters.insurance !== 'all' && patient.insuranceProvider !== currentFilters.insurance) { | |
| return false; | |
| } | |
| // Provider filter | |
| if (currentFilters.provider !== 'all' && patient.provider !== currentFilters.provider) { | |
| return false; | |
| } | |
| // Tag filter | |
| if (currentFilters.tag !== 'all' && !patient.tags.includes(currentFilters.tag)) { | |
| return false; | |
| } | |
| return true; | |
| }); | |
| currentPage = 1; | |
| render(); | |
| } | |
| // Render List | |
| function render() { | |
| const container = document.getElementById('patientList'); | |
| const start = (currentPage - 1) * pageSize; | |
| const end = start + pageSize; | |
| const pagePatients = filteredPatients.slice(start, end); | |
| if (pagePatients.length === 0) { | |
| container.innerHTML = ''; | |
| document.getElementById('emptyState').classList.remove('hidden'); | |
| } else { | |
| document.getElementById('emptyState').classList.add('hidden'); | |
| container.innerHTML = pagePatients.map(p => renderPatientRow(p)).join(''); | |
| lucide.createIcons(); | |
| } | |
| updatePagination(); | |
| updateBulkActions(); | |
| } | |
| // Pagination | |
| function updatePagination() { | |
| const total = filteredPatients.length; | |
| const start = (currentPage - 1) * pageSize + 1; | |
| const end = Math.min(currentPage * pageSize, total); | |
| document.getElementById('showingStart').textContent = total === 0 ? 0 : start; | |
| document.getElementById('showingEnd').textContent = end; | |
| document.getElementById('totalCount').textContent = total; | |
| document.getElementById('prevBtn').disabled = currentPage === 1; | |
| document.getElementById('nextBtn').disabled = end >= total; | |
| // Page numbers | |
| const totalPages = Math.ceil(total / pageSize); | |
| const pageContainer = document.getElementById('pageNumbers'); | |
| let html = ''; | |
| for (let i = 1; i <= totalPages; i++) { | |
| if (i === 1 || i === totalPages || (i >= currentPage - 1 && i <= currentPage + 1)) { | |
| html += `<button onclick="goToPage(${i})" class="w-8 h-8 flex items-center justify-center rounded-lg text-sm font-medium transition-colors ${i === currentPage ? 'bg-brand-600 text-white' : 'text-gray-700 hover:bg-gray-100'}">${i}</button>`; | |
| } else if (i === currentPage - 2 || i === currentPage + 2) { | |
| html += `<span class="px-1 text-gray-400">...</span>`; | |
| } | |
| } | |
| pageContainer.innerHTML = html; | |
| } | |
| function changePage(delta) { | |
| currentPage += delta; | |
| render(); | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| } | |
| function goToPage(page) { | |
| currentPage = page; | |
| render(); | |
| window.scrollTo({ top: 0, behavior: 'smooth' }); | |
| } | |
| function changePageSize() { | |
| pageSize = parseInt(document.getElementById('pageSize').value); | |
| currentPage = 1; | |
| render(); | |
| } | |
| // Row Selection | |
| function toggleRowSelection(id) { | |
| if (selectedRows.has(id)) { | |
| selectedRows.delete(id); | |
| } else { | |
| selectedRows.add(id); | |
| } | |
| render(); | |
| } | |
| function handleRowClick(event, id) { | |
| // Navigate to patient details | |
| showToast(`Navigating to /patients/${id}`, 'info'); | |
| // Simulate navigation | |
| console.log(`Navigate to /patients/${id}`); | |
| } | |
| function toggleSelectAll() { | |
| const checkboxes = document.querySelectorAll('.row-checkbox'); | |
| const allSelected = selectedRows.size === filteredPatients.length; | |
| if (allSelected) { | |
| selectedRows.clear(); | |
| } else { | |
| filteredPatients.forEach(p => selectedRows.add(p.id)); | |
| } | |
| render(); | |
| } | |
| function clearSelection() { | |
| selectedRows.clear(); | |
| render(); | |
| } | |
| function updateBulkActions() { | |
| const bar = document.getElementById('bulkActionsBar'); | |
| const count = document.getElementById('selectedCount'); | |
| if (selectedRows.size > 0) { | |
| bar.classList.remove('hidden'); | |
| count.textContent = selectedRows.size; | |
| } else { | |
| bar.classList.add('hidden'); | |
| } | |
| // Update select all checkbox | |
| const selectAllCheckbox = document.getElementById('selectAll'); | |
| if (filteredPatients.length > 0 && selectedRows.size === filteredPatients.length) { | |
| selectAllCheckbox.checked = true; | |
| selectAllCheckbox.indeterminate = false; | |
| } else if (selectedRows.size > 0) { | |
| selectAllCheckbox.checked = false; | |
| selectAllCheckbox.indeterminate = true; | |
| } else { | |
| selectAllCheckbox.checked = false; | |
| selectAllCheckbox.indeterminate = false; | |
| } | |
| } | |
| function bulkAction(action) { | |
| showToast(`${action.charAt(0).toUpperCase() + action.slice(1)} ${selectedRows.size} patients (demo)`, 'success'); | |
| if (action === 'archive') { | |
| clearSelection(); | |
| } | |
| } | |
| // Dropdown Menu | |
| let activeDropdown = null; | |
| function toggleDropdown(event, patientId) { | |
| event.stopPropagation(); | |
| // Close existing | |
| if (activeDropdown) { | |
| activeDropdown.remove(); | |
| activeDropdown = null; | |
| return; | |
| } | |
| // Create new dropdown | |
| const template = document.getElementById('dropdownTemplate'); | |
| const dropdown = template.cloneNode(true); | |
| dropdown.id = 'activeDropdown'; | |
| dropdown.classList.remove('hidden'); | |
| // Position | |
| const button = event.currentTarget; | |
| const rect = button.getBoundingClientRect(); | |
| dropdown.style.position = 'fixed'; | |
| dropdown.style.top = `${rect.bottom + 4}px`; | |
| dropdown.style.right = `${window.innerWidth - rect.right}px`; | |
| dropdown.style.zIndex = '100'; | |
| document.body.appendChild(dropdown); | |
| activeDropdown = dropdown; | |
| lucide.createIcons(); | |
| // Close on outside click | |
| setTimeout(() => { | |
| document.addEventListener('click', closeDropdown, { once: true }); | |
| }, 0); | |
| } | |
| function closeDropdown() { | |
| if (activeDropdown) { | |
| activeDropdown.remove(); | |
| activeDropdown = null; | |
| } | |
| } | |
| function handleAction(action) { | |
| closeDropdown(); | |
| const messages = { | |
| view: 'Viewing patient details', | |
| edit: 'Opening patient editor', | |
| schedule: 'Opening scheduler', | |
| telehealth: 'Starting telehealth session', | |
| invoice: 'Invoice created (demo)', | |
| message: 'Message composer opened', | |
| upload: 'Document upload opened', | |
| archive: 'Patient archived (demo)' | |
| }; | |
| showToast(messages[action] || 'Action completed', action === 'archive' ? 'warning' : 'success'); | |
| } | |
| // More Filters Toggle | |
| function toggleMoreFilters() { | |
| const panel = document.getElementById('moreFilters'); | |
| panel.classList.toggle('hidden'); | |
| } | |
| // Toast Notifications | |
| function showToast(message, type = 'info') { | |
| const container = document.getElementById('toast-container'); | |
| const toast = document.createElement('div'); | |
| const colors = { | |
| success: 'bg-green-50 text-green-800 border-green-200', | |
| warning: 'bg-amber-50 text-amber-800 border-amber-200', | |
| error: 'bg-red-50 text-red-800 border-red-200', | |
| info: 'bg-blue-50 text-blue-800 border-blue-200' | |
| }; | |
| const icons = { | |
| success: 'check-circle', | |
| warning: 'alert-triangle', | |
| error: 'x-circle', | |
| info: 'info' | |
| }; | |
| toast.className = `${colors[type]} border rounded-lg px-4 py-3 shadow-lg flex items-center gap-3 min-w-[300px] animate-fade-up`; | |
| toast.innerHTML = ` | |
| <i data-lucide="${icons[type]}" class="w-5 h-5"></i> | |
| <span class="text-sm font-medium">${message}</span> | |
| `; | |
| container.appendChild(toast); | |
| lucide.createIcons(); | |
| setTimeout(() => { | |
| toast.style.opacity = '0'; | |
| toast.style.transform = 'translateX(100%)'; | |
| toast.style.transition = 'all 300ms ease'; | |
| setTimeout(() => toast.remove(), 300); | |
| }, 3000); | |
| } | |
| // Event Listeners | |
| document.getElementById('searchInput').addEventListener('input', (e) => { | |
| currentFilters.search = e.target.value; | |
| applyFilters(); | |
| }); | |
| document.getElementById('statusFilter').addEventListener('change', (e) => { | |
| currentFilters.status = e.target.value; | |
| applyFilters(); | |
| }); | |
| document.getElementById('insuranceFilter').addEventListener('change', (e) => { | |
| currentFilters.insurance = e.target.value; | |
| applyFilters(); | |
| }); | |
| document.getElementById('providerFilter').addEventListener('change', (e) => { | |
| currentFilters.provider = e.target.value; | |
| applyFilters(); | |
| }); | |
| document.getElementById('tagFilter').addEventListener('change', (e) => { | |
| currentFilters.tag = e.target.value; | |
| applyFilters(); | |
| }); | |
| document.getElementById('selectAll').addEventListener('change', toggleSelectAll); | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| render(); | |
| lucide.createIcons(); | |
| }); |