// js/admin.js // Import components and utilities import { renderAdminCalendar, selectAdminDate, currentAdminMonth, currentAdminYear, selectedAdminDate } from './components/calendar.js'; import { initializeVisualTimePicker, showTimepicker } from './components/timepicker.js'; import { fetchAvailability, loadUids, createUid, deleteUid, addSingleTime, removeTime, flushAvailability, resetDatabase } from './utils/api.js'; import { timeToMinutes, formatTimeSlot, formatDate } from './utils/time-utils.js'; // Check if a date has availability async function checkAvailabilityForDate(date) { const availability = await fetchAvailability(); const dateStr = formatDate(date); return !!availability[dateStr]; } // Update time slots for a selected date async function updateTimeSlots(dateStr) { const timeSlots = document.getElementById('timeSlots'); if (!timeSlots) return; const selectedUid = document.getElementById('uidSelect').value; if (!selectedUid) return; try { // Fetch availability data const availability = await fetchAvailability(); // Get available times for the selected date const availableTimes = availability[dateStr] || []; // Clear existing time slots timeSlots.innerHTML = ''; // Add custom time input button const customTimeBtn = document.createElement('button'); customTimeBtn.type = 'button'; customTimeBtn.id = 'customTimeInput'; customTimeBtn.classList.add('btn', 'btn-success', 'btn-lg', 'w-100', 'mb-4'); customTimeBtn.style.backgroundColor = '#2e8b57'; // Money green color customTimeBtn.style.borderColor = '#2e8b57'; customTimeBtn.style.fontSize = '1.25rem'; customTimeBtn.style.padding = '0.75rem 1.25rem'; customTimeBtn.innerHTML = ' Add Time Slot'; timeSlots.appendChild(customTimeBtn); // Add divider const divider = document.createElement('hr'); timeSlots.appendChild(divider); // Initialize visual time picker const timepickerController = initializeVisualTimePicker(dateStr, availableTimes); // Sort available times availableTimes.sort((a, b) => timeToMinutes(a) - timeToMinutes(b)); // 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) .then(() => { // Refresh the time slots updateTimeSlots(dateStr); // Refresh the calendar to show availability indicator renderAdminCalendar(currentAdminMonth, currentAdminYear); }) .catch(error => { console.error('Error removing time:', error); showAlert('danger', 'Failed to remove time'); }); }); timeSlotContainer.appendChild(timeSlotItem); timeSlotContainer.appendChild(deleteIcon); timeSlotsContainer.appendChild(timeSlotContainer); }); timeSlots.appendChild(timeSlotsContainer); } // Initialize the timepicker when the custom time button is clicked customTimeBtn.addEventListener('click', (e) => { // Set up the onSubmit handler with the current dateStr window.activeTimepickerController = { dateStr: dateStr, // Store the date string for reference onSubmit: (time) => { console.log('Submitting time:', time, 'for date:', dateStr); addSingleTime(dateStr, time) .then(() => { console.log('Time added successfully'); // Refresh the time slots updateTimeSlots(dateStr); // Refresh the calendar to show availability indicator renderAdminCalendar(currentAdminMonth, currentAdminYear); }) .catch(error => { console.error('Error adding time:', error); showAlert('danger', 'Failed to add time'); }); } }; // Show the timepicker with the controller showTimepicker(timepickerController); // Stop event propagation to prevent the click outside handler from firing e.stopPropagation(); }); } catch (error) { console.error('Error updating time slots:', error); } } // Expose updateTimeSlots globally so it can be called from the timepicker component window.updateTimeSlots = updateTimeSlots; // Also expose addSingleTime globally for the timepicker component window.addSingleTime = addSingleTime; // Show an alert message function showAlert(type, message) { // Create alert element const alert = document.createElement('div'); alert.className = `alert alert-${type} alert-dismissible fade show`; alert.role = 'alert'; alert.innerHTML = ` ${message} `; // Add to page const mainContent = document.querySelector('.main-content'); if (mainContent) { mainContent.prepend(alert); // Auto-dismiss after 3 seconds setTimeout(() => { const bsAlert = new bootstrap.Alert(alert); bsAlert.close(); }, 3000); } } // Initialize the application when the DOM is loaded 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'); } } // Close time picker when clicking outside document.addEventListener('click', (e) => { const timepicker = document.getElementById('visualTimePicker'); const timepickerContainer = document.querySelector('.timepicker-container'); const customTimeBtn = document.getElementById('customTimeInput'); // Only close if we're not in the middle of a drag operation if (timepicker && timepicker.style.display === 'block' && e.target !== customTimeBtn && (!timepickerContainer || !timepickerContainer.contains(e.target)) && !window.timepickerDragging) { // Check the global dragging flag // Close the timepicker timepicker.style.display = 'none'; document.body.style.overflow = ''; document.body.style.paddingRight = ''; // Call the onClose callback if provided if (window.activeTimepickerController && window.activeTimepickerController.onClose) { window.activeTimepickerController.onClose(); } } }); // Listen for date selection events from the calendar component document.addEventListener('dateSelected', (event) => { if (event.detail && event.detail.dateStr) { updateTimeSlots(event.detail.dateStr); } }); // Handle UID selection const uidSelect = document.getElementById('uidSelect'); const deleteUidBtn = document.getElementById('deleteUid'); const flushDatabaseBtn = document.getElementById('flushDatabase'); 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'); // Show URL link and hide placeholder uidPlaceholder.classList.add('d-none'); uidUrl.classList.remove('d-none'); uidUrl.href = `${window.location.origin}/${selectedUid}`; uidUrl.textContent = `${window.location.origin}/${selectedUid}`; // Show copy button when URL is displayed if (document.getElementById('copyUrlBtn')) { document.getElementById('copyUrlBtn').classList.remove('d-none'); } // Render the calendar await renderAdminCalendar(currentAdminMonth, currentAdminYear); // Automatically select today's date if it's in the current month/year 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 await selectAdminDate(today, firstDay); } } 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 const selectedDateElem = document.getElementById('selectedDateDisplay'); const timeSlotsElem = document.getElementById('timeSlots'); // Clear UI elements if (selectedDateElem) selectedDateElem.textContent = 'Please select a UID to view the calendar'; if (timeSlotsElem) timeSlotsElem.innerHTML = ''; } }); // Handle create UID button const createUidBtn = document.getElementById('createUid'); 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 () { const modalUidInput = document.getElementById('modalUidInput'); if (modalUidInput) { modalUidInput.focus(); } }); createUidBtn.addEventListener('click', () => { // Clear input field const uidInput = document.getElementById('modalUidInput'); if (uidInput) { uidInput.value = ''; } // Show modal createUidModal.show(); }); // Add event listener for Enter key on the input field const modalUidInput = document.getElementById('modalUidInput'); if (modalUidInput) { modalUidInput.addEventListener('keyup', (event) => { if (event.key === 'Enter') { document.getElementById('confirmCreateUid').click(); } }); } // Handle confirm create UID button const confirmCreateUidBtn = document.getElementById('confirmCreateUid'); confirmCreateUidBtn.addEventListener('click', async () => { const uidInput = document.getElementById('modalUidInput'); if (uidInput && uidInput.value) { const uid = uidInput.value.trim().toLowerCase(); // Validate UID format if (!/^[a-z0-9-]+$/.test(uid)) { showAlert('danger', 'UID can only contain lowercase letters, numbers, and hyphens'); return; } try { await createUid(uid); // Reload UIDs and select the new one const uids = await loadUids(); // Clear existing options except the placeholder while (uidSelect.options.length > 1) { uidSelect.remove(1); } // Add UIDs to select uids.forEach(uid => { const option = document.createElement('option'); option.value = uid.uid; option.textContent = uid.uid; uidSelect.appendChild(option); }); // Select the new UID uidSelect.value = uid; uidSelect.dispatchEvent(new Event('change')); // Hide modal createUidModal.hide(); // Show success message showAlert('success', `UID "${uid}" created successfully`); } catch (error) { console.error('Error creating UID:', error); showAlert('danger', 'Failed to create UID'); } } }); // Handle delete UID button deleteUidBtn.addEventListener('click', async () => { const selectedUid = uidSelect.value; if (!selectedUid) return; if (confirm(`Are you sure you want to delete UID "${selectedUid}" and all associated availability?`)) { try { await deleteUid(selectedUid); // Reload UIDs and reset selection const uids = await loadUids(); // Clear existing options except the placeholder while (uidSelect.options.length > 1) { uidSelect.remove(1); } // Add UIDs to select uids.forEach(uid => { const option = document.createElement('option'); option.value = uid.uid; option.textContent = uid.uid; uidSelect.appendChild(option); }); // Reset selection uidSelect.value = ''; uidSelect.dispatchEvent(new Event('change')); // Show success message showAlert('success', `UID "${selectedUid}" deleted successfully`); } catch (error) { console.error('Error deleting UID:', error); showAlert('danger', 'Failed to delete UID'); } } }); // Handle flush database button flushDatabaseBtn.addEventListener('click', async () => { const selectedUid = uidSelect.value; if (!selectedUid) { showAlert('warning', 'Please select a UID first'); return; } if (confirm(`Are you sure you want to delete ALL availability entries for UID: ${selectedUid}?`)) { try { await flushAvailability(selectedUid); // Refresh data await fetchAvailability(); // Refresh availability data // Reset UI const selectedDateElem = document.getElementById('selectedDateDisplay'); const timeSlotsElem = document.getElementById('timeSlots'); // Clear UI elements if (selectedDateElem) selectedDateElem.textContent = ''; if (timeSlotsElem) timeSlotsElem.innerHTML = ''; // Re-render the calendar await renderAdminCalendar(currentAdminMonth, currentAdminYear); // Show success message showAlert('success', `All availability for UID "${selectedUid}" deleted successfully`); } catch (error) { console.error('Error flushing availability:', error); showAlert('danger', 'Failed to delete availability'); } } }); // Handle dev tools button const devToolsBtn = document.getElementById('devToolsBtn'); const devToolsModal = new bootstrap.Modal(document.getElementById('devToolsModal')); devToolsBtn.addEventListener('click', () => { devToolsModal.show(); }); // Handle reset database button const resetDatabaseBtn = document.getElementById('resetDatabase'); resetDatabaseBtn.addEventListener('click', async () => { if (confirm('WARNING: This will permanently delete ALL UIDs and availability data. This action cannot be undone. Are you sure?')) { try { await resetDatabase(); // Reload UIDs and reset selection const uids = await loadUids(); // Clear existing options except the placeholder while (uidSelect.options.length > 1) { uidSelect.remove(1); } // Add UIDs to select uids.forEach(uid => { const option = document.createElement('option'); option.value = uid.uid; option.textContent = uid.uid; uidSelect.appendChild(option); }); // Reset selection uidSelect.value = ''; uidSelect.dispatchEvent(new Event('change')); // Hide modal devToolsModal.hide(); // Show success message showAlert('success', 'Database reset successfully'); } catch (error) { console.error('Error resetting database:', error); showAlert('danger', 'Failed to reset database'); } } }); // Handle month navigation const prevMonthBtn = document.getElementById('adminPrevMonth'); const nextMonthBtn = document.getElementById('adminNextMonth'); prevMonthBtn.addEventListener('click', () => { // Update current month and year if (currentAdminMonth === 0) { currentAdminMonth = 11; currentAdminYear--; } else { currentAdminMonth--; } // Render calendar with new month renderAdminCalendar(currentAdminMonth, currentAdminYear); }); nextMonthBtn.addEventListener('click', () => { // Update current month and year if (currentAdminMonth === 11) { currentAdminMonth = 0; currentAdminYear++; } else { currentAdminMonth++; } // Render calendar with new month renderAdminCalendar(currentAdminMonth, currentAdminYear); }); // Load UIDs on page load try { const uids = await loadUids(); // Clear existing options except the placeholder while (uidSelect.options.length > 1) { uidSelect.remove(1); } // Add UIDs to select uids.forEach(uid => { const option = document.createElement('option'); option.value = uid.uid; option.textContent = uid.uid; uidSelect.appendChild(option); }); // Enable/disable buttons based on UID count const hasUids = uids.length > 0; flushDatabaseBtn.disabled = !hasUids; } catch (error) { console.error('Error loading UIDs:', error); } // Initial render with current date if (uidSelect.value) { renderAdminCalendar(currentAdminMonth, currentAdminYear); // Check if current date has availability and select it if it does const today = new Date(); if (today.getMonth() === currentAdminMonth && today.getFullYear() === currentAdminYear) { const hasAvailability = await checkAvailabilityForDate(today); if (hasAvailability) { const firstDay = new Date(currentAdminYear, currentAdminMonth, 1).getDay(); selectAdminDate(today, firstDay); } } } });