// public/admin.js (for admin.html) // Global variables let currentAdminDate = new Date(); let currentAdminMonth = currentAdminDate.getMonth(); let currentAdminYear = currentAdminDate.getFullYear(); let selectedAdminDate = null; const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; // Generate all possible time slots in chronological order (8am to 5pm) const timeSlotsData = [ '8:00am', '9:00am', '10:00am', '11:00am', '12:00pm', '1:00pm', '2:00pm', '3:00pm', '4:00pm', '5:00pm' ]; // Helper function to convert time string to minutes since midnight function timeToMinutes(timeStr) { const match = timeStr.match(/([0-9]+)(?::([0-9]+))?(am|pm)/i); if (!match) return 0; let hours = parseInt(match[1]); const minutes = parseInt(match[2] || '0'); const meridian = match[3].toLowerCase(); if (meridian === 'pm' && hours !== 12) { hours += 12; } else if (meridian === 'am' && hours === 12) { hours = 0; } return hours * 60 + minutes; } // Helper function to ensure consistent time format function formatTimeSlot(hour) { const meridian = hour >= 12 ? 'pm' : 'am'; const displayHour = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour); return `${displayHour}:00${meridian}`; } // Render the admin calendar with available dates highlighted async function renderAdminCalendar(month, year) { const adminCalendarDates = document.getElementById('adminCalendarDates'); const adminMonthYear = document.getElementById('adminMonthYear'); if (!adminCalendarDates || !adminMonthYear) { console.error('Required calendar elements not found'); return; } // Clear existing calendar adminCalendarDates.innerHTML = ''; adminMonthYear.textContent = `${months[month]} ${year}`; // Get current UID and availability const selectedUid = document.getElementById('uidSelect').value; if (!selectedUid) { // If no UID selected, just show empty calendar return; } const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); const today = new Date(); const availability = await fetchAvailability(); // Start of today (midnight) const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); // Create blank cells for days before the first day of the month for (let i = 0; i < firstDay; i++) { const blank = document.createElement('div'); blank.classList.add('date-item', 'p-2', 'rounded-pill', 'text-center', 'text-muted'); adminCalendarDates.appendChild(blank); } // Clear any existing selected date const existingSelected = adminCalendarDates.querySelector('.selected'); if (existingSelected) { existingSelected.classList.remove('selected'); } // Populate the calendar with days let todayElement = null; for (let i = 1; i <= daysInMonth; i++) { const day = document.createElement('div'); day.classList.add('date-item', 'p-2', 'rounded-pill', 'text-center'); day.textContent = i; const date = new Date(year, month, i); const dateStr = date.toISOString().split('T')[0]; // Check if date is in the past const isPastDate = date < startOfToday; if (date.toDateString() === today.toDateString()) { day.classList.add('current'); todayElement = day; } if (availability[dateStr]) { day.classList.add('available'); } if (isPastDate) { day.classList.add('disabled'); } else { day.addEventListener('click', () => selectAdminDate(date, firstDay)); } adminCalendarDates.appendChild(day); } // If a date was previously selected, re-select it if (selectedAdminDate && selectedAdminDate.getMonth() === month && selectedAdminDate.getFullYear() === year) { selectAdminDate(selectedAdminDate, firstDay); } } // Handle admin date selection async function selectAdminDate(date, firstDay) { selectedAdminDate = date; const calendarDates = document.getElementById('adminCalendarDates'); if (!calendarDates) return; // Remove selected class from all dates const allDates = calendarDates.querySelectorAll('.date-item'); allDates.forEach(item => item.classList.remove('selected')); // Add selected class to the clicked date const selectedDay = Array.from(allDates).find(item => { if (!item.textContent) return false; const dayText = item.textContent; const itemDate = new Date(currentAdminYear, currentAdminMonth, parseInt(dayText)); return itemDate.toDateString() === date.toDateString(); }); if (selectedDay) selectedDay.classList.add('selected'); const selectedDateDisplay = document.getElementById('selectedDateDisplay'); if (selectedDateDisplay) { selectedDateDisplay.textContent = `Selected Date: ${date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}`; } // Update time slots for the selected date await updateTimeSlots(date); } // Update time slots based on selected date async function updateTimeSlots(date) { const availability = await fetchAvailability(); const dateStr = date.toISOString().split('T')[0]; let availableTimes = []; // Handle case where availability might be undefined or null if (availability && availability[dateStr]) { if (typeof availability[dateStr] === 'string') { availableTimes = availability[dateStr].split(',').map(t => t.trim()); } else if (Array.isArray(availability[dateStr])) { availableTimes = availability[dateStr].map(t => t.trim()); } } // Sort available times chronologically availableTimes.sort((a, b) => timeToMinutes(a) - timeToMinutes(b)); const timeSlots = document.getElementById('timeSlots'); if (!timeSlots) return; timeSlots.innerHTML = ''; // Get current time in minutes for comparison const now = new Date(); const currentMinutes = now.getHours() * 60 + now.getMinutes(); const isToday = date.toDateString() === now.toDateString(); // Create a map of all time slots with their status const allSlots = new Map(); timeSlotsData.forEach(time => { const timeInMinutes = timeToMinutes(time); // Skip times that are in the past on the current day if (isToday && timeInMinutes <= currentMinutes) { return; } allSlots.set(time, availableTimes.includes(time)); }); // Sort all slots chronologically const sortedSlots = Array.from(allSlots.entries()) .sort((a, b) => timeToMinutes(a[0]) - timeToMinutes(b[0])); // Display all slots in chronological order sortedSlots.forEach(([time, isAvailable]) => { const button = document.createElement('button'); button.classList.add('time-slot', 'btn', 'rounded-pill', 'w-100', 'mb-2', 'py-2'); button.textContent = time; if (isAvailable) { button.classList.add('btn-primary', 'available'); button.addEventListener('click', () => { button.classList.remove('btn-primary', 'available'); button.classList.add('btn-outline-secondary'); removeTime(dateStr, time); }); } else { button.classList.add('btn-outline-secondary'); button.addEventListener('click', () => { button.classList.remove('btn-outline-secondary'); button.classList.add('btn-primary', 'available'); addSingleTime(dateStr, time); }); } timeSlots.appendChild(button); }); } // Fetch available dates from the API async function fetchAvailability() { try { const selectedUid = document.getElementById('uidSelect').value; if (!selectedUid) return {}; const response = await fetch(`/api/availability/${selectedUid}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const availability = await response.json(); return availability; } catch (error) { console.error('Error fetching availability:', error); return {}; } } // Helper function to update calendar UI after availability changes async function updateAvailabilityUI() { const availability = await fetchAvailability(); if (selectedAdminDate) { await updateTimeSlots(selectedAdminDate); } const calendarDates = document.getElementById('adminCalendarDates'); if (calendarDates) { const allDateElements = calendarDates.querySelectorAll('.date-item'); allDateElements.forEach(dateElement => { if (!dateElement.textContent) return; // Skip empty cells const day = parseInt(dateElement.textContent); const date = new Date(currentAdminYear, currentAdminMonth, day); const dateStr = date.toISOString().split('T')[0]; if (availability[dateStr]) { dateElement.classList.add('available'); } else { dateElement.classList.remove('available'); } }); } } // UID management functions async function loadUids() { try { const response = await fetch('/api/uids'); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to fetch UIDs'); } const uids = await response.json(); const select = document.getElementById('uidSelect'); select.innerHTML = ''; uids.forEach(uidObj => { const option = document.createElement('option'); option.value = uidObj.uid; option.textContent = uidObj.uid; select.appendChild(option); }); } catch (error) { console.error('Error loading UIDs:', error); } } async function createUid(uid) { try { const response = await fetch('/api/uids', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ uid }) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to create UID'); } // Reload UIDs and select the new one await loadUids(); // Select the new UID and trigger change event const select = document.getElementById('uidSelect'); select.value = uid; select.dispatchEvent(new Event('change')); return true; } catch (error) { console.error('Error creating UID:', error); return false; } } async function deleteUid(uid) { try { const response = await fetch(`/api/uids/${uid}`, { method: 'DELETE' }); if (!response.ok) throw new Error('Failed to delete UID'); await loadUids(); return true; } catch (error) { console.error('Error deleting UID:', error); return false; } } // Add a single time for the selected date async function addSingleTime(dateStr, time) { const selectedUid = document.getElementById('uidSelect').value; if (!selectedUid) { alert('Please select a UID first'); return; } try { const availability = await fetchAvailability(); let times = []; if (availability && availability[dateStr]) { if (typeof availability[dateStr] === 'string') { times = availability[dateStr].split(',').map(t => t.trim()); } else if (Array.isArray(availability[dateStr])) { times = [...availability[dateStr]]; } } times.push(time); const response = await fetch(`/api/availability/${selectedUid}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ date: dateStr, times: [...new Set(times)] }) // Remove duplicates }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || `HTTP error! status: ${response.status}`); } const result = await response.json(); console.log(result.message); await updateAvailabilityUI(); } catch (error) { console.error('Error adding time slot:', error); alert('Failed to add time slot. Please try again.'); } } // Remove a single time for the selected date async function removeTime(dateStr, time) { const selectedUid = document.getElementById('uidSelect').value; if (!selectedUid) { alert('Please select a UID first'); return; } try { const availability = await fetchAvailability(); let times = []; if (availability && availability[dateStr]) { if (typeof availability[dateStr] === 'string') { times = availability[dateStr].split(',').filter(t => t.trim() !== time); } else if (Array.isArray(availability[dateStr])) { times = availability[dateStr].filter(t => t.trim() !== time); } } const response = await fetch(`/api/availability/${selectedUid}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ date: dateStr, times }) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || `HTTP error! status: ${response.status}`); } await updateAvailabilityUI(); } catch (error) { console.error('Error removing time slot:', error); alert('Failed to remove time slot. Please try again.'); } } // Helper function to convert time string to minutes since midnight function timeToMinutes(timeStr) { const match = timeStr.match(/([0-9]+)(?::([0-9]+))?(am|pm)/i); if (!match) return 0; let [_, hours, minutes, period] = match; hours = parseInt(hours); minutes = minutes ? parseInt(minutes) : 0; period = period.toLowerCase(); if (period === 'pm' && hours !== 12) hours += 12; if (period === 'am' && hours === 12) hours = 0; return hours * 60 + minutes; } // Helper function to ensure consistent time format function formatTimeSlot(hour) { const period = hour < 12 ? 'am' : 'pm'; const displayHour = hour === 0 ? 12 : (hour > 12 ? hour - 12 : hour); return `${displayHour}:00${period}`; } // Handle admin date selection async function selectAdminDate(date, firstDay) { // Check if the date is in the past const today = new Date(); const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); if (date < startOfToday) { console.warn('Attempted to select a past date'); return; } // Update selected date and time slots selectedAdminDate = date; await updateTimeSlots(date); const dateItems = document.querySelectorAll('#adminCalendarDates .date-item'); dateItems.forEach(item => item.classList.remove('selected')); // Ensure only one date is selected // Find the exact date item for the clicked date const selectedDay = Array.from(dateItems).find(item => { const dayText = item.textContent; const itemDate = new Date(currentAdminYear, currentAdminMonth, parseInt(dayText)); return itemDate.toDateString() === date.toDateString(); }); if (selectedDay) selectedDay.classList.add('selected'); const selectedDateDisplay = document.getElementById('selectedDateDisplay'); if (selectedDateDisplay) { selectedDateDisplay.textContent = `Selected Date: ${date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}`; } } // Update time slots based on selected date async function updateTimeSlots(date) { const availability = await fetchAvailability(); const dateStr = date.toISOString().split('T')[0]; let availableTimes = []; // Handle case where availability might be undefined or null if (availability && availability[dateStr]) { if (typeof availability[dateStr] === 'string') { availableTimes = availability[dateStr].split(',').map(t => t.trim()); } else if (Array.isArray(availability[dateStr])) { availableTimes = availability[dateStr].map(t => t.trim()); } } // Sort available times chronologically availableTimes.sort((a, b) => timeToMinutes(a) - timeToMinutes(b)); const timeSlots = document.getElementById('timeSlots'); if (!timeSlots) return; timeSlots.innerHTML = ''; // Get current time in minutes for comparison const now = new Date(); const currentMinutes = now.getHours() * 60 + now.getMinutes(); const isToday = date.toDateString() === now.toDateString(); // Create a map of all time slots with their status const allSlots = new Map(); timeSlotsData.forEach(time => { const timeInMinutes = timeToMinutes(time); // Skip times that are in the past on the current day if (isToday && timeInMinutes <= currentMinutes) { return; } allSlots.set(time, availableTimes.includes(time)); }); // Sort all slots chronologically const sortedSlots = Array.from(allSlots.entries()) .sort((a, b) => timeToMinutes(a[0]) - timeToMinutes(b[0])); // Display all slots in chronological order sortedSlots.forEach(([time, isAvailable]) => { const button = document.createElement('button'); button.classList.add('time-slot', 'btn', 'rounded-pill', 'w-100', 'mb-2', 'py-2'); button.textContent = time; if (isAvailable) { button.classList.add('btn-primary', 'available'); button.addEventListener('click', () => { button.classList.remove('btn-primary', 'available'); button.classList.add('btn-outline-secondary'); removeTime(dateStr, time); }); } else { button.classList.add('btn-outline-secondary'); button.addEventListener('click', () => { button.classList.remove('btn-outline-secondary'); button.classList.add('btn-primary', 'available'); addSingleTime(dateStr, time); }); } timeSlots.appendChild(button); }); } // Fetch available dates from the API async function fetchAvailability() { try { const selectedUid = document.getElementById('uidSelect').value; if (!selectedUid) return {}; const response = await fetch(`/api/availability/${selectedUid}`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const availability = await response.json(); return availability; } catch (error) { console.error('Error fetching availability:', error); return {}; } } // Helper function to update calendar UI after availability changes async function updateAvailabilityUI() { const availability = await fetchAvailability(); if (selectedAdminDate) { await updateTimeSlots(selectedAdminDate); } const calendarDates = document.getElementById('adminCalendarDates'); if (calendarDates) { const allDateElements = calendarDates.querySelectorAll('.date-item'); allDateElements.forEach(dateElement => { if (!dateElement.textContent) return; // Skip empty cells const day = parseInt(dateElement.textContent); const date = new Date(currentAdminYear, currentAdminMonth, day); const dateStr = date.toISOString().split('T')[0]; if (availability[dateStr]) { dateElement.classList.add('available'); } else { dateElement.classList.remove('available'); } }); } } document.addEventListener('DOMContentLoaded', async () => { // Load existing UIDs await loadUids(); // Handle UID selection const uidSelect = document.getElementById('uidSelect'); const deleteUidBtn = document.getElementById('deleteUid'); const uidUrl = document.getElementById('uidUrl'); uidSelect.addEventListener('change', async () => { const selectedUid = uidSelect.value; deleteUidBtn.disabled = !selectedUid; flushDatabaseBtn.disabled = !selectedUid; // Clear current selections selectedAdminDate = null; const selectedDateElem = document.getElementById('selectedDateDisplay'); const timeSlotsElem = document.getElementById('timeSlots'); const calendarDatesElem = document.getElementById('adminCalendarDates'); const adminMonthYear = document.getElementById('adminMonthYear'); // Clear all UI elements if (selectedDateElem) selectedDateElem.textContent = ''; if (timeSlotsElem) timeSlotsElem.innerHTML = ''; if (calendarDatesElem) calendarDatesElem.innerHTML = ''; if (adminMonthYear) adminMonthYear.textContent = ''; if (selectedUid) { const url = `${window.location.origin}/${selectedUid}`; uidUrl.textContent = url; uidUrl.href = url; // Refresh calendar with new UID's data await renderAdminCalendar(currentAdminMonth, currentAdminYear); // Show availability for current date if (selectedAdminDate) { await updateTimeSlots(selectedAdminDate); } } else { uidUrl.textContent = 'Select a UID first'; uidUrl.href = '#'; // Add placeholder text if (selectedDateElem) selectedDateElem.textContent = 'Please select a UID to view the calendar'; await renderAdminCalendar(currentAdminMonth, currentAdminYear); } }); // Handle new UID creation const newUidInput = document.getElementById('newUid'); const createUidBtn = document.getElementById('createUid'); createUidBtn.addEventListener('click', async () => { const uid = newUidInput.value.trim().toLowerCase(); if (!/^[a-z0-9-]+$/.test(uid)) { alert('UID can only contain lowercase letters, numbers, and hyphens'); return; } if (await createUid(uid)) { newUidInput.value = ''; } else { alert('Failed to create UID. Please try again.'); } }); // Handle UID deletion deleteUidBtn.addEventListener('click', async () => { const uid = uidSelect.value; if (!uid) return; if (!confirm(`Are you sure you want to delete UID: ${uid}?\nThis will remove all associated availability data.`)) { return; } if (await deleteUid(uid)) { // Clear selection and trigger change event uidSelect.value = ''; uidSelect.dispatchEvent(new Event('change')); } else { alert('Failed to delete UID. Please try again.'); } }); // Initialize UI elements const adminCalendarDates = document.getElementById('adminCalendarDates'); const adminMonthYear = document.getElementById('adminMonthYear'); const adminPrevMonthBtn = document.getElementById('adminPrevMonth'); const adminNextMonthBtn = document.getElementById('adminNextMonth'); const selectedDateDisplay = document.getElementById('selectedDateDisplay'); const timeSlots = document.getElementById('timeSlots'); const flushDatabaseBtn = document.getElementById('flushDatabase'); const resetDatabaseBtn = document.getElementById('resetDatabase'); // Handle database reset resetDatabaseBtn.addEventListener('click', async () => { if (!confirm('⚠️ WARNING: This will permanently delete ALL UIDs and their associated availability data. This action cannot be undone.\n\nAre you absolutely sure you want to proceed?')) { return; } // Double confirm for dangerous action if (!confirm('Please confirm one more time that you want to reset the entire database. All data will be lost.')) { return; } try { const response = await fetch('/api/reset', { method: 'POST' }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || 'Failed to reset database'); } // Reload the page to reset all state window.location.reload(); } catch (error) { console.error('Error resetting database:', error); alert('Failed to reset database. Please try again.'); } }); // Check if a date has availability async function checkAvailabilityForDate(date) { const availability = await fetchAvailability(); const dateStr = date.toISOString().split('T')[0]; return !!availability[dateStr]; } // Flush (delete) all entries from the database flushDatabaseBtn.addEventListener('click', async () => { const selectedUid = document.getElementById('uidSelect').value; if (!selectedUid) { alert('Please select a UID first'); return; } if (!confirm(`Are you sure you want to delete ALL availability entries for UID: ${selectedUid}?`)) { return; } try { console.log(`Attempting to delete all availability for UID: ${selectedUid}...`); const response = await fetch(`/api/availability/${selectedUid}/flush`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' } }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || `HTTP error! status: ${response.status}`); } const result = await response.json(); console.log('Delete availability response:', result); // Refresh data await fetchAvailability(); // Refresh availability data // Reset UI selectedAdminDate = null; // Clear selected date selectedDateDisplay.textContent = ''; timeSlots.innerHTML = ''; // Clear time slots await renderAdminCalendar(currentAdminMonth, currentAdminYear); } catch (error) { console.error('Error deleting availability:', error); alert('Failed to delete availability. Please try again.'); } }); // Navigate to previous month (admin) adminPrevMonthBtn.addEventListener('click', () => { currentAdminMonth--; if (currentAdminMonth < 0) { currentAdminMonth = 11; currentAdminYear--; } renderAdminCalendar(currentAdminMonth, currentAdminYear); }); // Navigate to next month (admin) adminNextMonthBtn.addEventListener('click', () => { currentAdminMonth++; if (currentAdminMonth > 11) { currentAdminMonth = 0; currentAdminYear++; } renderAdminCalendar(currentAdminMonth, currentAdminYear); }); // Handle global database flush const flushGlobalDatabaseBtn = document.getElementById('flushGlobalDatabase'); if (flushGlobalDatabaseBtn) { flushGlobalDatabaseBtn.addEventListener('click', async () => { if (!confirm('WARNING: This will delete ALL UIDs and their availability data. This action cannot be undone. Are you sure you want to proceed?')) { return; } try { const response = await fetch('/api/flush-global', { method: 'DELETE', headers: { 'Content-Type': 'application/json' } }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || `HTTP error! status: ${response.status}`); } const result = await response.json(); console.log('Global database flush response:', result); alert(result.message || 'Database flushed successfully!'); // Reset everything selectedAdminDate = null; const selectedDateElem = document.getElementById('selectedDateDisplay'); const timeSlotsElem = document.getElementById('timeSlots'); const calendarDatesElem = document.getElementById('adminCalendarDates'); if (selectedDateElem) selectedDateElem.textContent = ''; if (timeSlotsElem) timeSlotsElem.innerHTML = ''; if (calendarDatesElem) calendarDatesElem.innerHTML = ''; // Reload UIDs await loadUids(); // Update UID URL display const uidUrl = document.getElementById('uidUrl'); if (uidUrl) uidUrl.textContent = 'Select a UID first'; // Disable delete button const deleteUidBtn = document.getElementById('deleteUid'); if (deleteUidBtn) deleteUidBtn.disabled = true; } catch (error) { console.error('Error flushing database:', error); alert('Failed to flush database. Please try again.'); } }); } // Initial render with current date renderAdminCalendar(currentAdminMonth, currentAdminYear); if (currentAdminDate.getDate() <= new Date(currentAdminYear, currentAdminMonth, 0).getDate()) { const hasAvailability = await checkAvailabilityForDate(currentAdminDate); if (hasAvailability) { selectAdminDate(currentAdminDate, new Date(currentAdminYear, currentAdminMonth, 1).getDay()); } } });