freesched/public/admin.js

1549 lines
60 KiB
JavaScript
Raw Normal View History

2025-02-21 20:20:12 +00:00
// 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();
2025-02-25 05:27:29 +00:00
const currentMinutes = today.getHours() * 60 + today.getMinutes();
2025-02-21 20:20:12 +00:00
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');
2025-02-25 05:36:59 +00:00
blank.classList.add('date-item', 'p-2', 'rounded-pill', 'text-center', 'text-muted', 'empty-cell');
blank.style.cursor = 'default'; // Remove pointer cursor
blank.style.pointerEvents = 'none'; // Disable hover and click events
2025-02-21 20:20:12 +00:00
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;
2025-02-25 05:27:29 +00:00
const isToday = date.toDateString() === today.toDateString();
2025-02-21 20:20:12 +00:00
2025-02-25 05:27:29 +00:00
if (isToday) {
2025-02-21 20:20:12 +00:00
day.classList.add('current');
todayElement = day;
}
2025-02-25 05:27:29 +00:00
// Check if the date has available time slots
let hasAvailableTimes = false;
2025-02-21 20:20:12 +00:00
if (availability[dateStr]) {
2025-02-25 05:27:29 +00:00
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) {
2025-02-21 20:20:12 +00:00
day.classList.add('available');
}
2025-02-25 05:27:29 +00:00
2025-02-21 20:20:12 +00:00
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) {
2025-02-25 05:27:29 +00:00
// 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;
}
2025-02-21 20:20:12 +00:00
2025-02-25 05:27:29 +00:00
// 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 => {
2025-02-21 20:20:12 +00:00
const dayText = item.textContent;
const itemDate = new Date(currentAdminYear, currentAdminMonth, parseInt(dayText));
return itemDate.toDateString() === date.toDateString();
});
if (selectedDay) selectedDay.classList.add('selected');
2025-02-25 05:27:29 +00:00
// Remove the selected date display text
2025-02-21 20:20:12 +00:00
const selectedDateDisplay = document.getElementById('selectedDateDisplay');
if (selectedDateDisplay) {
2025-02-25 05:27:29 +00:00
selectedDateDisplay.textContent = '';
2025-02-21 20:20:12 +00:00
}
}
// 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])) {
2025-02-25 05:27:29 +00:00
availableTimes = [...availability[dateStr]];
2025-02-21 20:20:12 +00:00
}
}
// Sort available times chronologically
availableTimes.sort((a, b) => timeToMinutes(a) - timeToMinutes(b));
const timeSlots = document.getElementById('timeSlots');
if (!timeSlots) return;
2025-02-25 05:27:29 +00:00
// Clear existing time slots
2025-02-21 20:20:12 +00:00
timeSlots.innerHTML = '';
2025-02-25 05:27:29 +00:00
// 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();
2025-02-21 20:20:12 +00:00
removeTime(dateStr, time);
});
2025-02-25 05:27:29 +00:00
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();
}
2025-02-27 22:14:07 +00:00
// Create the timepicker modal from the template - moved to a separate function for clarity
document.body.insertAdjacentHTML('beforeend', createTimepickerTemplate());
// Initialize timepicker functionality and store the reference to the controller
const timepickerController = initializeTimepickerFunctionality(dateStr, availableTimes);
2025-02-25 05:27:29 +00:00
2025-02-27 22:14:07 +00:00
// Show time picker when button is clicked
timeButton.addEventListener('click', (e) => {
showTimepicker(timepickerController);
e.stopPropagation();
});
2025-02-25 05:27:29 +00:00
2025-02-27 22:14:07 +00:00
// Close time picker when clicking outside
document.addEventListener('click', (e) => {
const timepicker = document.getElementById('visualTimePicker');
const timepickerContainer = document.querySelector('.timepicker-container');
// Only close if we're not in the middle of a drag operation
if (timepicker &&
timepicker.style.display === 'block' &&
e.target !== timeButton &&
(!timepickerContainer || !timepickerContainer.contains(e.target)) &&
!window.timepickerDragging) { // Check the global dragging flag
timepickerController.closeTimepicker();
}
});
}
// Helper function to show the timepicker
function showTimepicker(timepickerController) {
const timepicker = document.getElementById('visualTimePicker');
if (!timepicker) return;
// First make the timepicker visible but with opacity 0
timepicker.style.display = 'block';
timepicker.style.opacity = '0';
// Force a reflow to ensure the clock dimensions are calculated
setTimeout(() => {
// Always start with hours view when opening the timepicker
timepickerController.switchToHoursView();
// Make the timepicker visible
timepicker.style.opacity = '1';
document.body.style.overflow = 'hidden';
document.body.style.paddingRight = '0px';
}, 10);
}
// Create the timepicker HTML template
function createTimepickerTemplate() {
return `
<div class="timepicker-modal" role="dialog" tabindex="-1" id="visualTimePicker" style="display: none;">
<div id="timepickerWrapper" class="timepicker-wrapper h-100 d-flex align-items-center justify-content-center flex-column position-fixed animation fade-in" style="animation-duration: 300ms;">
<div class="d-flex align-items-center justify-content-center flex-column timepicker-container">
<div class="d-flex flex-column timepicker-elements justify-content-around">
2025-02-25 05:27:29 +00:00
2025-02-27 22:14:07 +00:00
<div id="timepickerHead" class="timepicker-head d-flex flex-row align-items-center justify-content-center" style="padding-right:0px">
<div class="timepicker-head-content d-flex w-100 justify-content-evenly">
<div class="timepicker-current-wrapper">
<span class="position-relative h-100">
<button type="button" class="timepicker-current timepicker-hour active" tabindex="0" style="pointer-events: none;">12</button>
</span>
<button type="button" class="timepicker-dot" disabled="">:</button>
<span class="position-relative h-100">
<button type="button" class="timepicker-current timepicker-minute" tabindex="0">00</button>
</span>
</div>
2025-02-25 05:27:29 +00:00
2025-02-27 22:14:07 +00:00
<div class="d-flex flex-column justify-content-center timepicker-mode-wrapper">
<button type="button" class="timepicker-hour-mode timepicker-am" tabindex="0">AM</button>
<button type="button" class="timepicker-hour-mode timepicker-pm active" tabindex="0">PM</button>
</div>
</div>
</div>
<div id="timepickerClockWrapper" class="timepicker-clock-wrapper d-flex justify-content-center flex-column align-items-center">
<div class="timepicker-clock timepicker-clock-animation">
<span class="timepicker-middle-dot position-absolute"></span>
<div class="timepicker-hand-pointer position-absolute" style="transform: rotateZ(360deg);">
<div class="timepicker-circle position-absolute active"></div>
</div>
<!-- Clock face will be dynamically generated by JS -->
</div>
</div>
</div>
<div id="timepickerFooter" class="timepicker-footer">
<div class="w-100 d-flex justify-content-between">
<button type="button" class="timepicker-button timepicker-clear" tabindex="0">Clear</button>
<button type="button" class="timepicker-button timepicker-cancel" tabindex="0">Cancel</button>
<button type="button" class="timepicker-button timepicker-submit" tabindex="0">Ok</button>
</div>
</div>
</div>
</div>
</div>`;
}
// Initialize the timepicker functionality
function initializeTimepickerFunctionality(dateStr, availableTimes) {
// DOM elements - group related elements together
const timeElements = {
hour: document.querySelector('.timepicker-hour'),
minute: document.querySelector('.timepicker-minute'),
am: document.querySelector('.timepicker-am'),
pm: document.querySelector('.timepicker-pm'),
dot: document.querySelector('.timepicker-dot')
};
const clockElements = {
wrapper: document.querySelector('.timepicker-clock-wrapper'),
clock: document.querySelector('.timepicker-clock'),
hand: document.querySelector('.timepicker-hand-pointer')
};
const actionButtons = {
clear: document.querySelector('.timepicker-clear'),
cancel: document.querySelector('.timepicker-cancel'),
submit: document.querySelector('.timepicker-submit')
};
// State variables
const state = {
currentView: 'hours',
selectedHour: 12,
selectedMinute: 0,
isDragging: false,
isPM: timeElements.pm.classList.contains('active')
};
// Create a global variable to track if we're dragging from inside the timepicker
window.timepickerDragging = false;
// Initialize
setupEventListeners();
renderClockFace(state.currentView);
function setupEventListeners() {
// Header hour/minute buttons
timeElements.hour.addEventListener('click', () => switchView('hours'));
timeElements.minute.addEventListener('click', () => switchView('minutes'));
2025-02-25 05:27:29 +00:00
2025-02-27 22:14:07 +00:00
// Clock face events
clockElements.clock.addEventListener('mousedown', handleClockMouseDown);
document.addEventListener('mousemove', handleClockMouseMove);
document.addEventListener('mouseup', handleClockMouseUp);
// Prevent text selection on the clock
clockElements.clock.style.userSelect = 'none';
clockElements.clock.style.webkitUserSelect = 'none';
clockElements.clock.style.msUserSelect = 'none';
// AM/PM buttons
timeElements.am.addEventListener('click', () => setAmPm('AM'));
timeElements.pm.addEventListener('click', () => setAmPm('PM'));
// Action buttons
actionButtons.clear.addEventListener('click', clearTime);
actionButtons.cancel.addEventListener('click', closeTimepicker);
actionButtons.submit.addEventListener('click', submitTime);
2025-02-25 05:27:29 +00:00
}
2025-02-27 22:14:07 +00:00
function switchView(view) {
state.currentView = view;
2025-02-25 05:27:29 +00:00
2025-02-27 22:14:07 +00:00
// Update active state in header
if (view === 'hours') {
timeElements.hour.classList.add('active');
timeElements.minute.classList.remove('active');
timeElements.hour.style.pointerEvents = 'none';
timeElements.minute.style.pointerEvents = 'auto';
} else {
timeElements.hour.classList.remove('active');
timeElements.minute.classList.add('active');
timeElements.hour.style.pointerEvents = 'auto';
timeElements.minute.style.pointerEvents = 'none';
}
renderClockFace(view);
2025-02-25 05:27:29 +00:00
}
2025-02-27 22:14:07 +00:00
function renderClockFace(view) {
// Clear existing time tips
const existingTips = clockElements.clock.querySelectorAll('.timepicker-time-tips-hours, .timepicker-time-tips-minutes');
existingTips.forEach(tip => tip.remove());
2025-02-25 05:27:29 +00:00
2025-02-27 22:14:07 +00:00
if (view === 'hours') {
renderHoursFace();
updateHandPosition(state.selectedHour, 'hours');
} else {
renderMinutesFace();
updateHandPosition(state.selectedMinute, 'minutes');
}
2025-02-25 05:27:29 +00:00
}
2025-02-27 22:14:07 +00:00
function renderHoursFace() {
const clockRadius = clockElements.clock.offsetWidth / 2;
const tipRadius = clockRadius * 0.85; // Position closer to the edge (85% of radius)
for (let hour = 1; hour <= 12; hour++) {
const tip = createClockTip(hour, clockRadius, tipRadius, 'hours');
2025-02-25 05:27:29 +00:00
2025-02-27 22:14:07 +00:00
if (hour === state.selectedHour) {
tip.classList.add('active');
}
2025-02-25 05:27:29 +00:00
2025-02-27 22:14:07 +00:00
clockElements.clock.appendChild(tip);
}
}
function renderMinutesFace() {
const clockRadius = clockElements.clock.offsetWidth / 2;
const tipRadius = clockRadius * 0.85; // Position closer to the edge (85% of radius)
2025-02-25 05:27:29 +00:00
2025-02-27 22:14:07 +00:00
for (let minute = 0; minute < 60; minute += 5) {
const tip = createClockTip(minute, clockRadius, tipRadius, 'minutes');
if (minute === state.selectedMinute) {
tip.classList.add('active');
}
clockElements.clock.appendChild(tip);
}
}
2025-02-25 05:27:29 +00:00
2025-02-27 22:14:07 +00:00
// Helper function to create clock face tips (numbers)
function createClockTip(value, clockRadius, tipRadius, type) {
// Calculate angle and position
let angle, displayValue;
if (type === 'hours') {
angle = ((value * 30) - 90) * (Math.PI / 180);
displayValue = value;
} else {
angle = ((value * 6) - 90) * (Math.PI / 180);
displayValue = value.toString().padStart(2, '0');
}
// Calculate position
const left = clockRadius + tipRadius * Math.cos(angle);
const top = clockRadius + tipRadius * Math.sin(angle);
// Create the tip element
const tip = document.createElement('span');
tip.className = `timepicker-time-tips-${type}`;
tip.style.left = `${left}px`;
tip.style.top = `${top}px`;
tip.style.position = 'absolute';
tip.style.transform = 'translate(-50%, -50%)';
tip.style.userSelect = 'none';
tip.style.webkitUserSelect = 'none';
tip.style.msUserSelect = 'none';
const tipElement = document.createElement('span');
tipElement.className = 'timepicker-tips-element';
tipElement.textContent = displayValue;
tip.appendChild(tipElement);
return tip;
}
function updateHandPosition(value, view) {
let angle;
if (view === 'hours') {
// For hours, convert 12 to 0 for calculation purposes
const hour = value === 12 ? 0 : value;
angle = (hour / 12) * 360;
} else {
angle = (value / 60) * 360;
}
clockElements.hand.style.transform = `rotateZ(${angle}deg)`;
// Update the active circle on the hand
const circle = clockElements.hand.querySelector('.timepicker-circle');
if (circle) {
circle.classList.add('active');
}
}
function handleClockMouseDown(event) {
state.isDragging = true;
// Set the global flag to indicate we're dragging from inside the timepicker
window.timepickerDragging = true;
updateTimeFromClockPosition(event);
}
function handleClockMouseMove(event) {
if (state.isDragging) {
updateTimeFromClockPosition(event);
}
}
function handleClockMouseUp() {
if (state.isDragging) {
state.isDragging = false;
// Reset the global dragging flag after a short delay
// This allows the click event to process first
setTimeout(() => {
window.timepickerDragging = false;
}, 10);
// If we just finished selecting an hour, switch to minutes
if (state.currentView === 'hours') {
switchView('minutes');
}
}
}
function updateTimeFromClockPosition(event) {
const clockRect = clockElements.clock.getBoundingClientRect();
const centerX = clockRect.left + clockRect.width / 2;
const centerY = clockRect.top + clockRect.height / 2;
// Calculate angle from center to mouse position
const x = event.clientX - centerX;
const y = event.clientY - centerY;
// Calculate angle in degrees, starting from 12 o'clock position
let angle = Math.atan2(y, x) * (180 / Math.PI) + 90;
// Normalize angle to 0-360
if (angle < 0) {
angle += 360;
}
if (state.currentView === 'hours') {
updateHourFromAngle(angle);
} else {
updateMinuteFromAngle(angle);
}
}
2025-02-25 05:27:29 +00:00
2025-02-27 22:14:07 +00:00
function updateHourFromAngle(angle) {
// Convert angle to hour (1-12)
let hour = Math.round(angle / 30);
// Handle edge cases for full circle
if (hour === 0 || hour > 12) hour = 12;
state.selectedHour = hour;
timeElements.hour.textContent = hour;
updateHandPosition(hour, 'hours');
// Update active class on hour tips
const hourTips = clockElements.clock.querySelectorAll('.timepicker-time-tips-hours');
hourTips.forEach(tip => {
const hourValue = parseInt(tip.querySelector('.timepicker-tips-element').textContent);
if (hourValue === state.selectedHour) {
tip.classList.add('active');
} else {
tip.classList.remove('active');
}
});
}
2025-02-25 05:27:29 +00:00
2025-02-27 22:14:07 +00:00
function updateMinuteFromAngle(angle) {
// Convert angle to minute (0-55, step 5)
let minute = Math.floor(angle / 6);
2025-02-25 05:27:29 +00:00
2025-02-27 22:14:07 +00:00
// Round to nearest 5
minute = Math.round(minute / 5) * 5;
// Handle edge case for full circle
if (minute >= 60) minute = 0;
state.selectedMinute = minute;
timeElements.minute.textContent = minute.toString().padStart(2, '0');
updateHandPosition(minute, 'minutes');
// Update active class on minute tips
const minuteTips = clockElements.clock.querySelectorAll('.timepicker-time-tips-minutes');
minuteTips.forEach(tip => {
const minuteValue = parseInt(tip.querySelector('.timepicker-tips-element').textContent);
if (minuteValue === state.selectedMinute) {
tip.classList.add('active');
} else {
tip.classList.remove('active');
}
});
}
function setAmPm(period) {
if (period === 'AM') {
timeElements.am.classList.add('active');
timeElements.pm.classList.remove('active');
state.isPM = false;
} else {
timeElements.am.classList.remove('active');
timeElements.pm.classList.add('active');
state.isPM = true;
}
}
function clearTime() {
state.selectedHour = 12;
state.selectedMinute = 0;
timeElements.hour.textContent = '12';
timeElements.minute.textContent = '00';
setAmPm('PM');
switchView('hours');
}
function closeTimepicker() {
const timepicker = document.getElementById('visualTimePicker');
if (timepicker) {
timepicker.style.display = 'none';
document.body.style.overflow = '';
document.body.style.paddingRight = '';
}
}
function submitTime() {
const formattedHour = state.selectedHour;
const formattedMinute = state.selectedMinute.toString().padStart(2, '0');
const period = state.isPM ? 'PM' : 'AM';
const timeValue = `${formattedHour}:${formattedMinute}${period.toLowerCase()}`;
2025-02-25 05:27:29 +00:00
// 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);
2025-02-21 20:20:12 +00:00
}
2025-02-27 22:14:07 +00:00
closeTimepicker();
}
// Return controller object with public methods
return {
renderClockFace,
closeTimepicker,
switchToHoursView: () => switchView('hours')
};
2025-02-21 20:20:12 +00:00
}
// 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');
2025-02-25 05:27:29 +00:00
const today = new Date();
const currentMinutes = today.getHours() * 60 + today.getMinutes();
2025-02-21 20:20:12 +00:00
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];
2025-02-25 05:27:29 +00:00
// 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;
2025-02-21 20:20:12 +00:00
if (availability[dateStr]) {
2025-02-25 05:27:29 +00:00
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) {
2025-02-21 20:20:12 +00:00
dateElement.classList.add('available');
} else {
dateElement.classList.remove('available');
}
2025-02-25 05:27:29 +00:00
// Always ensure past dates have the disabled class
if (isPastDate) {
dateElement.classList.add('disabled');
dateElement.classList.remove('available');
}
2025-02-21 20:20:12 +00:00
});
}
}
// 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}`;
}
2025-02-25 05:27:29 +00:00
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 */
2025-02-21 20:20:12 +00:00
}
2025-02-25 05:27:29 +00:00
/* 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;
2025-02-21 20:20:12 +00:00
}
2025-02-25 05:27:29 +00:00
2025-02-25 05:36:59 +00:00
/* 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;
}
2025-02-25 05:27:29 +00:00
/* Time picker styling */
#visualTimePicker {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
border-radius: 8px;
overflow: hidden;
2025-02-21 20:20:12 +00:00
}
2025-02-25 05:27:29 +00:00
#visualTimePicker .card-header {
background-color: #0d6efd;
2025-02-21 20:20:12 +00:00
}
2025-02-25 05:27:29 +00:00
#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');
});
2025-02-21 20:20:12 +00:00
}
});
2025-02-25 05:27:29 +00:00
// 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');
}
2025-02-21 20:20:12 +00:00
}
2025-02-25 05:27:29 +00:00
2025-02-21 20:20:12 +00:00
// Load existing UIDs
await loadUids();
// Handle UID selection
const uidSelect = document.getElementById('uidSelect');
const deleteUidBtn = document.getElementById('deleteUid');
const uidUrl = document.getElementById('uidUrl');
2025-02-25 05:27:29 +00:00
const uidPlaceholder = document.getElementById('uidPlaceholder');
2025-02-21 20:20:12 +00:00
uidSelect.addEventListener('change', async () => {
const selectedUid = uidSelect.value;
deleteUidBtn.disabled = !selectedUid;
flushDatabaseBtn.disabled = !selectedUid;
2025-02-25 05:27:29 +00:00
// 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');
}
}
2025-02-21 20:20:12 +00:00
// 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;
2025-02-25 05:27:29 +00:00
2025-02-21 20:20:12 +00:00
// Refresh calendar with new UID's data
await renderAdminCalendar(currentAdminMonth, currentAdminYear);
2025-02-25 05:27:29 +00:00
// 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');
}
2025-02-21 20:20:12 +00:00
}
} else {
2025-02-25 05:27:29 +00:00
uidUrl.textContent = '';
2025-02-21 20:20:12 +00:00
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');
2025-02-25 05:27:29 +00:00
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();
});
2025-02-21 20:20:12 +00:00
2025-02-25 05:27:29 +00:00
// 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();
2025-02-21 20:20:12 +00:00
if (!/^[a-z0-9-]+$/.test(uid)) {
alert('UID can only contain lowercase letters, numbers, and hyphens');
return;
}
if (await createUid(uid)) {
2025-02-25 05:27:29 +00:00
createUidModal.hide();
2025-02-21 20:20:12 +00:00
} 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');
2025-02-25 05:27:29 +00:00
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();
});
2025-02-21 20:20:12 +00:00
// 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
2025-02-27 23:25:53 +00:00
// Re-render the calendar
2025-02-21 20:20:12 +00:00
await renderAdminCalendar(currentAdminMonth, currentAdminYear);
2025-02-27 23:25:53 +00:00
// If a date was previously selected in the calendar, update the time slots UI
const selectedDateElement = document.querySelector('#adminCalendarDates .date-item.selected');
if (selectedDateElement) {
const day = parseInt(selectedDateElement.textContent);
selectedAdminDate = new Date(currentAdminYear, currentAdminMonth, day);
await updateTimeSlots(selectedAdminDate);
} else {
// If no date was selected, select today's date if it's in the current month
const today = new Date();
if (today.getMonth() === currentAdminMonth && today.getFullYear() === currentAdminYear) {
selectedAdminDate = today;
await updateTimeSlots(today);
// Find and highlight today in the calendar
const dateItems = document.querySelectorAll('#adminCalendarDates .date-item');
const todayDay = today.getDate();
const todayElement = Array.from(dateItems).find(item => {
return item.textContent && parseInt(item.textContent) === todayDay;
});
if (todayElement) {
todayElement.classList.add('selected');
}
}
}
2025-02-21 20:20:12 +00:00
} 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());
}
}
});