1/** 2 * MeetFlow â modern booking wizard (month calendar, slots, provider choice). 3 */ 4(function($) { 5 'use strict'; 6 7 var strings = (typeof meetflowBooking !== 'undefined' && meetflowBooking.strings) ? meetflowBooking.strings : {}; 8 var locale = (meetflowBooking && meetflowBooking.locale) ? meetflowBooking.locale : 'en-US'; 9 10 function escHtml(value) { 11 return $('<div>').text(value == null ? '' : String(value)).html(); 12 } 13 14 function formatDate(date) { 15 var y = date.getFullYear(); 16 var m = String(date.getMonth() + 1).padStart(2, '0'); 17 var d = String(date.getDate()).padStart(2, '0'); 18 return y + '-' + m + '-' + d; 19 } 20 21 function parseDate(str) { 22 var p = String(str).split('-'); 23 return new Date(parseInt(p[0], 10), parseInt(p[1], 10) - 1, parseInt(p[2], 10)); 24 } 25 26 function formatDisplayDate(str) { 27 try { 28 return new Intl.DateTimeFormat(locale, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }).format(parseDate(str)); 29 } catch (e) { 30 return str; 31 } 32 } 33 34 var MeetFlowBooking = { 35 selectedDate: null, 36 selectedTime: null, 37 selectedProvider: null, 38 viewMonth: null, 39 minDate: null, 40 maxDate: null, 41 42 init: function() { 43 var today = new Date(); 44 today.setHours(0, 0, 0, 0); 45 this.minDate = new Date(today); 46 this.maxDate = new Date(today); 47 this.maxDate.setDate(this.maxDate.getDate() + 60); 48 this.viewMonth = new Date(today.getFullYear(), today.getMonth(), 1); 49 50 var tomorrow = new Date(today); 51 tomorrow.setDate(tomorrow.getDate() + 1); 52 this.selectedDate = formatDate(tomorrow); 53 $('#kitech-datepicker').val(this.selectedDate); 54 55 this.renderMonthCalendar(); 56 this.initProviders(); 57 this.bindEvents(); 58 this.goToStep('date'); 59 this.updateResetButton(); 60 }, 61 62 initProviders: function() { 63 var cfg = (meetflowBooking && meetflowBooking.meetingProviders) ? meetflowBooking.meetingProviders : null; 64 if (!cfg || !cfg.allow_choice || !cfg.providers || cfg.providers.length < 2) { 65 $('#meetflow-provider-picker').prop('hidden', true); 66 if (cfg && cfg.providers && cfg.providers.length === 1) { 67 this.selectedProvider = cfg.providers[0].id; 68 } 69 return; 70 } 71 72 var html = ''; 73 cfg.providers.forEach(function(p, idx) { 74 var checked = (cfg.default === p.id || idx === 0) ? ' checked' : ''; 75 if (checked) { 76 MeetFlowBooking.selectedProvider = p.id; 77 } 78 html += '<label class="meetflow-provider-option" style="--mf-provider-accent:' + escHtml(p.accent) + '">' + 79 '<input type="radio" name="meetflow_conference_provider" value="' + escHtml(p.id) + '"' + checked + '>' + 80 '<span class="meetflow-provider-option__label">' + escHtml(p.label) + '</span>' + 81 '</label>'; 82 }); 83 84 $('.meetflow-provider-options').html(html); 85 $('#meetflow-provider-picker').prop('hidden', false); 86 }, 87 88 bindEvents: function() { 89 var self = this; 90 91 $(document).on('click', '.mf-cal-prev', function(e) { 92 e.preventDefault(); 93 self.viewMonth.setMonth(self.viewMonth.getMonth() - 1); 94 self.renderMonthCalendar(); 95 }); 96 97 $(document).on('click', '.mf-cal-next', function(e) { 98 e.preventDefault(); 99 self.viewMonth.setMonth(self.viewMonth.getMonth() + 1); 100 self.renderMonthCalendar(); 101 }); 102 103 $(document).on('click', '.mf-cal-day:not(.is-disabled):not(.is-empty)', function() { 104 var date = $(this).data('date'); 105 if (!date) return; 106 self.selectedDate = date; 107 $('#kitech-datepicker').val(date); 108 $('.mf-cal-day').removeClass('is-selected'); 109 $(this).addClass('is-selected'); 110 self.loadTimeslots(date); 111 self.goToStep('time'); 112 }); 113 114 $(document).on('click', '.kitech-timeslot', function() { 115 if ($(this).hasClass('booked') || $(this).hasClass('blocked')) { 116 return false;
117 } 118 $('.kitech-timeslot').removeClass('selected'); 119 $(this).addClass('selected'); 120 self.selectedTime = $(this).data('time'); 121 self.goToStep('details'); 122 }); 123 124 $(document).on('change', 'input[name="meetflow_conference_provider"]', function() { 125 self.selectedProvider = $(this).val(); 126 }); 127 128 $('#meetflow-form').on('submit', function(e) { 129 e.preventDefault(); 130 self.submitBooking(); 131 }); 132 133 $('#meetflow-reset-btn').on('click', function(e) { 134 e.preventDefault(); 135 self.resetBooking(); 136 }); 137 }, 138 139 updateResetButton: function() { 140 var step = $('.meetflow-panel.is-active').data('panel'); 141 $('#meetflow-reset-btn').prop('hidden', step === 'date'); 142 }, 143 144 resetBooking: function() { 145 this.selectedTime = null; 146 this.selectedProvider = null; 147 148 var today = new Date(); 149 today.setHours(0, 0, 0, 0); 150 var tomorrow = new Date(today); 151 tomorrow.setDate(tomorrow.getDate() + 1); 152 this.selectedDate = formatDate(tomorrow); 153 this.viewMonth = new Date(tomorrow.getFullYear(), tomorrow.getMonth(), 1); 154 $('#kitech-datepicker').val(this.selectedDate); 155 156 $('#meetflow-form')[0].reset(); 157 if (meetflowBooking && meetflowBooking.defaultPhonePrefix) { 158 $('#kitech-phone-prefix').val(meetflowBooking.defaultPhonePrefix); 159 } 160 $('.kitech-message').remove(); 161 $('.kitech-timeslot').removeClass('selected'); 162 $('.kitech-timeslots').html('<p class="kitech-loading">' + escHtml(strings.loading || 'Loading available times...') + '</p>'); 163 164 this.initProviders(); 165 this.renderMonthCalendar(); 166 this.goToStep('date'); 167 this.updateResetButton(); 168 }, 169 170 getFullPhone: function() { 171 var prefix = ($('#kitech-phone-prefix').val() || '').trim(); 172 var local = ($('#kitech-phone').val() || '').trim(); 173 if (!prefix && local.indexOf('+') === 0) { 174 return local.replace(/\s+/g, ''); 175 } 176 var digits = local.replace(/[^\d]/g, '').replace(/^0+/, ''); 177 return prefix + digits; 178 }, 179 180 renderMonthCalendar: function() { 181 var self = this; 182 var month = this.viewMonth.getMonth(); 183 var year = this.viewMonth.getFullYear(); 184 var first = new Date(year, month, 1); 185 var startDay = (first.getDay() + 6) % 7; 186 var daysInMonth = new Date(year, month + 1, 0).getDate(); 187 188 var monthLabel; 189 try { 190 monthLabel = new Intl.DateTimeFormat(locale, { month: 'long', year: 'numeric' }).format(first); 191 } catch (e) { 192 monthLabel = (month + 1) + '/' + year; 193 } 194 $('#mf-cal-month-label').text(monthLabel); 195 196 var weekdays = []; 197 var base = new Date(2024, 0, 1); 198 for (var w = 0; w < 7; w++) { 199 try { 200 weekdays.push(new Intl.DateTimeFormat(locale, { weekday: 'short' }).format(new Date(2024, 0, 1 + w))); 201 } catch (e2) { 202 weekdays.push(['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'][w]); 203 } 204 } 205 $('.mf-cal-weekdays').html(weekdays.map(function(d) { 206 return '<span class="mf-cal-weekday">' + escHtml(d) + '</span>'; 207 }).join('')); 208 209 var html = ''; 210 var cell = 0; 211 for (var i = 0; i < startDay; i++) { 212 html += '<span class="mf-cal-day is-empty" role="gridcell"></span>'; 213 cell++; 214 } 215 216 for (var day = 1; day <= daysInMonth; day++) { 217 var dateObj = new Date(year, month, day); 218 var iso = formatDate(dateObj);
219 var classes = 'mf-cal-day'; 220 var disabled = dateObj < self.minDate || dateObj > self.maxDate; 221 if (disabled) classes += ' is-disabled'; 222 if (iso === self.selectedDate) classes += ' is-selected'; 223 if (iso === formatDate(new Date())) classes += ' is-today'; 224 html += '<button type="button" class="' + classes + '" data-date="' + escHtml(iso) + '" role="gridcell"' + (disabled ? ' disabled' : '') + '>' + day + '</button>'; 225 cell++; 226 } 227 228 while (cell % 7 !== 0) { 229 html += '<span class="mf-cal-day is-empty" role="gridcell"></span>'; 230 cell++; 231 } 232 233 $('#mf-cal-grid').html(html); 234 }, 235 236 goToStep: function(step) { 237 $('.meetflow-step').removeClass('is-active is-done'); 238 $('.meetflow-panel').prop('hidden', true).removeClass('is-active'); 239 240 var order = ['date', 'time', 'details']; 241 order.forEach(function(s, idx) { 242 var $step = $('.meetflow-step[data-step="' + s + '"]'); 243 if (s === step) { 244 $step.addClass('is-active'); 245 } else if (order.indexOf(step) > idx) { 246 $step.addClass('is-done'); 247 } 248 }); 249 250 $('.meetflow-panel[data-panel="' + step + '"]').prop('hidden', false).addClass('is-active'); 251 252 if (step === 'time' && this.selectedDate) { 253 $('#meetflow-selected-date').text(formatDisplayDate(this.selectedDate)); 254 } 255 256 this.updateResetButton(); 257 }, 258 259 loadTimeslots: function(date) { 260 var self = this; 261 this.selectedDate = date; 262 $('.kitech-timeslots').html('<div class="kitech-loading"><div class="kitech-spinner"></div><p>' + escHtml(strings.loading || 'Loading available times...') + '</p></div>'); 263 264 $.ajax({ 265 url: meetflowBooking.restUrl + 'timeslots', 266 method: 'GET', 267 data: { date: date }, 268 contentType: 'application/json; charset=UTF-8', 269 dataType: 'json', 270 beforeSend: function(xhr) { 271 xhr.setRequestHeader('X-WP-Nonce', meetflowBooking.restNonce); 272 }, 273 success: function(response) { 274 self.renderTimeslots(response); 275 }, 276 error: function() { 277 $('.kitech-timeslots').html('<div class="kitech-message error">' + escHtml(strings.errorLoading || 'Error loading time slots.') + '</div>'); 278 } 279 }); 280 }, 281 282 renderTimeslots: function(slots) { 283 var html = ''; 284 if (!slots || slots.length === 0) { 285 html = '<div class="kitech-message info">' + escHtml(strings.noSlots || 'No available time slots') + '</div>'; 286 } else { 287 var availableCount = 0; 288 slots.forEach(function(slot) { 289 var classes = 'kitech-timeslot'; 290 var title = ''; 291 if (!slot.available) { 292 classes += ' booked blocked'; 293 title = ' title="' + escHtml(strings.alreadyBooked || 'Already booked') + '"'; 294 } else { 295 availableCount++; 296 } 297 var selected = (slot.time === MeetFlowBooking.selectedTime) ? ' selected' : ''; 298 html += '<div class="' + classes + selected + '" data-time="' + escHtml(slot.time) + '"' + title + '>' + escHtml(slot.display) + '</div>'; 299 }); 300 if (availableCount === 0) { 301 html = '<div class="kitech-message warning">' + escHtml(strings.allBooked || 'All time slots booked. Please choose another date.') + '</div>' + html; 302 } 303 } 304 $('.kitech-timeslots').html(html); 305 }, 306 307 submitBooking: function() { 308 var self = this; 309 if (!this.validateForm()) return; 310 311 var formData = { 312 date: this.selectedDate, 313 time: this.selectedTime, 314 name: $('#kitech-name').val(), 315 email: $('#kitech-email').val(), 316 phone: this.getFullPhone(), 317 notes: $('#kitech-notes').val() 318 }; 319 320 if (this.selectedProvider) { 321 formData.conference_provider = this.selectedProvider; 322 } 323 324 var $submitBtn = $('#kitech-submit-btn'); 325 $submitBtn.prop('disabled', true).addClass('loading').text(strings.booking || 'Booking...'); 326 $('.kitech-message').remove(); 327 328 $.ajax({ 329 url: meetflowBooking.restUrl + 'booking', 330 method: 'POST', 331 contentType: 'application/json; charset=UTF-8', 332 dataType: 'json', 333 headers: { 334 'Content-Type': 'application/json; charset=UTF-8', 335 'X-WP-Nonce': meetflowBooking.restNonce 336 }, 337 data: JSON.stringify(formData), 338 success: function(response) { 339 self.showConfirmation(response); 340 }, 341 error: function(xhr) { 342 var message = strings.errorCreating || 'Error creating booking'; 343 if (xhr.responseJSON && xhr.responseJSON.message) { 344 message = xhr.responseJSON.message; 345 } else if (xhr.status === 409) { 346 message = strings.slotTaken || 'Slot just booked. Please choose another.'; 347 self.goToStep('time'); 348 self.loadTimeslots(self.selectedDate); 349 } 350 $('.meetflow-form').prepend('<div class="kitech-message error">' + escHtml(message) + '</div>'); 351 $submitBtn.prop('disabled', false).removeClass('loading').text(strings.bookAppointment || 'Book appointment'); 352 } 353 }); 354 }, 355 356 validateForm: function() { 357 var errors = []; 358 if (!this.selectedDate) errors.push(strings.selectDate || 'Please select a date'); 359 if (!this.selectedTime) errors.push(strings.selectTime || 'Please select a time'); 360 361 var cfg = meetflowBooking.meetingProviders; 362 if (cfg && cfg.allow_choice && cfg.providers && cfg.providers.length > 1 && !this.selectedProvider) { 363 errors.push(strings.platformRequired || 'Please choose a meeting platform'); 364 } 365 366 var name = $('#kitech-name').val(); 367 if (!name || name.trim().length < 2) errors.push(strings.enterName || 'Please enter your name'); 368 369 var email = $('#kitech-email').val(); 370 if (!email) { 371 errors.push(strings.emailRequired || 'Email required'); 372 } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
373 errors.push(strings.validEmail || 'Enter a valid email'); 374 } 375 376 if (!$('#kitech-phone').val().trim()) { 377 errors.push(strings.phoneRequired || 'Phone required'); 378 } else { 379 var fullPhone = this.getFullPhone(); 380 if (!/^\+[1-9]\d{7,14}$/.test(fullPhone)) { 381 errors.push(strings.phoneInvalid || 'Please enter a valid phone number'); 382 } 383 } 384 if (!$('#kitech-dsgvo').is(':checked')) errors.push(strings.acceptPrivacy || 'Accept privacy policy'); 385 386 if (errors.length) { 387 $('.kitech-message').remove(); 388 $('.meetflow-form').prepend('<div class="kitech-message error">' + errors.map(escHtml).join('<br>') + '</div>'); 389 return false; 390 } 391 return true; 392 }, 393 394 showConfirmation: function(data) { 395 var html = '<div class="kitech-confirmation">' + 396 '<div class="kitech-confirmation-icon"></div>' + 397 '<h3>' + escHtml(strings.bookingSuccess || 'Booking successful!') + '</h3>' + 398 '<p>' + escHtml(strings.confirmationEmail || 'You will receive a confirmation email.') + '</p>' + 399 '<p><strong>' + escHtml(strings.bookingIdLabel || 'Booking ID:') + '</strong> ' + escHtml(data.booking_id) + '</p>'; 400 401 if (data.meeting_url) { 402 html += '<p><a href="' + escHtml(data.meeting_url) + '" target="_blank" rel="noopener noreferrer" class="button meetflow-meeting-link kitech-meeting-link">' + escHtml(strings.joinMeeting || 'Join meeting') + '</a></p>'; 403 } 404 405 if (data.calendar && (data.calendar.google || data.calendar.outlook || data.calendar.ics_url)) { 406 html += '<div class="meetflow-calendar-actions"><p class="meetflow-calendar-actions__title">' + escHtml(strings.addToCalendar || 'Add to calendar') + '</p><div class="meetflow-calendar-actions__buttons">'; 407 if (data.calendar.google) { 408 html += '<a href="' + escHtml(data.calendar.google) + '" target="_blank" rel="noopener noreferrer" class="button meetflow-cal-btn meetflow-cal-btn--google">' + escHtml(strings.googleCalendar || 'Google Calendar') + '</a>'; 409 } 410 if (data.calendar.outlook) { 411 html += '<a href="' + escHtml(data.calendar.outlook) + '" target="_blank" rel="noopener noreferrer" class="button meetflow-cal-btn meetflow-cal-btn--outlook">' + escHtml(strings.outlookCalendar || 'Outlook') + '</a>'; 412 } 413 if (data.calendar.ics_url) { 414 html += '<a href="' + escHtml(data.calendar.ics_url) + '" class="button meetflow-cal-btn meetflow-cal-btn--ics">' + escHtml(strings.downloadIcs || 'Download .ics') + '</a>'; 415 } 416 html += '</div><p class="meetflow-calendar-actions__hint">' + escHtml(strings.icsHint || '') + '</p></div>'; 417 } 418 419 html += '<p class="kitech-confirmation-actions"><button type="button" class="button meetflow-submit-btn kitech-submit-btn kitech-book-again">' + escHtml(strings.anotherBooking || 'Another booking') + '</button></p></div>'; 420 421 $('.meetflow-calendar').html(html); 422 } 423 }; 424 425 $(document).ready(function() { 426 if ($('.meetflow-calendar[data-meetflow-wizard]').length) { 427 MeetFlowBooking.init(); 428 } 429 $(document).on('click', '.kitech-book-again', function() { 430 window.location.reload(); 431 }); 432 }); 433 434 window.MeetFlowBooking = MeetFlowBooking; 435})(jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.