const calendarEl = document.getElementById('calendar'); let nextAvailableDateSelector = $('.djangoAppt_next-available-date') const body = $('body'); let nonWorkingDays = []; let selectedDate = rescheduledDate || null; let staffId = $('#staff_id').val() || null; let previouslySelectedCell = null; let isRequestInProgress = false; const calendar = new FullCalendar.Calendar(calendarEl, { initialView: 'dayGridMonth', initialDate: selectedDate, timeZone: timezone, headerToolbar: { left: 'title', right: 'prev,today,next', }, themeSystem: 'bootstrap', nowIndicator: true, bootstrapFontAwesome: { close: 'fa-times', prev: 'fa-chevron-left', next: 'fa-chevron-right', prevYear: 'fa-angle-double-left', nextYear: 'fa-angle-double-right' }, selectable: true, dateClick: function (info) { const day = info.date.getDay(); // Get the day of the week (0 for Sunday, 6 for Saturday) if (nonWorkingDays.includes(day)) { return; } // If there's a previously selected cell, remove the class if (previouslySelectedCell) { previouslySelectedCell.classList.remove('selected-cell'); } // Add the class to the currently clicked cell info.dayEl.classList.add('selected-cell'); // Store the currently clicked cell previouslySelectedCell = info.dayEl; selectedDate = info.dateStr; getAvailableSlots(info.dateStr, staffId); }, datesSet: function (info) { highlightSelectedDate(); }, selectAllow: function (info) { const day = info.start.getDay(); // Get the day of the week (0 for Sunday, 6 for Saturday) if (nonWorkingDays.includes(day)) { return false; // Disallow selection for non-working days } return (info.start >= getDateWithoutTime(new Date())); }, dayCellClassNames: function (info) { const day = info.date.getDay(); if (nonWorkingDays.includes(day)) { return ['disabled-day']; } return []; }, }); calendar.setOption('locale', locale); $(document).ready(function () { staffId = $('#staff_id').val() || null; calendar.render(); const currentDate = rescheduledDate || moment.tz(timezone).format('YYYY-MM-DD'); getAvailableSlots(currentDate, staffId); }); function highlightSelectedDate() { setTimeout(function () { const dateCell = document.querySelector(`.fc-daygrid-day[data-date='${selectedDate}']`); if (dateCell) { dateCell.classList.add('selected-cell'); previouslySelectedCell = dateCell; } }, 10); } body.on('click', '.djangoAppt_btn-request-next-slot', function () { const serviceId = $(this).data('service-id'); requestNextAvailableSlot(serviceId); }) body.on('click', '.btn-submit-appointment', function () { const selectedSlot = $('.djangoAppt_appointment-slot.selected').text(); const selectedDate = $('.djangoAppt_date_chosen').text(); if (!selectedSlot || !selectedDate) { alert(selectDateAndTimeAlertTxt); return; } if (selectedSlot && selectedDate) { const startTime = convertTo24Hour(selectedSlot); const APPOINTMENT_BASE_TEMPLATE = localStorage.getItem('APPOINTMENT_BASE_TEMPLATE'); // Convert the selectedDate string to a valid format const dateParts = selectedDate.split(', '); const monthDayYear = dateParts[1] + "," + dateParts[2]; const formattedDate = new Date(monthDayYear + " " + startTime); const date = formattedDate.toISOString().slice(0, 10); const endTimeDate = new Date(formattedDate.getTime() + serviceDuration * 60000); const endTime = formatTime(endTimeDate); const reasonForRescheduling = $('#reason_for_rescheduling').val(); const form = $('.appointment-form'); let formAction = rescheduledDate ? appointmentRescheduleURL : appointmentRequestSubmitURL; form.attr('action', formAction); if (!form.find('input[name="appointment_request_id"]').length) { form.append($('', { type: 'hidden', name: 'appointment_request_id', value: appointmentRequestId })); } form.append($('', {type: 'hidden', name: 'date', value: date})); form.append($('', {type: 'hidden', name: 'start_time', value: startTime})); form.append($('', {type: 'hidden', name: 'end_time', value: endTime})); form.append($('', {type: 'hidden', name: 'service', value: serviceId})); form.append($('', {type: 'hidden', name: 'reason_for_rescheduling', value: reasonForRescheduling})); form.submit(); } else { const warningContainer = $('.warning-message'); if (warningContainer.find('submit-warning') === 0) { warningContainer.append('

' + selectTimeSlotWarningTxt + '

'); } } }); $('#staff_id').on('change', function () { staffId = $(this).val() || null; // If staffId is an empty string, set it to null let currentDate = null if (selectedDate == null) { currentDate = moment.tz(timezone).format('YYYY-MM-DD'); } else { currentDate = selectedDate; } fetchNonWorkingDays(staffId, function (newNonWorkingDays) { nonWorkingDays = newNonWorkingDays; // Update the nonWorkingDays array calendar.render(); // Re-render the calendar to apply changes // Fetch available slots for the current date getAvailableSlots(currentDate, staffId); }); }); function fetchNonWorkingDays(staffId, callback) { if (!staffId || staffId === 'none') { nonWorkingDays = []; // Reset nonWorkingDays calendar.render(); // Re-render the calendar callback([]); return; // Exit the function early } let ajaxData = { 'staff_member': staffId, }; $.ajax({ url: getNonWorkingDaysURL, data: ajaxData, dataType: 'json', success: function (data) { if (data.error) { console.error('Error fetching non-working days:', data.message); callback([]); } else { nonWorkingDays = data.non_working_days; calendar.render(); callback(data.non_working_days); } } }); } function getDateWithoutTime(dt) { dt.setHours(0, 0, 0, 0); return dt; } function convertTo24Hour(time12h) { const [time, modifier] = time12h.split(' '); let [hours, minutes] = time.split(':'); if (hours === '12') { hours = '00'; } if (modifier.toUpperCase() === 'PM') { hours = parseInt(hours, 10) + 12; } return `${hours}:${minutes}`; } function formatTime(date) { const hours = date.getHours(); const minutes = date.getMinutes(); return (hours < 10 ? '0' + hours : hours) + ':' + (minutes < 10 ? '0' + minutes : minutes); } function getAvailableSlots(selectedDate, staffId = null) { // Update the slot list with the available slots for the selected date const slotList = $('#slot-list'); const slotContainer = $('.slot-container'); const errorMessageContainer = $('.error-message'); // Clear previous error messages and slots slotList.empty(); errorMessageContainer.find('.djangoAppt_no-availability-text').remove(); // Remove the "Next available date" message nextAvailableDateSelector = $('.djangoAppt_next-available-date'); // Update the selector nextAvailableDateSelector.remove(); // Correctly check if staffId is 'none', null, or undefined and exit the function if true // Check if 'staffId' is 'none', null, or undefined and display an error message if (staffId === 'none' || staffId === null || staffId === undefined) { console.log('No staff ID provided, displaying error message.'); const errorMessage = $('

' + noStaffMemberSelectedTxt + '

'); errorMessageContainer.append(errorMessage); // Optionally disable the "submit" button here $('.btn-submit-appointment').attr('disabled', 'disabled'); return; // Exit the function early } let ajaxData = { 'selected_date': selectedDate, 'staff_member': staffId, }; fetchNonWorkingDays(staffId, function (nonWorkingDays) { // Check if nonWorkingDays is an array if (Array.isArray(nonWorkingDays)) { // Update the FullCalendar configuration // calendar.setOption('hiddenDays', nonWorkingDays); } else { // Handle the case where there's an error or no data // For now, we'll just log it, but you can handle it more gracefully if needed console.error('Failed to get non-working days:', nonWorkingDays); } }); // Send an AJAX request to get the available slots for the selected date if (isRequestInProgress) { return; // Exit the function if a request is already in progress } isRequestInProgress = true; $.ajax({ url: availableSlotsAjaxURL, data: ajaxData, dataType: 'json', success: function (data) { if (data.available_slots.length === 0) { const selectedDateObj = moment.tz(selectedDate, timezone); const selectedD = selectedDateObj.toDate(); const today = new Date(); today.setHours(0, 0, 0, 0); if (selectedD < today) { // Show an error message errorMessageContainer.append('

' + dateInPastErrorTxt + '

'); if (slotContainer.find('.djangoAppt_btn-request-next-slot').length === 0) { slotContainer.append(``); } // Disable the 'submit' button $('.btn-submit-appointment').attr('disabled', 'disabled'); } else { errorMessageContainer.find('.djangoAppt_no-availability-text').remove(); if (errorMessageContainer.find('.djangoAppt_no-availability-text').length === 0) { errorMessageContainer.append(`

${data.message}

`); } // Check if the returned message is 'No availability' if (data.message.toLowerCase() === 'no availability') { if (slotContainer.find('.djangoAppt_btn-request-next-slot').length === 0) { slotContainer.append(``); } } else { $('.djangoAppt_btn-request-next-slot').remove(); } } } else { // remove the button to request for next available slot $('.djangoAppt_no-availability-text').remove(); $('.djangoAppt_btn-request-next-slot').remove(); const uniqueSlots = [...new Set(data.available_slots)]; // remove duplicates for (let i = 0; i < uniqueSlots.length; i++) { slotList.append('
  • ' + uniqueSlots[i] + '
  • '); } // Attach click event to the slots $('.djangoAppt_appointment-slot').on('click', function () { // Remove the 'selected' class from all other appointment slots $('.djangoAppt_appointment-slot').removeClass('selected'); // Add the 'selected' class to the clicked appointment slot $(this).addClass('selected'); // Enable the submit button $('.btn-submit-appointment').removeAttr('disabled'); // Continue with the existing logic const selectedSlot = $(this).text(); $('#service-datetime-chosen').text(data.date_chosen + ' ' + selectedSlot); }); } // Update the date chosen $('.djangoAppt_date_chosen').text(data.date_chosen); $('#service-datetime-chosen').text(data.date_chosen); isRequestInProgress = false; }, error: function() { isRequestInProgress = false; // Ensure the flag is reset even if the request fails } }); } function requestNextAvailableSlot(serviceId) { const requestNextAvailableSlotURL = requestNextAvailableSlotURLTemplate.replace('0', serviceId); if (staffId === null) { return; } let ajaxData = { 'staff_member': staffId, }; $.ajax({ url: requestNextAvailableSlotURL, data: ajaxData, dataType: 'json', success: function (data) { // If there's an error, just log it and return let nextAvailableDateResponse = null; let formattedDate = null; if (data.error) { nextAvailableDateResponse = data.message; } else { // Set the date in the calendar to the next available date nextAvailableDateResponse = data.next_available_date; const selectedDateObj = moment.tz(nextAvailableDateResponse, timezone); const nextAvailableDate = selectedDateObj.toDate(); formattedDate = new Intl.DateTimeFormat('en-US', { year: 'numeric', month: 'long', day: 'numeric' }).format(nextAvailableDate); } // Check if the .next-available-date element already exists nextAvailableDateSelector = $('.djangoAppt_next-available-date'); // Update the selector let nextAvailableDateText = null; if (data.error) { nextAvailableDateText = nextAvailableDateResponse; } else { nextAvailableDateText = `Next available date: ${formattedDate}`; } if (nextAvailableDateSelector.length > 0) { // Update the content of the existing .next-available-date element nextAvailableDateSelector.text(nextAvailableDateText); } else { // If the .next-available-date element doesn't exist, create and append it const nextDateText = `

    ${nextAvailableDateText}

    `; $('.djangoAppt_btn-request-next-slot').after(nextDateText); } } }); }