810 lines
32 KiB
JavaScript
810 lines
32 KiB
JavaScript
|
// 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 = '<i class="bi bi-clock"></i> 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 = '<i class="bi bi-trash"></i>';
|
||
|
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}
|
||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||
|
`;
|
||
|
|
||
|
// 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 = '<i class="bi bi-clipboard"></i><span style="margin-left: 8px;">Copy URL</span>';
|
||
|
|
||
|
// 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 = '<i class="bi bi-check"></i> 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);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
});
|