1320 lines
51 KiB
JavaScript
1320 lines
51 KiB
JavaScript
// 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');
|
|
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 = '<i class="bi bi-clock"></i> 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 = '<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);
|
|
});
|
|
|
|
timeSlotContainer.appendChild(timeSlotItem);
|
|
timeSlotContainer.appendChild(deleteIcon);
|
|
timeSlotsContainer.appendChild(timeSlotContainer);
|
|
});
|
|
|
|
timeSlots.appendChild(timeSlotsContainer);
|
|
}
|
|
}
|
|
|
|
// Initialize visual time picker function
|
|
function initializeVisualTimePicker(dateStr, availableTimes) {
|
|
const timeButton = document.getElementById('customTimeInput');
|
|
if (!timeButton) return;
|
|
|
|
// Remove any existing time picker
|
|
const existingTimePicker = document.getElementById('visualTimePicker');
|
|
if (existingTimePicker) {
|
|
existingTimePicker.remove();
|
|
}
|
|
|
|
// Create the visual time picker container
|
|
const timePicker = document.createElement('div');
|
|
timePicker.id = 'visualTimePicker';
|
|
timePicker.classList.add('card', 'shadow');
|
|
timePicker.style.display = 'none';
|
|
timePicker.style.position = 'fixed';
|
|
timePicker.style.zIndex = '1050';
|
|
timePicker.style.width = '300px';
|
|
timePicker.style.top = '50%';
|
|
timePicker.style.left = '50%';
|
|
timePicker.style.transform = 'translate(-50%, -50%)';
|
|
|
|
// Create the header
|
|
const header = document.createElement('div');
|
|
header.classList.add('card-header', 'bg-primary', 'text-white', 'text-center', 'p-0');
|
|
|
|
// Create the time display in the header
|
|
const timeDisplay = document.createElement('div');
|
|
timeDisplay.classList.add('display-4', 'py-3', 'mb-0');
|
|
timeDisplay.innerHTML = '<span id="timePickerHour">12</span>:<span id="timePickerMinute">00</span> <span id="timePickerAMPM">PM</span>';
|
|
header.appendChild(timeDisplay);
|
|
|
|
timePicker.appendChild(header);
|
|
|
|
// Create the body
|
|
const body = document.createElement('div');
|
|
body.classList.add('card-body', 'bg-white');
|
|
|
|
// Create the clock face container
|
|
const clockFace = document.createElement('div');
|
|
clockFace.classList.add('time-picker-clock', 'mb-3');
|
|
clockFace.style.position = 'relative';
|
|
clockFace.style.width = '250px';
|
|
clockFace.style.height = '250px';
|
|
clockFace.style.margin = '0 auto';
|
|
clockFace.style.backgroundColor = '#f8f9fa';
|
|
clockFace.style.borderRadius = '50%';
|
|
|
|
// Add clock center dot
|
|
const clockCenter = document.createElement('div');
|
|
clockCenter.style.position = 'absolute';
|
|
clockCenter.style.top = '50%';
|
|
clockCenter.style.left = '50%';
|
|
clockCenter.style.transform = 'translate(-50%, -50%)';
|
|
clockCenter.style.width = '8px';
|
|
clockCenter.style.height = '8px';
|
|
clockCenter.style.backgroundColor = '#0d6efd';
|
|
clockCenter.style.borderRadius = '50%';
|
|
clockFace.appendChild(clockCenter);
|
|
|
|
// Add hour numbers to the clock face
|
|
for (let i = 1; i <= 12; i++) {
|
|
const hourNumber = document.createElement('div');
|
|
hourNumber.classList.add('hour-number');
|
|
hourNumber.textContent = i;
|
|
hourNumber.style.position = 'absolute';
|
|
hourNumber.style.width = '40px';
|
|
hourNumber.style.height = '40px';
|
|
hourNumber.style.textAlign = 'center';
|
|
hourNumber.style.lineHeight = '40px';
|
|
hourNumber.style.fontWeight = 'bold';
|
|
hourNumber.style.cursor = 'pointer';
|
|
|
|
// Calculate position (in a circle)
|
|
const angle = (i * 30 - 90) * (Math.PI / 180); // 30 degrees per hour, starting at 12 o'clock
|
|
const radius = 100; // Distance from center
|
|
const left = 125 + radius * Math.cos(angle);
|
|
const top = 125 + radius * Math.sin(angle);
|
|
|
|
hourNumber.style.left = `${left - 20}px`; // Adjust for element width
|
|
hourNumber.style.top = `${top - 20}px`; // Adjust for element height
|
|
|
|
// Add click event to select hour
|
|
hourNumber.addEventListener('click', () => {
|
|
document.getElementById('timePickerHour').textContent = i;
|
|
|
|
// Highlight selected hour
|
|
document.querySelectorAll('.hour-number').forEach(el => {
|
|
el.style.backgroundColor = 'transparent';
|
|
el.style.color = '#212529';
|
|
});
|
|
hourNumber.style.backgroundColor = '#0d6efd';
|
|
hourNumber.style.color = 'white';
|
|
|
|
// Show minute selection
|
|
showMinuteSelection();
|
|
});
|
|
|
|
clockFace.appendChild(hourNumber);
|
|
}
|
|
|
|
// Function to show minute selection
|
|
function showMinuteSelection() {
|
|
// Hide hour numbers
|
|
document.querySelectorAll('.hour-number').forEach(el => {
|
|
el.style.display = 'none';
|
|
});
|
|
|
|
// Show minute numbers
|
|
document.querySelectorAll('.minute-number').forEach(el => {
|
|
el.style.display = 'block';
|
|
});
|
|
}
|
|
|
|
// Function to show hour selection
|
|
function showHourSelection() {
|
|
// Hide minute numbers
|
|
document.querySelectorAll('.minute-number').forEach(el => {
|
|
el.style.display = 'none';
|
|
});
|
|
|
|
// Show hour numbers
|
|
document.querySelectorAll('.hour-number').forEach(el => {
|
|
el.style.display = 'block';
|
|
});
|
|
}
|
|
|
|
// Add minute numbers to the clock face (initially hidden)
|
|
const minuteValues = [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55];
|
|
minuteValues.forEach((minute, index) => {
|
|
const minuteNumber = document.createElement('div');
|
|
minuteNumber.classList.add('minute-number');
|
|
minuteNumber.textContent = minute.toString().padStart(2, '0');
|
|
minuteNumber.style.position = 'absolute';
|
|
minuteNumber.style.width = '40px';
|
|
minuteNumber.style.height = '40px';
|
|
minuteNumber.style.textAlign = 'center';
|
|
minuteNumber.style.lineHeight = '40px';
|
|
minuteNumber.style.fontWeight = 'bold';
|
|
minuteNumber.style.cursor = 'pointer';
|
|
minuteNumber.style.display = 'none'; // Initially hidden
|
|
|
|
// Calculate position (in a circle)
|
|
const angle = (index * 30 - 90) * (Math.PI / 180);
|
|
const radius = 100; // Distance from center
|
|
const left = 125 + radius * Math.cos(angle);
|
|
const top = 125 + radius * Math.sin(angle);
|
|
|
|
minuteNumber.style.left = `${left - 20}px`; // Adjust for element width
|
|
minuteNumber.style.top = `${top - 20}px`; // Adjust for element height
|
|
|
|
// Add click event to select minute
|
|
minuteNumber.addEventListener('click', () => {
|
|
document.getElementById('timePickerMinute').textContent = minute.toString().padStart(2, '0');
|
|
|
|
// Highlight selected minute
|
|
document.querySelectorAll('.minute-number').forEach(el => {
|
|
el.style.backgroundColor = 'transparent';
|
|
el.style.color = '#212529';
|
|
});
|
|
minuteNumber.style.backgroundColor = '#0d6efd';
|
|
minuteNumber.style.color = 'white';
|
|
|
|
// Show hour selection again
|
|
showHourSelection();
|
|
});
|
|
|
|
clockFace.appendChild(minuteNumber);
|
|
});
|
|
|
|
body.appendChild(clockFace);
|
|
|
|
// Create AM/PM toggle
|
|
const ampmToggle = document.createElement('div');
|
|
ampmToggle.classList.add('d-flex', 'justify-content-center', 'mb-3');
|
|
|
|
const amButton = document.createElement('button');
|
|
amButton.classList.add('btn', 'btn-outline-primary', 'me-2');
|
|
amButton.textContent = 'AM';
|
|
amButton.addEventListener('click', () => {
|
|
document.getElementById('timePickerAMPM').textContent = 'AM';
|
|
amButton.classList.add('active');
|
|
pmButton.classList.remove('active');
|
|
});
|
|
|
|
const pmButton = document.createElement('button');
|
|
pmButton.classList.add('btn', 'btn-outline-primary');
|
|
pmButton.textContent = 'PM';
|
|
pmButton.addEventListener('click', () => {
|
|
document.getElementById('timePickerAMPM').textContent = 'PM';
|
|
pmButton.classList.add('active');
|
|
amButton.classList.remove('active');
|
|
});
|
|
pmButton.classList.add('active'); // Add active class to PM button
|
|
|
|
ampmToggle.appendChild(amButton);
|
|
ampmToggle.appendChild(pmButton);
|
|
|
|
body.appendChild(ampmToggle);
|
|
|
|
// Create action buttons
|
|
const actionButtons = document.createElement('div');
|
|
actionButtons.classList.add('d-flex', 'justify-content-between');
|
|
|
|
const closeButton = document.createElement('button');
|
|
closeButton.classList.add('btn', 'btn-outline-secondary', 'flex-grow-1', 'me-2');
|
|
closeButton.textContent = 'CANCEL';
|
|
closeButton.addEventListener('click', () => {
|
|
timePicker.style.display = 'none';
|
|
});
|
|
|
|
const okButton = document.createElement('button');
|
|
okButton.classList.add('btn', 'btn-primary', 'flex-grow-1');
|
|
okButton.textContent = 'OK';
|
|
okButton.addEventListener('click', () => {
|
|
const hour = document.getElementById('timePickerHour').textContent;
|
|
const minute = document.getElementById('timePickerMinute').textContent;
|
|
const ampm = document.getElementById('timePickerAMPM').textContent.toLowerCase();
|
|
|
|
const timeValue = `${hour}:${minute}${ampm}`;
|
|
document.getElementById('customTimeInput').value = timeValue;
|
|
|
|
// Check if time already exists in available times
|
|
if (availableTimes.includes(timeValue)) {
|
|
// Silently handle duplicate time slots - no error message
|
|
console.log('Time slot already exists, not adding duplicate');
|
|
} else {
|
|
// Automatically add the time slot
|
|
addSingleTime(dateStr, timeValue);
|
|
}
|
|
|
|
// Hide the time picker
|
|
timePicker.style.display = 'none';
|
|
});
|
|
|
|
actionButtons.appendChild(closeButton);
|
|
actionButtons.appendChild(okButton);
|
|
|
|
body.appendChild(actionButtons);
|
|
|
|
timePicker.appendChild(body);
|
|
|
|
// Add the time picker to the document
|
|
document.body.appendChild(timePicker);
|
|
|
|
// Show time picker when button is clicked
|
|
timeButton.addEventListener('click', (e) => {
|
|
// Remove positioning relative to button
|
|
timePicker.style.display = 'block';
|
|
|
|
// Reset to default view (hour selection)
|
|
showHourSelection();
|
|
|
|
e.stopPropagation();
|
|
});
|
|
|
|
// Close time picker when clicking outside
|
|
document.addEventListener('click', (e) => {
|
|
if (e.target !== timeButton && !timePicker.contains(e.target)) {
|
|
timePicker.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
|
|
// Fetch available dates from the API
|
|
async function fetchAvailability() {
|
|
try {
|
|
const selectedUid = document.getElementById('uidSelect').value;
|
|
if (!selectedUid) return {};
|
|
|
|
const response = await fetch(`/api/availability/${selectedUid}`);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const availability = await response.json();
|
|
return availability;
|
|
} catch (error) {
|
|
console.error('Error fetching availability:', error);
|
|
return {};
|
|
}
|
|
}
|
|
|
|
// Helper function to update calendar UI after availability changes
|
|
async function updateAvailabilityUI() {
|
|
const availability = await fetchAvailability();
|
|
if (selectedAdminDate) {
|
|
await updateTimeSlots(selectedAdminDate);
|
|
}
|
|
|
|
const calendarDates = document.getElementById('adminCalendarDates');
|
|
if (calendarDates) {
|
|
const allDateElements = calendarDates.querySelectorAll('.date-item');
|
|
const today = new Date();
|
|
const currentMinutes = today.getHours() * 60 + today.getMinutes();
|
|
|
|
allDateElements.forEach(dateElement => {
|
|
if (!dateElement.textContent) return; // Skip empty cells
|
|
const day = parseInt(dateElement.textContent);
|
|
const date = new Date(currentAdminYear, currentAdminMonth, day);
|
|
const dateStr = date.toISOString().split('T')[0];
|
|
|
|
// Check if date is in the past
|
|
const startOfToday = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
|
const isPastDate = date < startOfToday;
|
|
const isToday = date.toDateString() === today.toDateString();
|
|
|
|
// Check if the date has available time slots
|
|
let hasAvailableTimes = false;
|
|
if (availability[dateStr]) {
|
|
let times = [];
|
|
if (typeof availability[dateStr] === 'string') {
|
|
times = availability[dateStr].split(',').map(t => t.trim());
|
|
} else if (Array.isArray(availability[dateStr])) {
|
|
times = availability[dateStr].map(t => t.trim());
|
|
}
|
|
|
|
// For today, filter out past times
|
|
if (isToday) {
|
|
times = times.filter(time => timeToMinutes(time) > currentMinutes);
|
|
}
|
|
|
|
// Only mark as available if there are valid times
|
|
hasAvailableTimes = times.length > 0;
|
|
}
|
|
|
|
// Only add 'available' class if the date is not in the past AND has available times
|
|
if (hasAvailableTimes && !isPastDate) {
|
|
dateElement.classList.add('available');
|
|
} else {
|
|
dateElement.classList.remove('available');
|
|
}
|
|
|
|
// Always ensure past dates have the disabled class
|
|
if (isPastDate) {
|
|
dateElement.classList.add('disabled');
|
|
dateElement.classList.remove('available');
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// UID management functions
|
|
async function loadUids() {
|
|
try {
|
|
const response = await fetch('/api/uids');
|
|
if (!response.ok) {
|
|
const errorData = await response.json();
|
|
throw new Error(errorData.error || 'Failed to fetch UIDs');
|
|
}
|
|
const uids = await response.json();
|
|
|
|
const select = document.getElementById('uidSelect');
|
|
select.innerHTML = '<option value="">Select a Schedule UID...</option>';
|
|
|
|
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;
|
|
}
|
|
|
|
/* 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');
|
|
}
|
|
}
|
|
|
|
// 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());
|
|
}
|
|
}
|
|
}); |