freesched/public/admin.js

826 lines
30 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'];
// Generate all possible time slots in chronological order (8am to 5pm)
const timeSlotsData = [
'8:00am', '9:00am', '10:00am', '11:00am', '12:00pm',
'1:00pm', '2:00pm', '3:00pm', '4:00pm', '5:00pm'
];
// 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 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;
if (date.toDateString() === today.toDateString()) {
day.classList.add('current');
todayElement = day;
}
if (availability[dateStr]) {
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) {
selectedAdminDate = date;
const calendarDates = document.getElementById('adminCalendarDates');
if (!calendarDates) return;
// Remove selected class from all dates
const allDates = calendarDates.querySelectorAll('.date-item');
allDates.forEach(item => item.classList.remove('selected'));
// Add selected class to the clicked date
const selectedDay = Array.from(allDates).find(item => {
if (!item.textContent) return false;
const dayText = item.textContent;
const itemDate = new Date(currentAdminYear, currentAdminMonth, parseInt(dayText));
return itemDate.toDateString() === date.toDateString();
});
if (selectedDay) selectedDay.classList.add('selected');
const selectedDateDisplay = document.getElementById('selectedDateDisplay');
if (selectedDateDisplay) {
selectedDateDisplay.textContent = `Selected Date: ${date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}`;
}
// Update time slots for the selected date
await updateTimeSlots(date);
}
// 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].map(t => t.trim());
}
}
// Sort available times chronologically
availableTimes.sort((a, b) => timeToMinutes(a) - timeToMinutes(b));
const timeSlots = document.getElementById('timeSlots');
if (!timeSlots) return;
timeSlots.innerHTML = '';
// Get current time in minutes for comparison
const now = new Date();
const currentMinutes = now.getHours() * 60 + now.getMinutes();
const isToday = date.toDateString() === now.toDateString();
// Create a map of all time slots with their status
const allSlots = new Map();
timeSlotsData.forEach(time => {
const timeInMinutes = timeToMinutes(time);
// Skip times that are in the past on the current day
if (isToday && timeInMinutes <= currentMinutes) {
return;
}
allSlots.set(time, availableTimes.includes(time));
});
// Sort all slots chronologically
const sortedSlots = Array.from(allSlots.entries())
.sort((a, b) => timeToMinutes(a[0]) - timeToMinutes(b[0]));
// Display all slots in chronological order
sortedSlots.forEach(([time, isAvailable]) => {
const button = document.createElement('button');
button.classList.add('time-slot', 'btn', 'rounded-pill', 'w-100', 'mb-2', 'py-2');
button.textContent = time;
if (isAvailable) {
button.classList.add('btn-primary', 'available');
button.addEventListener('click', () => {
button.classList.remove('btn-primary', 'available');
button.classList.add('btn-outline-secondary');
removeTime(dateStr, time);
});
} else {
button.classList.add('btn-outline-secondary');
button.addEventListener('click', () => {
button.classList.remove('btn-outline-secondary');
button.classList.add('btn-primary', 'available');
addSingleTime(dateStr, time);
});
}
timeSlots.appendChild(button);
});
}
// 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');
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];
if (availability[dateStr]) {
dateElement.classList.add('available');
} else {
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}`;
}
// 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');
const selectedDateDisplay = document.getElementById('selectedDateDisplay');
if (selectedDateDisplay) {
selectedDateDisplay.textContent = `Selected Date: ${date.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' })}`;
}
}
// 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].map(t => t.trim());
}
}
// Sort available times chronologically
availableTimes.sort((a, b) => timeToMinutes(a) - timeToMinutes(b));
const timeSlots = document.getElementById('timeSlots');
if (!timeSlots) return;
timeSlots.innerHTML = '';
// Get current time in minutes for comparison
const now = new Date();
const currentMinutes = now.getHours() * 60 + now.getMinutes();
const isToday = date.toDateString() === now.toDateString();
// Create a map of all time slots with their status
const allSlots = new Map();
timeSlotsData.forEach(time => {
const timeInMinutes = timeToMinutes(time);
// Skip times that are in the past on the current day
if (isToday && timeInMinutes <= currentMinutes) {
return;
}
allSlots.set(time, availableTimes.includes(time));
});
// Sort all slots chronologically
const sortedSlots = Array.from(allSlots.entries())
.sort((a, b) => timeToMinutes(a[0]) - timeToMinutes(b[0]));
// Display all slots in chronological order
sortedSlots.forEach(([time, isAvailable]) => {
const button = document.createElement('button');
button.classList.add('time-slot', 'btn', 'rounded-pill', 'w-100', 'mb-2', 'py-2');
button.textContent = time;
if (isAvailable) {
button.classList.add('btn-primary', 'available');
button.addEventListener('click', () => {
button.classList.remove('btn-primary', 'available');
button.classList.add('btn-outline-secondary');
removeTime(dateStr, time);
});
} else {
button.classList.add('btn-outline-secondary');
button.addEventListener('click', () => {
button.classList.remove('btn-outline-secondary');
button.classList.add('btn-primary', 'available');
addSingleTime(dateStr, time);
});
}
timeSlots.appendChild(button);
});
}
// 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');
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];
if (availability[dateStr]) {
dateElement.classList.add('available');
} else {
dateElement.classList.remove('available');
}
});
}
}
document.addEventListener('DOMContentLoaded', async () => {
// Load existing UIDs
await loadUids();
// Handle UID selection
const uidSelect = document.getElementById('uidSelect');
const deleteUidBtn = document.getElementById('deleteUid');
const uidUrl = document.getElementById('uidUrl');
uidSelect.addEventListener('change', async () => {
const selectedUid = uidSelect.value;
deleteUidBtn.disabled = !selectedUid;
flushDatabaseBtn.disabled = !selectedUid;
// 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);
// Show availability for current date
if (selectedAdminDate) {
await updateTimeSlots(selectedAdminDate);
}
} else {
uidUrl.textContent = 'Select a UID first';
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 newUidInput = document.getElementById('newUid');
const createUidBtn = document.getElementById('createUid');
createUidBtn.addEventListener('click', async () => {
const uid = newUidInput.value.trim().toLowerCase();
if (!/^[a-z0-9-]+$/.test(uid)) {
alert('UID can only contain lowercase letters, numbers, and hyphens');
return;
}
if (await createUid(uid)) {
newUidInput.value = '';
} 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');
// 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;
}
// Double confirm for dangerous action
if (!confirm('Please confirm one more time that you want to reset the entire database. All data will be lost.')) {
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());
}
}
});