// 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 timepicker modal from the template - moved to a separate function for clarity document.body.insertAdjacentHTML('beforeend', createTimepickerTemplate()); // Initialize timepicker functionality and store the reference to the controller const timepickerController = initializeTimepickerFunctionality(dateStr, availableTimes); // Show time picker when button is clicked timeButton.addEventListener('click', (e) => { showTimepicker(timepickerController); e.stopPropagation(); }); // Close time picker when clicking outside document.addEventListener('click', (e) => { const timepicker = document.getElementById('visualTimePicker'); const timepickerContainer = document.querySelector('.timepicker-container'); // Only close if we're not in the middle of a drag operation if (timepicker && timepicker.style.display === 'block' && e.target !== timeButton && (!timepickerContainer || !timepickerContainer.contains(e.target)) && !window.timepickerDragging) { // Check the global dragging flag timepickerController.closeTimepicker(); } }); } // Helper function to show the timepicker function showTimepicker(timepickerController) { const timepicker = document.getElementById('visualTimePicker'); if (!timepicker) return; // First make the timepicker visible but with opacity 0 timepicker.style.display = 'block'; timepicker.style.opacity = '0'; // Force a reflow to ensure the clock dimensions are calculated setTimeout(() => { // Always start with hours view when opening the timepicker timepickerController.switchToHoursView(); // Make the timepicker visible timepicker.style.opacity = '1'; document.body.style.overflow = 'hidden'; document.body.style.paddingRight = '0px'; }, 10); } // Create the timepicker HTML template function createTimepickerTemplate() { return `
`; } // Initialize the timepicker functionality function initializeTimepickerFunctionality(dateStr, availableTimes) { // DOM elements - group related elements together const timeElements = { hour: document.querySelector('.timepicker-hour'), minute: document.querySelector('.timepicker-minute'), am: document.querySelector('.timepicker-am'), pm: document.querySelector('.timepicker-pm'), dot: document.querySelector('.timepicker-dot') }; const clockElements = { wrapper: document.querySelector('.timepicker-clock-wrapper'), clock: document.querySelector('.timepicker-clock'), hand: document.querySelector('.timepicker-hand-pointer') }; const actionButtons = { clear: document.querySelector('.timepicker-clear'), cancel: document.querySelector('.timepicker-cancel'), submit: document.querySelector('.timepicker-submit') }; // State variables const state = { currentView: 'hours', selectedHour: 12, selectedMinute: 0, isDragging: false, isPM: timeElements.pm.classList.contains('active') }; // Create a global variable to track if we're dragging from inside the timepicker window.timepickerDragging = false; // Initialize setupEventListeners(); renderClockFace(state.currentView); function setupEventListeners() { // Header hour/minute buttons timeElements.hour.addEventListener('click', () => switchView('hours')); timeElements.minute.addEventListener('click', () => switchView('minutes')); // Clock face events clockElements.clock.addEventListener('mousedown', handleClockMouseDown); document.addEventListener('mousemove', handleClockMouseMove); document.addEventListener('mouseup', handleClockMouseUp); // Prevent text selection on the clock clockElements.clock.style.userSelect = 'none'; clockElements.clock.style.webkitUserSelect = 'none'; clockElements.clock.style.msUserSelect = 'none'; // AM/PM buttons timeElements.am.addEventListener('click', () => setAmPm('AM')); timeElements.pm.addEventListener('click', () => setAmPm('PM')); // Action buttons actionButtons.clear.addEventListener('click', clearTime); actionButtons.cancel.addEventListener('click', closeTimepicker); actionButtons.submit.addEventListener('click', submitTime); } function switchView(view) { state.currentView = view; // Update active state in header if (view === 'hours') { timeElements.hour.classList.add('active'); timeElements.minute.classList.remove('active'); timeElements.hour.style.pointerEvents = 'none'; timeElements.minute.style.pointerEvents = 'auto'; } else { timeElements.hour.classList.remove('active'); timeElements.minute.classList.add('active'); timeElements.hour.style.pointerEvents = 'auto'; timeElements.minute.style.pointerEvents = 'none'; } renderClockFace(view); } function renderClockFace(view) { // Clear existing time tips const existingTips = clockElements.clock.querySelectorAll('.timepicker-time-tips-hours, .timepicker-time-tips-minutes'); existingTips.forEach(tip => tip.remove()); if (view === 'hours') { renderHoursFace(); updateHandPosition(state.selectedHour, 'hours'); } else { renderMinutesFace(); updateHandPosition(state.selectedMinute, 'minutes'); } } function renderHoursFace() { const clockRadius = clockElements.clock.offsetWidth / 2; const tipRadius = clockRadius * 0.85; // Position closer to the edge (85% of radius) for (let hour = 1; hour <= 12; hour++) { const tip = createClockTip(hour, clockRadius, tipRadius, 'hours'); if (hour === state.selectedHour) { tip.classList.add('active'); } clockElements.clock.appendChild(tip); } } function renderMinutesFace() { const clockRadius = clockElements.clock.offsetWidth / 2; const tipRadius = clockRadius * 0.85; // Position closer to the edge (85% of radius) for (let minute = 0; minute < 60; minute += 5) { const tip = createClockTip(minute, clockRadius, tipRadius, 'minutes'); if (minute === state.selectedMinute) { tip.classList.add('active'); } clockElements.clock.appendChild(tip); } } // Helper function to create clock face tips (numbers) function createClockTip(value, clockRadius, tipRadius, type) { // Calculate angle and position let angle, displayValue; if (type === 'hours') { angle = ((value * 30) - 90) * (Math.PI / 180); displayValue = value; } else { angle = ((value * 6) - 90) * (Math.PI / 180); displayValue = value.toString().padStart(2, '0'); } // Calculate position const left = clockRadius + tipRadius * Math.cos(angle); const top = clockRadius + tipRadius * Math.sin(angle); // Create the tip element const tip = document.createElement('span'); tip.className = `timepicker-time-tips-${type}`; tip.style.left = `${left}px`; tip.style.top = `${top}px`; tip.style.position = 'absolute'; tip.style.transform = 'translate(-50%, -50%)'; tip.style.userSelect = 'none'; tip.style.webkitUserSelect = 'none'; tip.style.msUserSelect = 'none'; const tipElement = document.createElement('span'); tipElement.className = 'timepicker-tips-element'; tipElement.textContent = displayValue; tip.appendChild(tipElement); return tip; } function updateHandPosition(value, view) { let angle; if (view === 'hours') { // For hours, convert 12 to 0 for calculation purposes const hour = value === 12 ? 0 : value; angle = (hour / 12) * 360; } else { angle = (value / 60) * 360; } clockElements.hand.style.transform = `rotateZ(${angle}deg)`; // Update the active circle on the hand const circle = clockElements.hand.querySelector('.timepicker-circle'); if (circle) { circle.classList.add('active'); } } function handleClockMouseDown(event) { state.isDragging = true; // Set the global flag to indicate we're dragging from inside the timepicker window.timepickerDragging = true; updateTimeFromClockPosition(event); } function handleClockMouseMove(event) { if (state.isDragging) { updateTimeFromClockPosition(event); } } function handleClockMouseUp() { if (state.isDragging) { state.isDragging = false; // Reset the global dragging flag after a short delay // This allows the click event to process first setTimeout(() => { window.timepickerDragging = false; }, 10); // If we just finished selecting an hour, switch to minutes if (state.currentView === 'hours') { switchView('minutes'); } } } function updateTimeFromClockPosition(event) { const clockRect = clockElements.clock.getBoundingClientRect(); const centerX = clockRect.left + clockRect.width / 2; const centerY = clockRect.top + clockRect.height / 2; // Calculate angle from center to mouse position const x = event.clientX - centerX; const y = event.clientY - centerY; // Calculate angle in degrees, starting from 12 o'clock position let angle = Math.atan2(y, x) * (180 / Math.PI) + 90; // Normalize angle to 0-360 if (angle < 0) { angle += 360; } if (state.currentView === 'hours') { updateHourFromAngle(angle); } else { updateMinuteFromAngle(angle); } } function updateHourFromAngle(angle) { // Convert angle to hour (1-12) let hour = Math.round(angle / 30); // Handle edge cases for full circle if (hour === 0 || hour > 12) hour = 12; state.selectedHour = hour; timeElements.hour.textContent = hour; updateHandPosition(hour, 'hours'); // Update active class on hour tips const hourTips = clockElements.clock.querySelectorAll('.timepicker-time-tips-hours'); hourTips.forEach(tip => { const hourValue = parseInt(tip.querySelector('.timepicker-tips-element').textContent); if (hourValue === state.selectedHour) { tip.classList.add('active'); } else { tip.classList.remove('active'); } }); } function updateMinuteFromAngle(angle) { // Convert angle to minute (0-55, step 5) let minute = Math.floor(angle / 6); // Round to nearest 5 minute = Math.round(minute / 5) * 5; // Handle edge case for full circle if (minute >= 60) minute = 0; state.selectedMinute = minute; timeElements.minute.textContent = minute.toString().padStart(2, '0'); updateHandPosition(minute, 'minutes'); // Update active class on minute tips const minuteTips = clockElements.clock.querySelectorAll('.timepicker-time-tips-minutes'); minuteTips.forEach(tip => { const minuteValue = parseInt(tip.querySelector('.timepicker-tips-element').textContent); if (minuteValue === state.selectedMinute) { tip.classList.add('active'); } else { tip.classList.remove('active'); } }); } function setAmPm(period) { if (period === 'AM') { timeElements.am.classList.add('active'); timeElements.pm.classList.remove('active'); state.isPM = false; } else { timeElements.am.classList.remove('active'); timeElements.pm.classList.add('active'); state.isPM = true; } } function clearTime() { state.selectedHour = 12; state.selectedMinute = 0; timeElements.hour.textContent = '12'; timeElements.minute.textContent = '00'; setAmPm('PM'); switchView('hours'); } function closeTimepicker() { const timepicker = document.getElementById('visualTimePicker'); if (timepicker) { timepicker.style.display = 'none'; document.body.style.overflow = ''; document.body.style.paddingRight = ''; } } function submitTime() { const formattedHour = state.selectedHour; const formattedMinute = state.selectedMinute.toString().padStart(2, '0'); const period = state.isPM ? 'PM' : 'AM'; const timeValue = `${formattedHour}:${formattedMinute}${period.toLowerCase()}`; // 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); } closeTimepicker(); } // Return controller object with public methods return { renderClockFace, closeTimepicker, switchToHoursView: () => switchView('hours') }; } // 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()); } } });