thors1's picture
Initial DeepSite commit
399fd10 verified
// 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();
});