1/** 2 * Availability Core - Pure slot calculation and date logic for datepicker. 3 * Extracted for testability; used by avdp-public.js. 4 * 5 * @package Availability_Datepicker 6 * @since 2.4.0.13 7 */ 8 9(function (global) { 10 'use strict'; 11 12 var AVDPAvailabilityCore = {}; 13 14 /** 15 * Parse a date string flexibly, handling various formats. 16 * 17 * @param {string} dateStr Date string to parse. 18 * @param {string|null} [format] Optional format hint (e.g. 'd/m/Y', 'd-m-Y', 'd.m.Y', 'Y-m-d'). 19 * @returns {Date|null} 20 */ 21 function parseFlexibleDate(dateStr, format) { 22 if (!dateStr) return null; 23 24 var parsedDate; 25 var parts; 26 27 var isDayFirst = format && /^d[\/\-\.]m[\/\-\.]Y/.test(format); 28 29 if (!isDayFirst) { 30 parsedDate = new Date(dateStr); 31 32 if (isNaN(parsedDate.getTime())) { 33 parsedDate = new Date(dateStr.replace(/\//g, '-')); 34 } 35 36 if (!isNaN(parsedDate.getTime())) { 37 return parsedDate; 38 } 39 } 40 41 parts = dateStr.split(/[-\/.]/); 42 if (parts.length >= 3) { 43 if (isDayFirst) { 44 parsedDate = new Date( 45 parseInt(parts[2], 10), 46 parseInt(parts[1], 10) - 1, 47 parseInt(parts[0], 10) 48 ); 49 } else if (parts[0].length === 4) { 50 parsedDate = new Date( 51 parseInt(parts[0], 10), 52 parseInt(parts[1], 10) - 1, 53 parseInt(parts[2], 10) 54 ); 55 } else if (parts[2].length === 4) { 56 parsedDate = new Date( 57 parseInt(parts[2], 10), 58 parseInt(parts[1], 10) - 1, 59 parseInt(parts[0], 10) 60 ); 61 } 62 } 63 64 return parsedDate || new Date(NaN); 65 } 66 67 /** 68 * Find the first available date based on weekly_hours, disabled dates, etc. 69 */ 70 function getFirstAvailableDate(config) { 71 var dayMap = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; 72 var weekly = config.weekly_hours || {}; 73 var disabledDates = config.disabledDates || []; 74 var exemptedDates = config.exemptedDates || []; 75 var noticeMinutes = parseInt(config.minimum_notice, 10) || 0; 76 var bookingType = config.booking_type || 'fixed'; 77 78 var startDate = new Date(); 79 startDate.setTime(startDate.getTime() + (noticeMinutes * 60 * 1000)); 80 81 for (var i = 0; i < 365; i++) { 82 var checkDate = new Date(startDate); 83 checkDate.setDate(startDate.getDate() + i); 84 85 var dayName = dayMap[checkDate.getDay()]; 86 var dateStr = checkDate.getFullYear() + '-' + 87 String(checkDate.getMonth() + 1).padStart(2, '0') + '-' + 88 String(checkDate.getDate()).padStart(2, '0'); 89 90 if (exemptedDates.indexOf(dateStr) !== -1) { 91 return checkDate; 92 } 93 94 if (disabledDates.indexOf(dateStr) !== -1) { 95 continue; 96 } 97 98 if (weekly[dayName]) { 99 var dayConfig = weekly[dayName]; 100 var isEnabled = dayConfig.enabled === true || 101 dayConfig.enabled === 1 || 102 dayConfig.enabled === '1' || 103 dayConfig.enabled === 'true'; 104 105 if (bookingType === 'daily') { 106 if (isEnabled) { 107 return checkDate; 108 } 109 } else { 110 if (isEnabled && dayConfig.slots && dayConfig.slots.length > 0) { 111 // Ensure at least one slot survives the earliest possible booking time 112 // (startDate = now + noticeMinutes). This guards both the case where today's 113 // slots have passed and the case where minimum_notice pushes startDate into a 114 // future day whose slots are also entirely past that shifted start time. 115 var isStartDay = ( 116 checkDate.getFullYear() === startDate.getFullYear() && 117 checkDate.getMonth() === startDate.getMonth() && 118 checkDate.getDate() === startDate.getDate() 119 ); 120 if (isStartDay) { 121 var candidateSlots = calculateSlots(checkDate, config); 122 var remaining = filterTodaySlots(candidateSlots, 0, startDate); 123 if (remaining.length === 0) { 124 continue; 125 } 126 } 127 return checkDate; 128 } 129 } 130 } 131 } 132 133 return new Date(); 134 } 135 136 /** 137 * Calculate slots from a custom time range (for allowed dates override). 138 */ 139 function calculateSlotsFromCustomRange(date, start, end, config) { 140 var allowed = []; 141 var step = parseInt(config.step, 10) || 30; 142 var bufferBefore = parseInt(config.buffer_before, 10) || 0; 143 var bufferAfter = parseInt(config.buffer_after, 10) || 0; 144 var totalCycle = bufferBefore + step + bufferAfter; 145 146 var startParts = start.split(':'); 147 var endParts = end.split(':'); 148 var startMins = (parseInt(startParts[0], 10) * 60) + parseInt(startParts[1], 10); 149 var endMins = (parseInt(endParts[0], 10) * 60) + parseInt(endParts[1], 10); 150 151 var isCross = endMins < startMins; 152 var limit = isCross ? 1440 : endMins; 153 154 for (var slotStart = startMins + bufferBefore; slotStart + step <= limit; slotStart += totalCycle) { 155 var h = Math.floor(slotStart / 60); 156 var m = slotStart % 60; 157 allowed.push( 158 String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0') 159 ); 160 } 161 162 if (isCross) { 163 for (var slotStart = bufferBefore; slotStart + step <= endMins; slotStart += totalCycle) { 164 var h = Math.floor(slotStart / 60); 165 var m = slotStart % 60; 166 var timeStr = String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0'); 167 if (allowed.indexOf(timeStr) === -1) { 168 allowed.push(timeStr); 169 } 170 } 171 } 172 173 allowed.sort(); 174 return allowed; 175 } 176 177 /** 178 * Calculate slots client-side from weekly hours or allowed_dates_with_times 179 */ 180 function calculateSlots(date, config) { 181 var y = date.getFullYear(); 182 var m = String(date.getMonth() + 1).padStart(2, '0'); 183 var d = String(date.getDate()).padStart(2, '0'); 184 var dateStr = y + '-' + m + '-' + d; 185 186 var allowedDatesWithTimes = config.allowed_dates_with_times || []; 187 for (var i = 0; i < allowedDatesWithTimes.length; i++) { 188 var allowedDate = allowedDatesWithTimes[i]; 189 if (allowedDate.date === dateStr && allowedDate.start && allowedDate.end) { 190 return calculateSlotsFromCustomRange(date, allowedDate.start, allowedDate.end, config); 191 } 192 } 193 194 var dayMap = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; 195 var dayName = dayMap[date.getDay()]; 196 var weekly = config.weekly_hours; 197 198 if (!weekly || !weekly[dayName]) { 199 return []; 200 } 201 202 var dayConfig = weekly[dayName]; 203 var isEnabled = dayConfig.enabled === true || 204 dayConfig.enabled === 1 || 205 dayConfig.enabled === '1' || 206 dayConfig.enabled === 'true'; 207 208 if (!isEnabled) { 209 return []; 210 } 211 212 var slotsData = dayConfig.slots; 213 if (!slotsData || slotsData.length === 0) { 214 return []; 215 } 216 217 var allowed = []; 218 var step = parseInt(config.step, 10) || 30; 219 var bufferBefore = parseInt(config.buffer_before, 10) || 0; 220 var bufferAfter = parseInt(config.buffer_after, 10) || 0; 221 var totalCycle = bufferBefore + step + bufferAfter; 222 223 slotsData.forEach(function (slotRange) { 224 var startParts = slotRange.start.split(':'); 225 var endParts = slotRange.end.split(':'); 226 227 var startMins = (parseInt(startParts[0], 10) * 60) + parseInt(startParts[1], 10); 228 var endMins = (parseInt(endParts[0], 10) * 60) + parseInt(endParts[1], 10); 229 230 var isCross = endMins < startMins; 231 var limit = isCross ? 1440 : endMins; 232 233 for (var slotStart = startMins + bufferBefore; slotStart + step <= limit; slotStart += totalCycle) { 234 var h = Math.floor(slotStart / 60); 235 var m = slotStart % 60; 236 allowed.push( 237 String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0') 238 ); 239 } 240 }); 241 242 var yestDate = new Date(date); 243 yestDate.setDate(date.getDate() - 1); 244 var yestDayName = dayMap[yestDate.getDay()]; 245 246 if (weekly[yestDayName] && weekly[yestDayName].enabled && weekly[yestDayName].slots) {
247 weekly[yestDayName].slots.forEach(function (slotRange) { 248 var startParts = slotRange.start.split(':'); 249 var endParts = slotRange.end.split(':'); 250 var startMins = (parseInt(startParts[0], 10) * 60) + parseInt(startParts[1], 10); 251 var endMins = (parseInt(endParts[0], 10) * 60) + parseInt(endParts[1], 10); 252 253 if (endMins < startMins) { 254 for (var slotStart = bufferBefore; slotStart + step <= endMins; slotStart += totalCycle) { 255 var h = Math.floor(slotStart / 60); 256 var m = slotStart % 60; 257 var timeStr = String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0'); 258 if (allowed.indexOf(timeStr) === -1) { 259 allowed.push(timeStr); 260 } 261 } 262 } 263 }); 264 } 265 266 allowed.sort(); 267 268 return allowed; 269 } 270 271 /** 272 * Check if a start date can begin a booking that spans min_days consecutive 273 * calendar days that are all available. 274 * 275 * A disabled weekday inside the window breaks the consecutive run and returns 276 * false immediately â matching the user expectation that a 5-night stay cannot 277 * start on Wednesday when the property is closed on weekends. 278 * An exempted date overrides both disabled-weekday and blocked-date rules and 279 * counts as an available day in the sequence. 280 */ 281 function canStartSequentialBooking(startDate, minDays, config) { 282 if (!minDays || minDays <= 0) { 283 return true; 284 } 285 286 var disabledWeekDays = (config.disabledWeekDays || []).map(function (d) { 287 return parseInt(d, 10); 288 }); 289 var blockedDates = config.disabledDates || []; 290 var exemptedDates = config.exemptedDates || []; 291 292 var current = new Date(startDate); 293 current.setHours(0, 0, 0, 0); 294 295 for (var i = 0; i < minDays; i++) { 296 var y = current.getFullYear(); 297 var m = String(current.getMonth() + 1).padStart(2, '0'); 298 var d = String(current.getDate()).padStart(2, '0'); 299 var dateStr = y + '-' + m + '-' + d; 300 var dayOfWeek = current.getDay(); 301 302 // Exempted dates are always available â they override every other rule. 303 if (exemptedDates.indexOf(dateStr) !== -1) { 304 current.setDate(current.getDate() + 1); 305 continue; 306 } 307 308 // A disabled weekday in the middle of the required span breaks the sequence. 309 if (disabledWeekDays.indexOf(dayOfWeek) !== -1) { 310 return false; 311 } 312 313 // An explicitly blocked date breaks the sequence. 314 if (blockedDates.indexOf(dateStr) !== -1) { 315 return false; 316 } 317 318 current.setDate(current.getDate() + 1); 319 } 320 321 return true; 322 } 323 324 /** 325 * Validate that the date range has a continuous sequence of available working days 326 */ 327 function validateSequence(startDate, endDate, config) { 328 var disabledWeekDays = (config.disabledWeekDays || []).map(function (d) { 329 return parseInt(d, 10); 330 }); 331 var blockedDates = config.disabledDates || []; 332 var exemptedDates = config.exemptedDates || []; 333 334 var current = new Date(startDate); 335 current.setHours(0, 0, 0, 0); 336 var end = new Date(endDate); 337 end.setHours(0, 0, 0, 0); 338 339 while (current <= end) { 340 var y = current.getFullYear(); 341 var m = String(current.getMonth() + 1).padStart(2, '0'); 342 var d = String(current.getDate()).padStart(2, '0'); 343 var dateStr = y + '-' + m + '-' + d; 344 var dayOfWeek = current.getDay(); 345 346 if (disabledWeekDays.indexOf(dayOfWeek) !== -1) { 347 current.setDate(current.getDate() + 1); 348 continue; 349 } 350 351 if (exemptedDates.indexOf(dateStr) !== -1) { 352 current.setDate(current.getDate() + 1); 353 continue; 354 } 355 356 if (blockedDates.indexOf(dateStr) !== -1) { 357 return { 358 valid: false, 359 blockedDate: dateStr 360 }; 361 } 362 363 current.setDate(current.getDate() + 1); 364 } 365 366 return { valid: true, blockedDate: null }; 367 } 368 369 /** 370 * Count available days between two dates (inclusive), excluding unavailable days. 371 * 372 * Exempted dates override both disabled-weekday and blocked-date rules and are 373 * counted as available. Used by avdp-public.js validateSelection to check 374 * min/max days against only the selectable days in a range. 375 * 376 * Pure function â no side effects, no DOM access. 377 * 378 * @param {Date} startDate 379 * @param {Date} endDate 380 * @param {Object} config Requires disabledDates, disabledWeekDays, exemptedDates. 381 * @returns {number} Number of available days. 382 */ 383 function countAvailableDays(startDate, endDate, config) { 384 var disabledDates = config.disabledDates || []; 385 var disabledWeekDays = (config.disabledWeekDays || []).map(function (d) { 386 return parseInt(d, 10); 387 }); 388 var exemptedDates = config.exemptedDates || []; 389 390 var count = 0; 391 var current = new Date(startDate); 392 current.setHours(0, 0, 0, 0); 393 var end = new Date(endDate); 394 end.setHours(0, 0, 0, 0); 395 396 while (current <= end) { 397 var y = current.getFullYear(); 398 var m = String(current.getMonth() + 1).padStart(2, '0'); 399 var d = String(current.getDate()).padStart(2, '0'); 400 var dateStr = y + '-' + m + '-' + d; 401 var dayOfWeek = current.getDay(); 402 403 var isAvailable; 404 if (exemptedDates.indexOf(dateStr) !== -1) { 405 isAvailable = true; 406 } else if (disabledDates.indexOf(dateStr) !== -1) { 407 isAvailable = false;
408 } else if (disabledWeekDays.indexOf(dayOfWeek) !== -1) { 409 isAvailable = false; 410 } else { 411 isAvailable = true; 412 } 413 414 if (isAvailable) { 415 count++; 416 } 417 418 current.setDate(current.getDate() + 1); 419 } 420 421 return count; 422 } 423 424 /** 425 * Filter time slots for today, removing any that fall before (now + noticeMinutes). 426 * 427 * Pure function â no side effects, no DOM access. 428 * Extracted here for testability; called by avdp-public.js updateAllowTimes. 429 * 430 * @param {string[]} slots Array of 'HH:MM' strings. 431 * @param {number} noticeMinutes Minimum notice in minutes (0 = filter past only). 432 * @param {Date} now Current time (defaults to new Date()). 433 * @returns {string[]} Filtered slots. 434 */ 435 function filterTodaySlots(slots, noticeMinutes, now) { 436 if (!slots || slots.length === 0) return slots; 437 var reference = now instanceof Date ? now : new Date(); 438 var minAvailableTime = new Date(reference.getTime() + ((noticeMinutes || 0) * 60 * 1000)); 439 440 // If now + notice crosses midnight, every slot for today is already past. 441 if (minAvailableTime.getDate() !== reference.getDate() || 442 minAvailableTime.getMonth() !== reference.getMonth() || 443 minAvailableTime.getFullYear() !== reference.getFullYear()) { 444 return []; 445 } 446 447 var minMins = (minAvailableTime.getHours() * 60) + minAvailableTime.getMinutes(); 448 449 return slots.filter(function (timeStr) { 450 var parts = timeStr.split(':'); 451 var slotMins = (parseInt(parts[0], 10) * 60) + parseInt(parts[1], 10); 452 return slotMins >= minMins; 453 }); 454 } 455 456 AVDPAvailabilityCore.parseFlexibleDate = parseFlexibleDate; 457 AVDPAvailabilityCore.getFirstAvailableDate = getFirstAvailableDate; 458 AVDPAvailabilityCore.calculateSlots = calculateSlots; 459 AVDPAvailabilityCore.calculateSlotsFromCustomRange = calculateSlotsFromCustomRange; 460 AVDPAvailabilityCore.canStartSequentialBooking = canStartSequentialBooking; 461 AVDPAvailabilityCore.validateSequence = validateSequence; 462 AVDPAvailabilityCore.countAvailableDays = countAvailableDays; 463 AVDPAvailabilityCore.filterTodaySlots = filterTodaySlots; 464 465 if (typeof module !== 'undefined' && module.exports) { 466 module.exports = AVDPAvailabilityCore; 467 } else { 468 global.AVDPAvailabilityCore = AVDPAvailabilityCore; 469 } 470 471})(typeof window !== 'undefined' ? window : this);
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.