Spaces:
Running
Running
| // Sample data | |
| const appointments = [ | |
| { | |
| id: 'APT-2024-0092', | |
| date: '2024-01-15', | |
| time: '09:00 AM', | |
| duration: '30 min', | |
| provider: { name: 'Dr. Emily Chen', specialty: 'Internal Medicine', avatar: 'http://static.photos/people/320x240/2' }, | |
| category: 'Primary Care', | |
| type: 'In-person', | |
| location: 'Main Clinic, Room 304', | |
| status: 'Confirmed', | |
| notes: 2, | |
| reason: 'Annual physical examination', | |
| isUpcoming: true, | |
| history: [ | |
| { status: 'Created', time: 'Jan 8, 2024 2:30 PM', user: 'System' }, | |
| { status: 'Confirmed', time: 'Jan 8, 2024 2:35 PM', user: 'Dr. Chen' } | |
| ] | |
| }, | |
| { | |
| id: 'APT-2024-0095', | |
| date: '2024-01-18', | |
| time: '02:30 PM', | |
| duration: '45 min', | |
| provider: { name: 'Dr. James Wilson', specialty: 'Dermatology', avatar: 'http://static.photos/people/320x240/3' }, | |
| category: 'Dermatology', | |
| type: 'Telehealth', | |
| location: 'Video Consultation', | |
| status: 'Pending', | |
| notes: 0, | |
| reason: 'Rash follow-up consultation', | |
| isUpcoming: true, | |
| history: [ | |
| { status: 'Created', time: 'Jan 10, 2024 10:00 AM', user: 'Sarah Mitchell' } | |
| ] | |
| }, | |
| { | |
| id: 'APT-2024-0100', | |
| date: '2024-02-05', | |
| time: '11:00 AM', | |
| duration: '60 min', | |
| provider: { name: 'Dr. Sarah Park', specialty: 'Lab Services', avatar: 'http://static.photos/people/320x240/4' }, | |
| category: 'Lab', | |
| type: 'In-person', | |
| location: 'Lab Center, 2nd Floor', | |
| status: 'Confirmed', | |
| notes: 1, | |
| reason: 'Blood work and cholesterol panel', | |
| isUpcoming: true, | |
| history: [ | |
| { status: 'Created', time: 'Jan 20, 2024 9:00 AM', user: 'System' }, | |
| { status: 'Confirmed', time: 'Jan 20, 2024 9:15 AM', user: 'Staff' } | |
| ] | |
| }, | |
| { | |
| id: 'APT-2024-0085', | |
| date: '2023-12-15', | |
| time: '10:00 AM', | |
| duration: '30 min', | |
| provider: { name: 'Dr. Emily Chen', specialty: 'Internal Medicine', avatar: 'http://static.photos/people/320x240/2' }, | |
| category: 'Follow-up', | |
| type: 'In-person', | |
| location: 'Main Clinic, Room 304', | |
| status: 'Completed', | |
| notes: 3, | |
| reason: 'Medication review', | |
| isUpcoming: false, | |
| history: [ | |
| { status: 'Created', time: 'Dec 1, 2023 2:00 PM', user: 'System' }, | |
| { status: 'Confirmed', time: 'Dec 1, 2023 2:05 PM', user: 'Dr. Chen' }, | |
| { status: 'Checked-in', time: 'Dec 15, 2023 9:45 AM', user: 'Reception' }, | |
| { status: 'Completed', time: 'Dec 15, 2023 10:30 AM', user: 'Dr. Chen' } | |
| ] | |
| }, | |
| { | |
| id: 'APT-2024-0078', | |
| date: '2023-11-20', | |
| time: '03:00 PM', | |
| duration: '30 min', | |
| provider: { name: 'Dr. James Wilson', specialty: 'Dermatology', avatar: 'http://static.photos/people/320x240/3' }, | |
| category: 'Dermatology', | |
| type: 'In-person', | |
| location: 'Dermatology Suite, Room 105', | |
| status: 'Completed', | |
| notes: 2, | |
| reason: 'Skin examination', | |
| isUpcoming: false, | |
| history: [ | |
| { status: 'Created', time: 'Nov 10, 2023 11:00 AM', user: 'System' }, | |
| { status: 'Confirmed', time: 'Nov 10, 2023 11:05 AM', user: 'Auto-confirm' }, | |
| { status: 'Completed', time: 'Nov 20, 2023 3:30 PM', user: 'Dr. Wilson' } | |
| ] | |
| }, | |
| { | |
| id: 'APT-2024-0072', | |
| date: '2023-10-28', | |
| time: '09:30 AM', | |
| duration: '30 min', | |
| provider: { name: 'Dr. Emily Chen', specialty: 'Internal Medicine', avatar: 'http://static.photos/people/320x240/2' }, | |
| category: 'Primary Care', | |
| type: 'Telehealth', | |
| location: 'Phone Consultation', | |
| status: 'Canceled', | |
| notes: 0, | |
| reason: 'Prescription refill', | |
| isUpcoming: false, | |
| history: [ | |
| { status: 'Created', time: 'Oct 20, 2023 9:00 AM', user: 'Patient Portal' }, | |
| { status: 'Confirmed', time: 'Oct 20, 2023 9:05 AM', user: 'System' }, | |
| { status: 'Canceled', time: 'Oct 27, 2023 4:00 PM', user: 'Sarah Mitchell' } | |
| ] | |
| } | |
| ]; | |
| // State | |
| let currentView = 'list'; | |
| let currentCalendarView = 'month'; | |
| let selectedAppointment = null; | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', () => { | |
| renderListView(); | |
| renderCalendar(); | |
| renderUpcomingWeek(); | |
| // Re-initialize icons after all rendering is done | |
| if (typeof lucide !== 'undefined') { | |
| lucide.createIcons(); | |
| } | |
| }); | |
| // Sidebar Toggle | |
| function toggleSidebar() { | |
| const sidebar = document.getElementById('sidebar'); | |
| const overlay = document.getElementById('sidebarOverlay'); | |
| sidebar.classList.toggle('-translate-x-full'); | |
| overlay.classList.toggle('hidden'); | |
| } | |
| // Profile Dropdown | |
| function toggleProfileDropdown() { | |
| const dropdown = document.getElementById('profileDropdown'); | |
| dropdown.classList.toggle('hidden'); | |
| } | |
| // More Dropdown | |
| function toggleMoreDropdown() { | |
| const dropdown = document.getElementById('moreDropdown'); | |
| dropdown.classList.toggle('hidden'); | |
| } | |
| // Close dropdowns when clicking outside | |
| document.addEventListener('click', (e) => { | |
| if (!e.target.closest('.relative')) { | |
| const profileDropdown = document.getElementById('profileDropdown'); | |
| const moreDropdown = document.getElementById('moreDropdown'); | |
| if (profileDropdown) profileDropdown.classList.add('hidden'); | |
| if (moreDropdown) moreDropdown.classList.add('hidden'); | |
| } | |
| }); | |
| // View Switching | |
| function switchView(view) { | |
| currentView = view; | |
| const listView = document.getElementById('listView'); | |
| const calendarView = document.getElementById('calendarView'); | |
| const listBtn = document.getElementById('listViewBtn'); | |
| const calendarBtn = document.getElementById('calendarViewBtn'); | |
| const sortDropdown = document.getElementById('sortDropdown'); | |
| if (view === 'list') { | |
| listView.classList.remove('hidden'); | |
| calendarView.classList.add('hidden'); | |
| listBtn.classList.add('bg-teal-50', 'text-teal-700'); | |
| listBtn.classList.remove('text-gray-600', 'hover:text-gray-900'); | |
| calendarBtn.classList.remove('bg-teal-50', 'text-teal-700'); | |
| calendarBtn.classList.add('text-gray-600', 'hover:text-gray-900'); | |
| if (sortDropdown && sortDropdown.parentElement) { | |
| sortDropdown.parentElement.style.opacity = '1'; | |
| sortDropdown.parentElement.style.pointerEvents = 'auto'; | |
| } | |
| } else { | |
| listView.classList.add('hidden'); | |
| calendarView.classList.remove('hidden'); | |
| calendarBtn.classList.add('bg-teal-50', 'text-teal-700'); | |
| calendarBtn.classList.remove('text-gray-600', 'hover:text-gray-900'); | |
| listBtn.classList.remove('bg-teal-50', 'text-teal-700'); | |
| listBtn.classList.add('text-gray-600', 'hover:text-gray-900'); | |
| if (sortDropdown && sortDropdown.parentElement) { | |
| sortDropdown.parentElement.style.opacity = '0.5'; | |
| sortDropdown.parentElement.style.pointerEvents = 'none'; | |
| } | |
| } | |
| if (typeof lucide !== 'undefined') { | |
| lucide.createIcons(); | |
| } | |
| } | |
| // Render List View | |
| function renderListView() { | |
| const upcomingList = document.getElementById('upcomingList'); | |
| const historyList = document.getElementById('historyList'); | |
| const upcoming = appointments.filter(a => a.isUpcoming); | |
| const history = appointments.filter(a => !a.isUpcoming); | |
| if (upcomingList) { | |
| upcomingList.innerHTML = upcoming.map(appt => createAppointmentRow(appt)).join(''); | |
| } | |
| if (historyList) { | |
| historyList.innerHTML = history.map(appt => createAppointmentRow(appt)).join(''); | |
| } | |
| if (typeof lucide !== 'undefined') { | |
| lucide.createIcons(); | |
| } | |
| } | |
| function createAppointmentRow(appt) { | |
| if (!appt) return ''; | |
| const statusColors = { | |
| 'Confirmed': 'bg-green-100 text-green-700 border-green-200', | |
| 'Pending': 'bg-yellow-100 text-yellow-700 border-yellow-200', | |
| 'Completed': 'bg-gray-100 text-gray-700 border-gray-200', | |
| 'Canceled': 'bg-red-100 text-red-700 border-red-200', | |
| 'No-show': 'bg-orange-100 text-orange-700 border-orange-200' | |
| }; | |
| const categoryColors = { | |
| 'Primary Care': 'bg-blue-50 text-blue-700 border-blue-200', | |
| 'Dermatology': 'bg-purple-50 text-purple-700 border-purple-200', | |
| 'Lab': 'bg-pink-50 text-pink-700 border-pink-200', | |
| 'Follow-up': 'bg-indigo-50 text-indigo-700 border-indigo-200' | |
| }; | |
| const statusClass = statusColors[appt.status] || 'bg-gray-100 text-gray-700 border-gray-200'; | |
| const categoryClass = categoryColors[appt.category] || 'bg-gray-100 text-gray-700 border-gray-200'; | |
| return ` | |
| <tr class="hover:bg-gray-50 transition-colors cursor-pointer" onclick="openDrawer('${appt.id}')"> | |
| <td class="px-6 py-4 whitespace-nowrap"> | |
| <div class="text-sm font-medium text-gray-900">${formatDate(appt.date)}</div> | |
| <div class="text-xs text-gray-500">${appt.time || ''} • ${appt.duration || ''}</div> | |
| </td> | |
| <td class="px-6 py-4"> | |
| <div class="flex items-center gap-3"> | |
| <img src="${appt.provider?.avatar || ''}" class="w-8 h-8 rounded-full object-cover" alt=""> | |
| <div> | |
| <div class="text-sm font-medium text-gray-900">${appt.provider?.name || ''}</div> | |
| <div class="text-xs text-gray-500">${appt.provider?.specialty || ''}</div> | |
| </div> | |
| </div> | |
| </td> | |
| <td class="px-6 py-4"> | |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${categoryClass}"> | |
| ${appt.category || ''} | |
| </span> | |
| </td> | |
| <td class="px-6 py-4"> | |
| <div class="flex items-center gap-1.5 text-sm text-gray-600"> | |
| <i data-lucide="${appt.type === 'Telehealth' ? 'video' : 'map-pin'}" class="w-3.5 h-3.5"></i> | |
| ${appt.type || ''} | |
| </div> | |
| </td> | |
| <td class="px-6 py-4 text-sm text-gray-600 max-w-xs truncate"> | |
| ${appt.location || ''} | |
| </td> | |
| <td class="px-6 py-4"> | |
| <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${statusClass} status-pill"> | |
| ${appt.status || ''} | |
| </span> | |
| </td> | |
| <td class="px-6 py-4 text-center"> | |
| ${(appt.notes || 0) > 0 ? `<div class="inline-flex items-center gap-1 text-teal-600"><i data-lucide="file-text" class="w-4 h-4"></i><span class="text-xs font-medium">${appt.notes}</span></div>` : '<span class="text-gray-300">-</span>'} | |
| </td> | |
| <td class="px-6 py-4 text-right"> | |
| <div class="flex items-center justify-end gap-2"> | |
| <button onclick="event.stopPropagation(); openNewAppointmentModal()" class="p-1.5 hover:bg-gray-200 rounded-lg transition-colors text-gray-400 hover:text-gray-600"> | |
| <i data-lucide="pencil" class="w-4 h-4"></i> | |
| </button> | |
| <button onclick="event.stopPropagation()" class="p-1.5 hover:bg-gray-200 rounded-lg transition-colors text-gray-400 hover:text-gray-600"> | |
| <i data-lucide="more-vertical" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| </td> | |
| </tr> | |
| `; | |
| } | |
| // Calendar Functions | |
| function setCalendarView(view) { | |
| currentCalendarView = view; | |
| document.getElementById('monthViewBtn').className = view === 'month' ? 'px-3 py-1.5 text-sm font-medium rounded-lg bg-teal-50 text-teal-700' : 'px-3 py-1.5 text-sm font-medium rounded-lg text-gray-600 hover:bg-gray-50'; | |
| document.getElementById('weekViewBtn').className = view === 'week' ? 'px-3 py-1.5 text-sm font-medium rounded-lg bg-teal-50 text-teal-700' : 'px-3 py-1.5 text-sm font-medium rounded-lg text-gray-600 hover:bg-gray-50'; | |
| document.getElementById('dayViewBtn').className = view === 'day' ? 'px-3 py-1.5 text-sm font-medium rounded-lg bg-teal-50 text-teal-700' : 'px-3 py-1.5 text-sm font-medium rounded-lg text-gray-600 hover:bg-gray-50'; | |
| } | |
| function renderCalendar() { | |
| const grid = document.getElementById('calendarGrid'); | |
| if (!grid) return; | |
| const daysInMonth = 31; // simplified | |
| const startDay = 1; // Monday | |
| let html = ''; | |
| // Empty cells for start of month | |
| for (let i = 0; i < startDay; i++) { | |
| html += `<div class="calendar-day bg-gray-50"></div>`; | |
| } | |
| // Days | |
| for (let day = 1; day <= daysInMonth; day++) { | |
| const dayAppointments = appointments.filter(a => { | |
| if (!a || !a.date) return false; | |
| const apptDate = new Date(a.date); | |
| return apptDate && apptDate.getDate() === day; | |
| }); | |
| const isToday = day === 15; // Example | |
| html += ` | |
| <div class="calendar-day p-2 ${isToday ? 'bg-teal-50/30' : ''}"> | |
| <div class="flex items-center justify-between mb-1"> | |
| <span class="text-sm font-medium ${isToday ? 'text-teal-700 bg-teal-100 w-6 h-6 rounded-full flex items-center justify-center' : 'text-gray-700'}">${day}</span> | |
| ${dayAppointments.length > 3 ? `<span class="text-xs text-gray-500 cursor-pointer hover:text-teal-600">+${dayAppointments.length - 2} more</span>` : ''} | |
| </div> | |
| <div class="space-y-1"> | |
| ${dayAppointments.slice(0, 3).map(appt => { | |
| if (!appt || !appt.status) return ''; | |
| const statusColors = { | |
| 'Confirmed': 'border-l-teal-500', | |
| 'Pending': 'border-l-yellow-500', | |
| 'Completed': 'border-l-gray-400', | |
| 'Canceled': 'border-l-red-400' | |
| }; | |
| const lastName = appt.provider && appt.provider.name ? appt.provider.name.split(' ')[1] || '' : ''; | |
| return ` | |
| <div onclick="openDrawer('${appt.id}')" class="calendar-event text-xs p-1.5 bg-white border-l-2 ${statusColors[appt.status] || 'border-l-gray-300'} rounded shadow-sm cursor-pointer hover:bg-gray-50"> | |
| <div class="font-medium text-gray-900 truncate">${appt.time || ''}</div> | |
| <div class="text-gray-500 truncate">${lastName}</div> | |
| <div class="flex items-center gap-1 mt-0.5"> | |
| <span class="w-1.5 h-1.5 rounded-full bg-teal-500"></span> | |
| <span class="text-[10px] text-gray-400">${appt.category || ''}</span> | |
| </div> | |
| </div> | |
| `; | |
| }).join('')} | |
| </div> | |
| </div> | |
| `; | |
| } | |
| grid.innerHTML = html; | |
| } | |
| function renderUpcomingWeek() { | |
| const container = document.getElementById('upcomingWeekList'); | |
| if (!container) return; | |
| const upcoming = appointments.filter(a => a && a.isUpcoming).slice(0, 5); | |
| container.innerHTML = upcoming.map(appt => ` | |
| <div onclick="openDrawer('${appt.id}')" class="p-3 rounded-lg border border-gray-200 hover:border-teal-300 hover:shadow-sm cursor-pointer transition-all bg-white slide-in"> | |
| <div class="flex items-start justify-between mb-2"> | |
| <span class="text-xs font-semibold text-teal-700 bg-teal-50 px-2 py-0.5 rounded">${formatDayOfWeek(appt.date)}</span> | |
| <span class="text-xs text-gray-500">${appt.time || ''}</span> | |
| </div> | |
| <div class="text-sm font-medium text-gray-900 mb-0.5">${appt.category || ''}</div> | |
| <div class="text-xs text-gray-500 flex items-center gap-1"> | |
| <img src="${appt.provider?.avatar || ''}" class="w-4 h-4 rounded-full"> | |
| ${appt.provider?.name || ''} | |
| </div> | |
| </div> | |
| `).join(''); | |
| if (typeof lucide !== 'undefined') { | |
| lucide.createIcons(); | |
| } | |
| } | |
| // Drawer Functions | |
| function openDrawer(appointmentId) { | |
| const appt = appointments.find(a => a && a.id === appointmentId); | |
| if (!appt) return; | |
| selectedAppointment = appt; | |
| const overlay = document.getElementById('drawerOverlay'); | |
| const drawer = document.getElementById('appointmentDrawer'); | |
| const content = document.getElementById('drawerContent'); | |
| const apptIdEl = document.getElementById('drawerApptId'); | |
| if (!overlay || !drawer || !content || !apptIdEl) return; | |
| apptIdEl.textContent = appt.id; | |
| const statusColors = { | |
| 'Confirmed': 'text-green-700 bg-green-50 border-green-200', | |
| 'Pending': 'text-yellow-700 bg-yellow-50 border-yellow-200', | |
| 'Completed': 'text-gray-700 bg-gray-100 border-gray-200', | |
| 'Canceled': 'text-red-700 bg-red-50 border-red-200' | |
| }; | |
| const statusColors = { | |
| 'Confirmed': 'text-green-700 bg-green-50 border-green-200', | |
| 'Pending': 'text-yellow-700 bg-yellow-50 border-yellow-200', | |
| 'Completed': 'text-gray-700 bg-gray-100 border-gray-200', | |
| 'Canceled': 'text-red-700 bg-red-50 border-red-200' | |
| }; | |
| content.innerHTML = ` | |
| <div class="space-y-6"> | |
| <!-- Header Info --> | |
| <div class="text-center pb-6 border-b border-gray-100"> | |
| <div class="text-3xl font-bold text-gray-900 mb-1">${formatDateFull(appt.date)}</div> | |
| <div class="text-lg text-gray-500 mb-3">${appt.time || ''} • ${appt.duration || ''}</div> | |
| <span class="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium border ${statusColors[appt.status] || 'bg-gray-100 text-gray-700 border-gray-200'}"> | |
| ${appt.status || ''} | |
| </span> | |
| </div> | |
| <!-- Status Timeline --> | |
| <div> | |
| <h4 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-3">Status Timeline</h4> | |
| <div class="space-y-3"> | |
| ${(appt.history || []).map((h, i, arr) => ` | |
| <div class="flex gap-3"> | |
| <div class="flex flex-col items-center"> | |
| <div class="w-2.5 h-2.5 rounded-full ${i === arr.length - 1 ? 'bg-teal-500' : 'bg-gray-300'}"></div> | |
| ${i < arr.length - 1 ? '<div class="w-0.5 flex-1 bg-gray-200 my-1"></div>' : ''} | |
| </div> | |
| <div class="pb-3"> | |
| <div class="text-sm font-medium text-gray-900">${h.status || ''}</div> | |
| <div class="text-xs text-gray-500">${h.time || ''} • ${h.user || ''}</div> | |
| </div> | |
| </div> | |
| `).join('') || 'No history available'} | |
| </div> | |
| </div> | |
| <!-- Provider --> | |
| <div class="flex items-start gap-3 p-4 bg-gray-50 rounded-lg"> | |
| <img src="${appt.provider?.avatar || ''}" class="w-12 h-12 rounded-full object-cover border-2 border-white shadow-sm"> | |
| <div class="flex-1"> | |
| <div class="font-semibold text-gray-900">${appt.provider?.name || 'Unknown Provider'}</div> | |
| <div class="text-sm text-gray-500 mb-2">${appt.provider?.specialty || ''}</div> | |
| <button class="text-xs text-teal-600 hover:text-teal-700 font-medium flex items-center gap-1"> | |
| <i data-lucide="message-circle" class="w-3 h-3"></i> | |
| Message Provider | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Category & Type --> | |
| <div class="grid grid-cols-2 gap-4"> | |
| <div> | |
| <div class="text-xs text-gray-500 mb-1">Category</div> | |
| <div class="inline-flex items-center px-2.5 py-1 rounded-md text-sm font-medium bg-blue-50 text-blue-700 border border-blue-200"> | |
| ${appt.category || ''} | |
| </div> | |
| </div> | |
| <div> | |
| <div class="text-xs text-gray-500 mb-1">Type</div> | |
| <div class="flex items-center gap-1.5 text-sm font-medium text-gray-900"> | |
| <i data-lucide="${appt.type === 'Telehealth' ? 'video' : 'map-pin'}" class="w-4 h-4 text-gray-400"></i> | |
| ${appt.type || ''} | |
| </div> | |
| ${appt.type === 'Telehealth' ? '<button class="mt-2 w-full px-3 py-1.5 bg-teal-600 text-white text-xs font-medium rounded hover:bg-teal-700 transition-colors">Join Video Link</button>' : ''} | |
| </div> | |
| </div> | |
| <!-- Location --> | |
| <div> | |
| <div class="text-xs text-gray-500 mb-1 uppercase tracking-wider">Location</div> | |
| <div class="flex items-start gap-2 text-sm text-gray-900"> | |
| <i data-lucide="map-pin" class="w-4 h-4 text-gray-400 mt-0.5"></i> | |
| <span>${appt.location || ''}</span> | |
| </div> | |
| </div> | |
| <!-- Reason --> | |
| <div> | |
| <div class="text-xs text-gray-500 mb-1 uppercase tracking-wider">Reason for Visit</div> | |
| <p class="text-sm text-gray-700 bg-gray-50 p-3 rounded-lg">${appt.reason || ''}</p> | |
| </div> | |
| <!-- Notes Timeline --> | |
| <div> | |
| <div class="flex items-center justify-between mb-3"> | |
| <div class="text-xs font-semibold text-gray-500 uppercase tracking-wider">Notes</div> | |
| <button class="text-xs text-teal-600 hover:text-teal-700 font-medium">+ Add Note</button> | |
| </div> | |
| ${(appt.notes || 0) > 0 ? ` | |
| <div class="space-y-3"> | |
| <div class="border-l-2 border-teal-200 pl-3 py-1"> | |
| <div class="text-xs text-gray-500 mb-1">Jan 8, 2024 2:35 PM • Dr. Chen</div> | |
| <p class="text-sm text-gray-700">Patient confirmed appointment via portal. No special requirements noted.</p> | |
| </div> | |
| ${appt.notes > 1 ? ` | |
| <div class="border-l-2 border-gray-200 pl-3 py-1"> | |
| <div class="text-xs text-gray-500 mb-1">Jan 5, 2024 9:00 AM • Sarah M.</div> | |
| <p class="text-sm text-gray-700">Requested morning slot due to work schedule.</p> | |
| </div> | |
| ` : ''} | |
| </div> | |
| ` : '<div class="text-sm text-gray-400 italic">No notes added yet</div>'} | |
| </div> | |
| <!-- Actions --> | |
| <div class="pt-4 border-t border-gray-100 flex gap-2"> | |
| <button onclick="event.stopPropagation(); cancelAppointment('${appt.id}')" class="flex-1 px-4 py-2 border border-red-200 text-red-600 rounded-lg hover:bg-red-50 text-sm font-medium transition-colors"> | |
| Cancel Appointment | |
| </button> | |
| <button onclick="event.stopPropagation(); sendReminder('${appt.id}')" class="flex-1 px-4 py-2 border border-gray-200 text-gray-700 rounded-lg hover:bg-gray-50 text-sm font-medium transition-colors flex items-center justify-center gap-2"> | |
| <i data-lucide="bell" class="w-4 h-4"></i> | |
| Send Reminder | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| overlay.classList.remove('hidden'); | |
| setTimeout(() => overlay.classList.remove('opacity-0'), 10); | |
| drawer.classList.remove('translate-x-full'); | |
| if (typeof lucide !== 'undefined') { | |
| lucide.createIcons(); | |
| } | |
| } | |
| function closeDrawer() { | |
| const overlay = document.getElementById('drawerOverlay'); | |
| const drawer = document.getElementById('appointmentDrawer'); | |
| overlay.classList.add('opacity-0'); | |
| drawer.classList.add('translate-x-full'); | |
| setTimeout(() => { | |
| overlay.classList.add('hidden'); | |
| }, 300); | |
| selectedAppointment = null; | |
| } | |
| // Modal Functions | |
| function openNewAppointmentModal() { | |
| const modal = document.getElementById('newAppointmentModal'); | |
| modal.classList.remove('hidden'); | |
| setTimeout(() => { | |
| modal.querySelector('div[class*="scale-100"]').classList.remove('scale-95', 'opacity-0'); | |
| }, 10); | |
| } | |
| function closeNewAppointmentModal() { | |
| const modal = document.getElementById('newAppointmentModal'); | |
| modal.classList.add('hidden'); | |
| } | |
| function saveAppointment() { | |
| // Simulate saving | |
| closeNewAppointmentModal(); | |
| // Show success notification (simplified) | |
| alert('Appointment scheduled successfully!'); | |
| } | |
| // New Note Modal Functions | |
| function openNewNoteModal() { | |
| const modal = document.getElementById('newNoteModal'); | |
| if (modal) { | |
| modal.classList.remove('hidden'); | |
| if (typeof lucide !== 'undefined') { | |
| lucide.createIcons(); | |
| } | |
| } | |
| } | |
| function closeNewNoteModal() { | |
| const modal = document.getElementById('newNoteModal'); | |
| modal.classList.add('hidden'); | |
| } | |
| function saveNote() { | |
| closeNewNoteModal(); | |
| alert('Note saved successfully!'); | |
| } | |
| // Notes view toggle (for tab switching - basic implementation) | |
| function showNotesView() { | |
| document.getElementById('listView').classList.add('hidden'); | |
| document.getElementById('calendarView').classList.add('hidden'); | |
| document.getElementById('notesSection').classList.remove('hidden'); | |
| } | |
| function hideNotesView() { | |
| document.getElementById('notesSection').classList.add('hidden'); | |
| } | |
| // Helper Functions | |
| function formatDate(dateStr) { | |
| if (!dateStr) return ''; | |
| const date = new Date(dateStr); | |
| if (isNaN(date.getTime())) return ''; | |
| return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }); | |
| } | |
| function formatDateFull(dateStr) { | |
| if (!dateStr) return ''; | |
| const date = new Date(dateStr); | |
| if (isNaN(date.getTime())) return ''; | |
| return date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' }); | |
| } | |
| function formatDayOfWeek(dateStr) { | |
| if (!dateStr) return ''; | |
| const date = new Date(dateStr); | |
| if (isNaN(date.getTime())) return ''; | |
| return date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' }); | |
| } | |
| function cancelAppointment(id) { | |
| if (confirm('Are you sure you want to cancel this appointment?')) { | |
| alert('Appointment canceled'); | |
| closeDrawer(); | |
| } | |
| } | |
| function sendReminder(id) { | |
| alert('Reminder sent to patient'); | |
| } | |
| // Keyboard shortcuts | |
| document.addEventListener('keydown', (e) => { | |
| if (e.key === 'Escape') { | |
| closeDrawer(); | |
| closeNewAppointmentModal(); | |
| } | |
| }); |