// 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']; // 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 currentMinutes = today.getHours() * 60 + today.getMinutes(); 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', 'empty-cell'); blank.style.cursor = 'default'; // Remove pointer cursor blank.style.pointerEvents = 'none'; // Disable hover and click events 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; const isToday = date.toDateString() === today.toDateString(); if (isToday) { day.classList.add('current'); todayElement = day; } // Check if the date has available time slots let hasAvailableTimes = false; if (availability[dateStr]) { let times = []; if (typeof availability[dateStr] === 'string') { times = availability[dateStr].split(',').map(t => t.trim()); } else if (Array.isArray(availability[dateStr])) { times = availability[dateStr].map(t => t.trim()); } // For today, filter out past times if (isToday) { times = times.filter(time => timeToMinutes(time) > currentMinutes); } // Only mark as available if there are valid times hasAvailableTimes = times.length > 0; } // Only add 'available' class if the date is not in the past AND has available times if (hasAvailableTimes && !isPastDate) { 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) { // 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'); // Remove the selected date display text const selectedDateDisplay = document.getElementById('selectedDateDisplay'); if (selectedDateDisplay) { selectedDateDisplay.textContent = ''; } } // 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]]; } } // Sort available times chronologically availableTimes.sort((a, b) => timeToMinutes(a) - timeToMinutes(b)); const timeSlots = document.getElementById('timeSlots'); if (!timeSlots) return; // Clear existing time slots timeSlots.innerHTML = ''; // Create time button directly without the container and heading const timeButton = document.createElement('button'); timeButton.type = 'button'; timeButton.id = 'customTimeInput'; timeButton.classList.add('btn', 'btn-success', 'btn-lg', 'w-100', 'mb-4'); timeButton.style.backgroundColor = '#2e8b57'; // Money green color timeButton.style.borderColor = '#2e8b57'; timeButton.style.fontSize = '1.25rem'; timeButton.style.padding = '0.75rem 1.25rem'; timeButton.innerHTML = ' Add Time Slot'; // Add button directly to time slots timeSlots.appendChild(timeButton); // Initialize visual time picker initializeVisualTimePicker(dateStr, availableTimes); // Add divider const divider = document.createElement('hr'); timeSlots.appendChild(divider); // Display existing time slots if (availableTimes.length === 0) { const noTimesMessage = document.createElement('p'); noTimesMessage.classList.add('text-muted', 'text-center'); noTimesMessage.textContent = 'No time slots available for this date.'; timeSlots.appendChild(noTimesMessage); } else { // Create container for time slot list const timeSlotsContainer = document.createElement('div'); timeSlotsContainer.classList.add('d-flex', 'flex-column', 'gap-2', 'mt-3'); // Display all slots in chronological order availableTimes.forEach(time => { // Create a container div for the time slot const timeSlotContainer = document.createElement('div'); timeSlotContainer.classList.add('position-relative', 'w-100', 'd-flex', 'align-items-center', 'mb-2'); // Create the time slot pill const timeSlotItem = document.createElement('div'); timeSlotItem.classList.add('btn', 'btn-light', 'rounded-pill', 'px-4', 'py-3', 'flex-grow-1'); timeSlotItem.style.backgroundColor = '#e9f2ff'; timeSlotItem.style.color = '#0d6efd'; timeSlotItem.style.border = '1px solid #0d6efd'; timeSlotItem.style.textAlign = 'center'; timeSlotItem.style.fontSize = '1.1rem'; timeSlotItem.style.fontWeight = '500'; timeSlotItem.style.transition = 'all 0.2s ease'; // Add time text directly to the pill timeSlotItem.textContent = time; // Add delete button/icon as a separate element const deleteIcon = document.createElement('button'); deleteIcon.innerHTML = ''; deleteIcon.classList.add('btn', 'btn-sm', 'text-danger', 'ms-3'); deleteIcon.style.backgroundColor = 'transparent'; deleteIcon.style.border = 'none'; deleteIcon.style.fontSize = '1.2rem'; deleteIcon.style.padding = '8px'; deleteIcon.style.cursor = 'pointer'; deleteIcon.style.transition = 'all 0.2s ease'; deleteIcon.style.opacity = '0.8'; deleteIcon.style.borderRadius = '50%'; deleteIcon.style.display = 'flex'; deleteIcon.style.alignItems = 'center'; deleteIcon.style.justifyContent = 'center'; // Add hover effects timeSlotItem.addEventListener('mouseenter', () => { timeSlotItem.style.backgroundColor = '#0d6efd'; timeSlotItem.style.color = 'white'; timeSlotItem.style.transform = 'translateY(-2px)'; timeSlotItem.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)'; }); timeSlotItem.addEventListener('mouseleave', () => { timeSlotItem.style.backgroundColor = '#e9f2ff'; timeSlotItem.style.color = '#0d6efd'; timeSlotItem.style.transform = 'translateY(0)'; timeSlotItem.style.boxShadow = 'none'; }); // Add hover effects for delete icon deleteIcon.addEventListener('mouseenter', () => { deleteIcon.style.opacity = '1'; deleteIcon.style.transform = 'scale(1.2)'; deleteIcon.style.backgroundColor = 'rgba(220, 53, 69, 0.1)'; }); deleteIcon.addEventListener('mouseleave', () => { deleteIcon.style.opacity = '0.8'; deleteIcon.style.transform = 'scale(1)'; deleteIcon.style.backgroundColor = 'transparent'; }); deleteIcon.addEventListener('click', (e) => { e.stopPropagation(); removeTime(dateStr, time); }); timeSlotContainer.appendChild(timeSlotItem); timeSlotContainer.appendChild(deleteIcon); timeSlotsContainer.appendChild(timeSlotContainer); }); timeSlots.appendChild(timeSlotsContainer); } } // Initialize visual time picker function function initializeVisualTimePicker(dateStr, availableTimes) { const timeButton = document.getElementById('customTimeInput'); if (!timeButton) return; // Remove any existing time picker const existingTimePicker = document.getElementById('visualTimePicker'); if (existingTimePicker) { existingTimePicker.remove(); } // Create the visual time picker container const timePicker = document.createElement('div'); timePicker.id = 'visualTimePicker'; timePicker.classList.add('card', 'shadow'); timePicker.style.display = 'none'; timePicker.style.position = 'fixed'; timePicker.style.zIndex = '1050'; timePicker.style.width = '300px'; timePicker.style.top = '50%'; timePicker.style.left = '50%'; timePicker.style.transform = 'translate(-50%, -50%)'; // Create the header const header = document.createElement('div'); header.classList.add('card-header', 'bg-primary', 'text-white', 'text-center', 'p-0'); // Create the time display in the header const timeDisplay = document.createElement('div'); timeDisplay.classList.add('display-4', 'py-3', 'mb-0'); timeDisplay.innerHTML = '12:00 PM'; header.appendChild(timeDisplay); timePicker.appendChild(header); // Create the body const body = document.createElement('div'); body.classList.add('card-body', 'bg-white'); // Create the clock face container const clockFace = document.createElement('div'); clockFace.classList.add('time-picker-clock', 'mb-3'); clockFace.style.position = 'relative'; clockFace.style.width = '250px'; clockFace.style.height = '250px'; clockFace.style.margin = '0 auto'; clockFace.style.backgroundColor = '#f8f9fa'; clockFace.style.borderRadius = '50%'; // Add clock center dot const clockCenter = document.createElement('div'); clockCenter.style.position = 'absolute'; clockCenter.style.top = '50%'; clockCenter.style.left = '50%'; clockCenter.style.transform = 'translate(-50%, -50%)'; clockCenter.style.width = '8px'; clockCenter.style.height = '8px'; clockCenter.style.backgroundColor = '#0d6efd'; clockCenter.style.borderRadius = '50%'; clockFace.appendChild(clockCenter); // Add hour numbers to the clock face for (let i = 1; i <= 12; i++) { const hourNumber = document.createElement('div'); hourNumber.classList.add('hour-number'); hourNumber.textContent = i; hourNumber.style.position = 'absolute'; hourNumber.style.width = '40px'; hourNumber.style.height = '40px'; hourNumber.style.textAlign = 'center'; hourNumber.style.lineHeight = '40px'; hourNumber.style.fontWeight = 'bold'; hourNumber.style.cursor = 'pointer'; // Calculate position (in a circle) const angle = (i * 30 - 90) * (Math.PI / 180); // 30 degrees per hour, starting at 12 o'clock const radius = 100; // Distance from center const left = 125 + radius * Math.cos(angle); const top = 125 + radius * Math.sin(angle); hourNumber.style.left = `${left - 20}px`; // Adjust for element width hourNumber.style.top = `${top - 20}px`; // Adjust for element height // Add click event to select hour hourNumber.addEventListener('click', () => { document.getElementById('timePickerHour').textContent = i; // Highlight selected hour document.querySelectorAll('.hour-number').forEach(el => { el.style.backgroundColor = 'transparent'; el.style.color = '#212529'; }); hourNumber.style.backgroundColor = '#0d6efd'; hourNumber.style.color = 'white'; // Show minute selection showMinuteSelection(); }); clockFace.appendChild(hourNumber); } // Function to show minute selection function showMinuteSelection() { // Hide hour numbers document.querySelectorAll('.hour-number').forEach(el => { el.style.display = 'none'; }); // Show minute numbers document.querySelectorAll('.minute-number').forEach(el => { el.style.display = 'block'; }); } // Function to show hour selection function showHourSelection() { // Hide minute numbers document.querySelectorAll('.minute-number').forEach(el => { el.style.display = 'none'; }); // Show hour numbers document.querySelectorAll('.hour-number').forEach(el => { el.style.display = 'block'; }); } // Add minute numbers to the clock face (initially hidden) const minuteValues = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55]; minuteValues.forEach((minute, index) => { const minuteNumber = document.createElement('div'); minuteNumber.classList.add('minute-number'); minuteNumber.textContent = minute.toString().padStart(2, '0'); minuteNumber.style.position = 'absolute'; minuteNumber.style.width = '40px'; minuteNumber.style.height = '40px'; minuteNumber.style.textAlign = 'center'; minuteNumber.style.lineHeight = '40px'; minuteNumber.style.fontWeight = 'bold'; minuteNumber.style.cursor = 'pointer'; minuteNumber.style.display = 'none'; // Initially hidden // Calculate position (in a circle) const angle = (index * 30 - 90) * (Math.PI / 180); const radius = 100; // Distance from center const left = 125 + radius * Math.cos(angle); const top = 125 + radius * Math.sin(angle); minuteNumber.style.left = `${left - 20}px`; // Adjust for element width minuteNumber.style.top = `${top - 20}px`; // Adjust for element height // Add click event to select minute minuteNumber.addEventListener('click', () => { document.getElementById('timePickerMinute').textContent = minute.toString().padStart(2, '0'); // Highlight selected minute document.querySelectorAll('.minute-number').forEach(el => { el.style.backgroundColor = 'transparent'; el.style.color = '#212529'; }); minuteNumber.style.backgroundColor = '#0d6efd'; minuteNumber.style.color = 'white'; // Show hour selection again showHourSelection(); }); clockFace.appendChild(minuteNumber); }); body.appendChild(clockFace); // Create AM/PM toggle const ampmToggle = document.createElement('div'); ampmToggle.classList.add('d-flex', 'justify-content-center', 'mb-3'); const amButton = document.createElement('button'); amButton.classList.add('btn', 'btn-outline-primary', 'me-2'); amButton.textContent = 'AM'; amButton.addEventListener('click', () => { document.getElementById('timePickerAMPM').textContent = 'AM'; amButton.classList.add('active'); pmButton.classList.remove('active'); }); const pmButton = document.createElement('button'); pmButton.classList.add('btn', 'btn-outline-primary'); pmButton.textContent = 'PM'; pmButton.addEventListener('click', () => { document.getElementById('timePickerAMPM').textContent = 'PM'; pmButton.classList.add('active'); amButton.classList.remove('active'); }); pmButton.classList.add('active'); // Add active class to PM button ampmToggle.appendChild(amButton); ampmToggle.appendChild(pmButton); body.appendChild(ampmToggle); // Create action buttons const actionButtons = document.createElement('div'); actionButtons.classList.add('d-flex', 'justify-content-between'); const closeButton = document.createElement('button'); closeButton.classList.add('btn', 'btn-outline-secondary', 'flex-grow-1', 'me-2'); closeButton.textContent = 'CANCEL'; closeButton.addEventListener('click', () => { timePicker.style.display = 'none'; }); const okButton = document.createElement('button'); okButton.classList.add('btn', 'btn-primary', 'flex-grow-1'); okButton.textContent = 'OK'; okButton.addEventListener('click', () => { const hour = document.getElementById('timePickerHour').textContent; const minute = document.getElementById('timePickerMinute').textContent; const ampm = document.getElementById('timePickerAMPM').textContent.toLowerCase(); const timeValue = `${hour}:${minute}${ampm}`; document.getElementById('customTimeInput').value = timeValue; // Check if time already exists in available times if (availableTimes.includes(timeValue)) { // Silently handle duplicate time slots - no error message console.log('Time slot already exists, not adding duplicate'); } else { // Automatically add the time slot addSingleTime(dateStr, timeValue); } // Hide the time picker timePicker.style.display = 'none'; }); actionButtons.appendChild(closeButton); actionButtons.appendChild(okButton); body.appendChild(actionButtons); timePicker.appendChild(body); // Add the time picker to the document document.body.appendChild(timePicker); // Show time picker when button is clicked timeButton.addEventListener('click', (e) => { // Remove positioning relative to button timePicker.style.display = 'block'; // Reset to default view (hour selection) showHourSelection(); e.stopPropagation(); }); // Close time picker when clicking outside document.addEventListener('click', (e) => { if (e.target !== timeButton && !timePicker.contains(e.target)) { timePicker.style.display = 'none'; } }); } // 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'); const today = new Date(); const currentMinutes = today.getHours() * 60 + today.getMinutes(); 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]; // Check if date is in the past const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate()); const isPastDate = date < startOfToday; const isToday = date.toDateString() === today.toDateString(); // Check if the date has available time slots let hasAvailableTimes = false; if (availability[dateStr]) { let times = []; if (typeof availability[dateStr] === 'string') { times = availability[dateStr].split(',').map(t => t.trim()); } else if (Array.isArray(availability[dateStr])) { times = availability[dateStr].map(t => t.trim()); } // For today, filter out past times if (isToday) { times = times.filter(time => timeToMinutes(time) > currentMinutes); } // Only mark as available if there are valid times hasAvailableTimes = times.length > 0; } // Only add 'available' class if the date is not in the past AND has available times if (hasAvailableTimes && !isPastDate) { dateElement.classList.add('available'); } else { dateElement.classList.remove('available'); } // Always ensure past dates have the disabled class if (isPastDate) { dateElement.classList.add('disabled'); 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}`; } document.addEventListener('DOMContentLoaded', async () => { // Add custom CSS for the current date to make it more distinguishable const style = document.createElement('style'); style.textContent = ` .date-item.current { border: 2px solid #ffc107 !important; /* Yellow border */ background-color: rgba(255, 193, 7, 0.1) !important; /* Light yellow background */ font-weight: bold !important; color: #212529 !important; /* Ensure text is visible */ } /* Ensure current date styling takes precedence but doesn't interfere with selection */ .date-item.current.selected { border: 2px solid #0d6efd !important; /* Blue border for selected */ background-color: #0d6efd !important; /* Dark blue background when selected */ color: white !important; /* White text for better contrast */ font-weight: bold !important; } /* Prevent hover effects on empty calendar cells */ .date-item.empty-cell { cursor: default !important; pointer-events: none !important; } .date-item.empty-cell:hover { background-color: transparent !important; transform: none !important; } /* Time picker styling */ #visualTimePicker { box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important; border-radius: 8px; overflow: hidden; } #visualTimePicker .card-header { background-color: #0d6efd; } #visualTimePicker .display-4 { font-size: 3rem; font-weight: 300; color: white; } .time-picker-clock { box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); } .hour-number, .minute-number { border-radius: 50%; transition: all 0.2s ease; } .hour-number:hover, .minute-number:hover { background-color: rgba(13, 110, 253, 0.1); } /* Time slot pills styling */ .time-slot { transition: all 0.2s ease; } .time-slot:hover { transform: translateY(-2px); } /* Make all UID buttons consistent */ #createUid, #deleteUid, #flushDatabase, #copyUrlBtn { border-radius: 30px; padding: 0.75rem 1.5rem; font-size: 1.25rem; font-weight: 500; height: 60px; display: flex; align-items: center; justify-content: center; width: 100%; transition: all 0.3s ease; } #createUid { background-color: #2e8b57; /* Money green color */ border-color: #2e8b57; } #createUid:hover { background-color: #267349; border-color: #267349; } /* URL container styling */ .url-container { display: flex; align-items: center; gap: 10px; } .url-display { flex-grow: 1; overflow: hidden; text-overflow: ellipsis; } `; document.head.appendChild(style); // Create and add the Copy URL button const urlContainer = document.querySelector('.form-control-lg.bg-light.p-3'); if (urlContainer) { // Create a wrapper div for the URL and button const wrapper = document.createElement('div'); wrapper.classList.add('url-container', 'mt-2'); wrapper.style.display = 'flex'; wrapper.style.alignItems = 'center'; wrapper.style.gap = '10px'; wrapper.style.flexWrap = 'nowrap'; // Prevent wrapping // Move the existing elements to the wrapper const uidPlaceholder = document.getElementById('uidPlaceholder'); const uidUrl = document.getElementById('uidUrl'); // Create a div to contain the URL display const urlDisplay = document.createElement('div'); urlDisplay.classList.add('url-display'); urlDisplay.style.overflow = 'hidden'; urlDisplay.style.textOverflow = 'ellipsis'; urlDisplay.style.whiteSpace = 'nowrap'; // Keep URL on one line urlDisplay.style.minWidth = '0'; // Allow flex item to shrink below content size urlDisplay.style.flexGrow = '1'; // Take available space urlDisplay.style.flexShrink = '1'; // Allow shrinking // Move the existing elements to the URL display div if (uidPlaceholder) urlDisplay.appendChild(uidPlaceholder); if (uidUrl) urlDisplay.appendChild(uidUrl); // Add the URL display to the wrapper wrapper.appendChild(urlDisplay); // Create the Copy URL button const copyUrlBtn = document.createElement('button'); copyUrlBtn.id = 'copyUrlBtn'; copyUrlBtn.classList.add('btn', 'btn-primary', 'rounded-pill', 'd-none'); copyUrlBtn.style.height = '40px'; copyUrlBtn.style.padding = '0 15px'; copyUrlBtn.style.fontSize = '0.9rem'; copyUrlBtn.style.whiteSpace = 'nowrap'; copyUrlBtn.style.width = 'auto'; // Only as wide as content copyUrlBtn.style.minWidth = 'fit-content'; // Ensure it fits content copyUrlBtn.style.flexShrink = '0'; // Prevent button from shrinking copyUrlBtn.innerHTML = 'Copy URL'; // Add click event to copy URL to clipboard copyUrlBtn.addEventListener('click', () => { const url = document.getElementById('uidUrl').href; if (url && url !== '#') { navigator.clipboard.writeText(url) .then(() => { // Change button text temporarily to indicate success const originalText = copyUrlBtn.innerHTML; copyUrlBtn.innerHTML = ' Copied!'; copyUrlBtn.classList.remove('btn-primary'); copyUrlBtn.classList.add('btn-success'); // Reset button after 2 seconds setTimeout(() => { copyUrlBtn.innerHTML = originalText; copyUrlBtn.classList.remove('btn-success'); copyUrlBtn.classList.add('btn-primary'); }, 2000); }) .catch(err => { console.error('Failed to copy URL: ', err); alert('Failed to copy URL to clipboard'); }); } }); // Add the button to the wrapper wrapper.appendChild(copyUrlBtn); // Clear the container and add the wrapper urlContainer.innerHTML = ''; urlContainer.appendChild(document.createElement('small')).classList.add('d-block', 'text-muted', 'mb-1'); urlContainer.querySelector('small').textContent = 'Public Access URL:'; urlContainer.appendChild(wrapper); // Show the button if a UID is already selected const selectedUid = document.getElementById('uidSelect').value; if (selectedUid) { copyUrlBtn.classList.remove('d-none'); } } // Load existing UIDs await loadUids(); // Handle UID selection const uidSelect = document.getElementById('uidSelect'); const deleteUidBtn = document.getElementById('deleteUid'); const uidUrl = document.getElementById('uidUrl'); const uidPlaceholder = document.getElementById('uidPlaceholder'); uidSelect.addEventListener('change', async () => { const selectedUid = uidSelect.value; deleteUidBtn.disabled = !selectedUid; flushDatabaseBtn.disabled = !selectedUid; // Show or hide calendar and availability sections based on UID selection const calendarSection = document.getElementById('calendarSection'); const calendarDatesSection = document.getElementById('calendarDatesSection'); const availabilitySection = document.getElementById('availabilitySection'); if (selectedUid) { calendarSection.classList.remove('d-none'); calendarDatesSection.classList.remove('d-none'); availabilitySection.classList.remove('d-none'); // Show URL link and hide placeholder uidPlaceholder.classList.add('d-none'); uidUrl.classList.remove('d-none'); // Show copy button when URL is displayed if (document.getElementById('copyUrlBtn')) { document.getElementById('copyUrlBtn').classList.remove('d-none'); } } else { calendarSection.classList.add('d-none'); calendarDatesSection.classList.add('d-none'); availabilitySection.classList.add('d-none'); // Hide URL link and show placeholder uidPlaceholder.classList.remove('d-none'); uidUrl.classList.add('d-none'); // Hide copy button when no URL is displayed if (document.getElementById('copyUrlBtn')) { document.getElementById('copyUrlBtn').classList.add('d-none'); } } // 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); // Automatically select today's date and show time slots const today = new Date(); if (today.getMonth() === currentAdminMonth && today.getFullYear() === currentAdminYear) { // Get the first day of the month to pass to selectAdminDate const firstDay = new Date(currentAdminYear, currentAdminMonth, 1).getDay(); // Select today's date selectedAdminDate = today; await updateTimeSlots(today); // Highlight today in the calendar const dateItems = document.querySelectorAll('#adminCalendarDates .date-item'); dateItems.forEach(item => item.classList.remove('selected')); // Find today's element in the calendar const todayDay = today.getDate(); const todayElement = Array.from(dateItems).find(item => { return item.textContent && parseInt(item.textContent) === todayDay; }); if (todayElement) { todayElement.classList.add('selected'); } } } else { uidUrl.textContent = ''; 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 createUidBtn = document.getElementById('createUid'); const modalUidInput = document.getElementById('modalUidInput'); const confirmCreateUidBtn = document.getElementById('confirmCreateUid'); const createUidModal = new bootstrap.Modal(document.getElementById('createUidModal')); // Add event listener for modal shown event to focus the input field document.getElementById('createUidModal').addEventListener('shown.bs.modal', function () { modalUidInput.focus(); }); createUidBtn.addEventListener('click', () => { modalUidInput.value = ''; createUidModal.show(); }); // Add event listener for Enter key on the input field modalUidInput.addEventListener('keyup', (event) => { if (event.key === 'Enter') { confirmCreateUidBtn.click(); } }); confirmCreateUidBtn.addEventListener('click', async () => { const uid = modalUidInput.value.trim().toLowerCase(); if (!/^[a-z0-9-]+$/.test(uid)) { alert('UID can only contain lowercase letters, numbers, and hyphens'); return; } if (await createUid(uid)) { createUidModal.hide(); } 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'); const devToolsBtn = document.getElementById('devToolsBtn'); const devToolsModal = new bootstrap.Modal(document.getElementById('devToolsModal')); // Show Dev Tools modal when button is clicked devToolsBtn.addEventListener('click', () => { devToolsModal.show(); }); // 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; } 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()); } } });