Spaces:
Running
Running
| // Sample patient data | |
| let patients = [ | |
| { | |
| id: 1, | |
| name: "Emma Thompson", | |
| avatar: "http://static.photos/people/200x200/1", | |
| tags: ["Critical", "VIP"], | |
| created: "2024-01-15", | |
| approved: "2024-01-16", | |
| expires: "2025-01-16", | |
| notes: "Severe allergy to penicillin. Requires constant monitoring.", | |
| phone: "+1 (555) 123-4567", | |
| email: "emma.t@email.com", | |
| status: "approved" | |
| }, | |
| { | |
| id: 2, | |
| name: "James Wilson", | |
| avatar: "http://static.photos/people/200x200/2", | |
| tags: ["New Patient"], | |
| created: "2024-03-20", | |
| approved: "2024-03-21", | |
| expires: "2024-09-21", | |
| notes: "Post-surgery recovery. Physical therapy scheduled.", | |
| phone: "+1 (555) 234-5678", | |
| email: "j.wilson@email.com", | |
| status: "expired" | |
| }, | |
| { | |
| id: 3, | |
| name: "Sarah Chen", | |
| avatar: "http://static.photos/people/200x200/3", | |
| tags: ["Follow-up"], | |
| created: "2024-02-10", | |
| approved: "2024-02-11", | |
| expires: "2025-02-11", | |
| notes: "Routine diabetes check. Blood sugar levels stable.", | |
| phone: "+1 (555) 345-6789", | |
| email: "sarah.chen@email.com", | |
| status: "approved" | |
| }, | |
| { | |
| id: 4, | |
| name: "Michael Brown", | |
| avatar: "http://static.photos/people/200x200/4", | |
| tags: ["Urgent"], | |
| created: "2024-04-05", | |
| approved: "2024-04-05", | |
| expires: "2024-10-05", | |
| notes: "Cardiac monitoring required. High blood pressure history.", | |
| phone: "+1 (555) 456-7890", | |
| email: "mbrown@email.com", | |
| status: "expired" | |
| }, | |
| { | |
| id: 5, | |
| name: "Lisa Anderson", | |
| avatar: "http://static.photos/people/200x200/5", | |
| tags: ["Regular"], | |
| created: "2024-01-28", | |
| approved: "2024-01-29", | |
| expires: "2025-01-29", | |
| notes: "Annual physical completed. All vitals normal.", | |
| phone: "+1 (555) 567-8901", | |
| email: "lisa.a@email.com", | |
| status: "approved" | |
| }, | |
| { | |
| id: 6, | |
| name: "David Martinez", | |
| avatar: "http://static.photos/people/200x200/6", | |
| tags: ["New Patient", "Insurance Pending"], | |
| created: "2024-05-12", | |
| approved: "2024-05-13", | |
| expires: "2025-05-13", | |
| notes: "Initial consultation completed. Awaiting test results.", | |
| phone: "+1 (555) 678-9012", | |
| email: "d.martinez@email.com", | |
| status: "approved" | |
| }, | |
| { | |
| id: 7, | |
| name: "Jennifer Taylor", | |
| avatar: "http://static.photos/people/200x200/7", | |
| tags: ["Critical"], | |
| created: "2023-11-15", | |
| approved: "2023-11-16", | |
| expires: "2024-05-16", | |
| notes: "Oncology patient. Chemotherapy cycle 4 of 6.", | |
| phone: "+1 (555) 789-0123", | |
| email: "jtaylor@email.com", | |
| status: "expired" | |
| }, | |
| { | |
| id: 8, | |
| name: "Robert Johnson", | |
| avatar: "http://static.photos/people/200x200/8", | |
| tags: ["Senior"], | |
| created: "2024-03-01", | |
| approved: "2024-03-02", | |
| expires: "2025-03-02", | |
| notes: "Arthritis management. Mobility assistance required.", | |
| phone: "+1 (555) 890-1234", | |
| email: "r.johnson@email.com", | |
| status: "approved" | |
| }, | |
| { | |
| id: 9, | |
| name: "Amanda White", | |
| avatar: "http://static.photos/people/200x200/9", | |
| tags: ["Pediatric"], | |
| created: "2024-04-20", | |
| approved: "2024-04-21", | |
| expires: "2025-04-21", | |
| notes: "Vaccination schedule up to date. Growth chart normal.", | |
| phone: "+1 (555) 901-2345", | |
| email: "amanda.w@email.com", | |
| status: "approved" | |
| }, | |
| { | |
| id: 10, | |
| name: "Christopher Lee", | |
| avatar: "http://static.photos/people/200x200/10", | |
| tags: ["Follow-up", "Physical Therapy"], | |
| created: "2024-02-28", | |
| approved: "2024-02-29", | |
| expires: "2024-08-29", | |
| notes: "Knee replacement recovery. PT exercises 3x weekly.", | |
| phone: "+1 (555) 012-3456", | |
| email: "chris.lee@email.com", | |
| status: "expired" | |
| }, | |
| { | |
| id: 11, | |
| name: "Maria Garcia", | |
| avatar: "http://static.photos/people/200x200/11", | |
| tags: ["Prenatal"], | |
| created: "2024-01-10", | |
| approved: "2024-01-11", | |
| expires: "2024-10-11", | |
| notes: "Second trimester. Ultrasound scheduled next week.", | |
| phone: "+1 (555) 111-2222", | |
| email: "maria.g@email.com", | |
| status: "approved" | |
| }, | |
| { | |
| id: 12, | |
| name: "Kevin Davis", | |
| avatar: "http://static.photos/people/200x200/12", | |
| tags: ["Emergency"], | |
| created: "2024-05-15", | |
| approved: "2024-05-15", | |
| expires: "2024-11-15", | |
| notes: "Fractured wrist. Cast applied, follow-up in 2 weeks.", | |
| phone: "+1 (555) 222-3333", | |
| email: "kevin.d@email.com", | |
| status: "expired" | |
| } | |
| ]; | |
| // State management | |
| let currentPage = 1; | |
| let itemsPerPage = 10; | |
| let sortColumn = null; | |
| let sortDirection = 'asc'; | |
| let filteredData = [...patients]; | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| renderTable(); | |
| lucide.createIcons(); | |
| }); | |
| // Sidebar toggle for mobile | |
| function toggleSidebar() { | |
| const sidebar = document.getElementById('sidebar'); | |
| const overlay = document.getElementById('mobile-overlay'); | |
| const isClosed = sidebar.classList.contains('-translate-x-full'); | |
| if (isClosed) { | |
| sidebar.classList.remove('-translate-x-full'); | |
| overlay.classList.remove('hidden'); | |
| } else { | |
| sidebar.classList.add('-translate-x-full'); | |
| overlay.classList.add('hidden'); | |
| } | |
| } | |
| // Sorting functionality | |
| function sortTable(column) { | |
| // Reset all sort icons | |
| document.querySelectorAll('.sort-icon').forEach(icon => { | |
| icon.setAttribute('data-lucide', 'chevrons-up-down'); | |
| icon.classList.remove('sort-asc', 'sort-desc'); | |
| }); | |
| // Toggle direction if same column | |
| if (sortColumn === column) { | |
| sortDirection = sortDirection === 'asc' ? 'desc' : 'asc'; | |
| } else { | |
| sortColumn = column; | |
| sortDirection = 'asc'; | |
| } | |
| // Update icon for current column | |
| const currentIcon = document.getElementById(`sort-${column}`); | |
| if (currentIcon) { | |
| currentIcon.setAttribute('data-lucide', sortDirection === 'asc' ? 'chevron-up' : 'chevron-down'); | |
| currentIcon.classList.add(sortDirection === 'asc' ? 'sort-asc' : 'sort-desc'); | |
| } | |
| // Sort data | |
| filteredData.sort((a, b) => { | |
| let valA = a[column]; | |
| let valB = b[column]; | |
| if (column === 'name') { | |
| valA = a.name.toLowerCase(); | |
| valB = b.name.toLowerCase(); | |
| } | |
| if (valA < valB) return sortDirection === 'asc' ? -1 : 1; | |
| if (valA > valB) return sortDirection === 'asc' ? 1 : -1; | |
| return 0; | |
| }); | |
| lucide.createIcons(); | |
| renderTable(); | |
| } | |
| // Filter functionality | |
| function filterTable() { | |
| const searchTerm = document.getElementById('searchInput').value.toLowerCase(); | |
| const statusFilter = document.getElementById('statusFilter').value; | |
| filteredData = patients.filter(patient => { | |
| const matchesSearch = patient.name.toLowerCase().includes(searchTerm) || | |
| patient.email.toLowerCase().includes(searchTerm) || | |
| patient.phone.includes(searchTerm); | |
| const matchesStatus = statusFilter === 'all' || patient.status === statusFilter; | |
| return matchesSearch && matchesStatus; | |
| }); | |
| currentPage = 1; | |
| renderTable(); | |
| } | |
| // Render table | |
| function renderTable() { | |
| const tbody = document.getElementById('tableBody'); | |
| const start = (currentPage - 1) * itemsPerPage; | |
| const end = start + itemsPerPage; | |
| const paginatedData = filteredData.slice(start, end); | |
| tbody.innerHTML = paginatedData.map(patient => { | |
| const statusColors = { | |
| approved: 'bg-emerald-100 text-emerald-800 border-emerald-200', | |
| expired: 'bg-rose-100 text-rose-800 border-rose-200' | |
| }; | |
| const statusIcons = { | |
| approved: 'check-circle', | |
| expired: 'x-circle' | |
| }; | |
| const tagColors = { | |
| 'Critical': 'bg-red-100 text-red-700', | |
| 'VIP': 'bg-purple-100 text-purple-700', | |
| 'New Patient': 'bg-blue-100 text-blue-700', | |
| 'Follow-up': 'bg-amber-100 text-amber-700', | |
| 'Urgent': 'bg-orange-100 text-orange-700', | |
| 'Regular': 'bg-gray-100 text-gray-700', | |
| 'Insurance Pending': 'bg-yellow-100 text-yellow-700', | |
| 'Senior': 'bg-indigo-100 text-indigo-700', | |
| 'Pediatric': 'bg-pink-100 text-pink-700', | |
| 'Prenatal': 'bg-teal-100 text-teal-700', | |
| 'Emergency': 'bg-red-100 text-red-700', | |
| 'Physical Therapy': 'bg-cyan-100 text-cyan-700' | |
| }; | |
| return ` | |
| <tr class="table-row transition-colors border-b border-gray-100 last:border-0"> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="flex items-center"> | |
| <img class="h-10 w-10 rounded-full object-cover border-2 border-gray-200" src="${patient.avatar}" alt=""> | |
| <div class="ml-4"> | |
| <div class="text-sm font-semibold text-gray-900">${patient.name}</div> | |
| <div class="text-xs text-gray-500">ID: #${String(patient.id).padStart(4, '0')}</div> | |
| </div> | |
| </div> | |
| </td> | |
| <td class="px-6 py-4"> | |
| <div class="flex flex-wrap gap-1"> | |
| ${patient.tags.map(tag => ` | |
| <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${tagColors[tag] || 'bg-gray-100 text-gray-700'}"> | |
| ${tag} | |
| </span> | |
| `).join('')} | |
| </div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600"> | |
| ${formatDate(patient.created)} | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600"> | |
| ${formatDate(patient.approved)} | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-600"> | |
| ${formatDate(patient.expires)} | |
| </td> | |
| <td class="px-6 py-4"> | |
| <div class="text-sm text-gray-600 max-w-xs truncate" title="${patient.notes}"> | |
| ${patient.notes} | |
| </div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="text-sm text-gray-900">${patient.phone}</div> | |
| <div class="text-sm text-gray-500">${patient.email}</div> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <span class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium border ${statusColors[patient.status]} capitalize"> | |
| <i data-lucide="${statusIcons[patient.status]}" class="w-3 h-3 mr-1"></i> | |
| ${patient.status} | |
| </span> | |
| </td> | |
| <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> | |
| <div class="flex items-center justify-end space-x-2"> | |
| <button onclick="viewPatient(${patient.id})" class="text-blue-600 hover:text-blue-900 p-1 hover:bg-blue-50 rounded transition-colors" title="View"> | |
| <i data-lucide="eye" class="w-4 h-4"></i> | |
| </button> | |
| <button onclick="editPatient(${patient.id})" class="text-gray-600 hover:text-gray-900 p-1 hover:bg-gray-100 rounded transition-colors" title="Edit"> | |
| <i data-lucide="edit-2" class="w-4 h-4"></i> | |
| </button> | |
| <button onclick="deletePatient(${patient.id})" class="text-red-600 hover:text-red-900 p-1 hover:bg-red-50 rounded transition-colors" title="Delete"> | |
| <i data-lucide="trash-2" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| </td> | |
| </tr> | |
| `; | |
| }).join(''); | |
| // Update pagination info | |
| document.getElementById('showingStart').textContent = filteredData.length > 0 ? start + 1 : 0; | |
| document.getElementById('showingEnd').textContent = Math.min(end, filteredData.length); | |
| document.getElementById('totalItems').textContent = filteredData.length; | |
| // Update pagination buttons | |
| document.getElementById('prevBtn').disabled = currentPage === 1; | |
| document.getElementById('nextBtn').disabled = end >= filteredData.length; | |
| renderPaginationNumbers(); | |
| lucide.createIcons(); | |
| } | |
| function renderPaginationNumbers() { | |
| const totalPages = Math.ceil(filteredData.length / itemsPerPage); | |
| const container = document.getElementById('paginationNumbers'); | |
| 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 rounded-md text-sm font-medium transition-colors ${currentPage === i ? 'bg-blue-600 text-white' : 'text-gray-700 hover:bg-gray-100 border border-gray-300'}"> | |
| ${i} | |
| </button> | |
| `; | |
| } else if (i === currentPage - 2 || i === currentPage + 2) { | |
| html += `<span class="px-2 text-gray-400">...</span>`; | |
| } | |
| } | |
| container.innerHTML = html; | |
| } | |
| function changePage(direction) { | |
| const totalPages = Math.ceil(filteredData.length / itemsPerPage); | |
| const newPage = currentPage + direction; | |
| if (newPage >= 1 && newPage <= totalPages) { | |
| currentPage = newPage; | |
| renderTable(); | |
| } | |
| } | |
| function goToPage(page) { | |
| currentPage = page; | |
| renderTable(); | |
| } | |
| function formatDate(dateString) { | |
| const options = { year: 'numeric', month: 'short', day: 'numeric' }; | |
| return new Date(dateString).toLocaleDateString('en-US', options); | |
| } | |
| // Action handlers | |
| function viewPatient(id) { | |
| showToast('Viewing patient details...'); | |
| } | |
| function editPatient(id) { | |
| showToast('Opening edit mode...'); | |
| } | |
| function deletePatient(id) { | |
| if (confirm('Are you sure you want to delete this patient record?')) { | |
| patients = patients.filter(p => p.id !== id); | |
| filterTable(); | |
| showToast('Patient deleted successfully'); | |
| } | |
| } | |
| function openAddModal() { | |
| document.getElementById('addModal').classList.remove('hidden'); | |
| } | |
| function closeAddModal() { | |
| document.getElementById('addModal').classList.add('hidden'); | |
| } | |
| function savePatient() { | |
| closeAddModal(); | |
| showToast('New patient added successfully'); | |
| } | |
| function exportData() { | |
| showToast('Exporting patient data to CSV...'); | |
| } | |
| 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 modal on escape key | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape') { | |
| closeAddModal(); | |
| } | |
| }); |