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 < / b u t t o n >
< / s p a n >
< button type = "button" class = "timepicker-dot" disabled = "" > : < / b u t t o n >
< span class = "position-relative h-100" >
< button type = "button" class = "timepicker-current timepicker-minute" tabindex = "0" > 00 < / b u t t o n >
< / s p a n >
< / d i v >
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 < / b u t t o n >
< button type = "button" class = "timepicker-hour-mode timepicker-pm active" tabindex = "0" > PM < / b u t t o n >
< / d i v >
< / d i v >
< / d i v >
< 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" > < / s p a n >
< div class = "timepicker-hand-pointer position-absolute" style = "transform: rotateZ(360deg);" >
< div class = "timepicker-circle position-absolute active" > < / d i v >
< / d i v >
<!-- Clock face will be dynamically generated by JS -- >
< / d i v >
< / d i v >
< / d i v >
< 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 < / b u t t o n >
< button type = "button" class = "timepicker-button timepicker-cancel" tabindex = "0" > Cancel < / b u t t o n >
< button type = "button" class = "timepicker-button timepicker-submit" tabindex = "0" > Ok < / b u t t o n >
< / d i v >
< / d i v >
< / d i v >
< / d i v >
< / d i v > ` ;
}
// 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 : 2 px 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 : 2 px solid # 0 d6efd ! important ; /* Blue border for selected */
background - color : # 0 d6efd ! 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.5 rem 1 rem rgba ( 0 , 0 , 0 , 0.15 ) ! important ;
border - radius : 8 px ;
overflow : hidden ;
2025-02-21 20:20:12 +00:00
}
2025-02-25 05:27:29 +00:00
# visualTimePicker . card - header {
background - color : # 0 d6efd ;
2025-02-21 20:20:12 +00:00
}
2025-02-25 05:27:29 +00:00
# visualTimePicker . display - 4 {
font - size : 3 rem ;
font - weight : 300 ;
color : white ;
}
. time - picker - clock {
box - shadow : 0 0 10 px rgba ( 0 , 0 , 0 , 0.1 ) ;
}
. hour - number , . minute - number {
border - radius : 50 % ;
transition : all 0.2 s ease ;
}
. hour - number : hover , . minute - number : hover {
background - color : rgba ( 13 , 110 , 253 , 0.1 ) ;
}
/* Time slot pills styling */
. time - slot {
transition : all 0.2 s ease ;
}
. time - slot : hover {
transform : translateY ( - 2 px ) ;
}
/* Make all UID buttons consistent */
# createUid , # deleteUid , # flushDatabase , # copyUrlBtn {
border - radius : 30 px ;
padding : 0.75 rem 1.5 rem ;
font - size : 1.25 rem ;
font - weight : 500 ;
height : 60 px ;
display : flex ;
align - items : center ;
justify - content : center ;
width : 100 % ;
transition : all 0.3 s ease ;
}
# createUid {
background - color : # 2e8 b57 ; /* Money green color */
border - color : # 2e8 b57 ;
}
# createUid : hover {
background - color : # 267349 ;
border - color : # 267349 ;
}
/* URL container styling */
. url - container {
display : flex ;
align - items : center ;
gap : 10 px ;
}
. 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 } ? \n This 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 ( ) ) ;
}
}
} ) ;