1(function ($) { 2function initDatePicker() { 3 $(".event-date").each(function () { 4 5 if ($(this).hasClass("hasDatepicker")) { 6 $(this).datepicker("destroy"); 7 } 8 9 $(this).datepicker({ 10 dateFormat: "mm/dd/yy", 11 changeMonth: true, 12 changeYear: true 13 }); 14 15 }); 16} 17document.addEventListener("DOMContentLoaded", function () { 18 19 // Disable past dates 20 let today = new Date().toISOString().split('T')[0]; 21 document.querySelectorAll(".event-date").forEach(function (input) { 22 input.setAttribute("min", today); 23 }); 24 25}); 26 27let blockedSlots = []; 28 29function loadBlockedSlots() { 30 $.post(erm_ajax.ajax_url, { 31 action: 'erm_get_blocked_slots' 32 }, function(resp) { 33 if(resp.success){ 34 blockedSlots = resp.data; 35 } 36 }); 37} 38 39loadBlockedSlots(); 40 41 42$(document).ready(function () { 43 44 45 initDatePicker(); 46 /* ------------------------------- 47 ADD / REMOVE EVENT ROW 48 ---------------------------------*/ 49 50 $('#erm_add_row').on('click', function () { 51 52 if ($('.erm-event-wrapper').length >= 2) { 53 alert('Only one alternate date allowed'); 54 return; 55 } 56 57 var wrapper = $('.erm-event-wrapper:first').clone(); 58 59 // Clear all values 60 wrapper.find('input').val(''); 61 62 // Reset datepicker 63 wrapper.find('.event-date') 64 .removeClass('hasDatepicker') 65 .removeAttr('id'); 66 67 // Change label 68 wrapper.find('.erm-row-label').text('Alternate Date'); 69 70 // Show remove button ONLY on alternate row 71 wrapper.find('.erm-remove-column').show(); 72 73 // Add alternate row 74 $('#erm_event_rows').append(wrapper); 75 76 // Reinitialize datepicker 77 initDatePicker(); 78 }); 79 80 81 82 $(document).on('click', '.erm-remove-row', function () { 83 84 if ($('.erm-event-wrapper').length > 1) { 85 $(this).closest('.erm-event-wrapper').remove(); 86 } 87 88 }); 89 90 /* -------------------------------- 91 TIME VALIDATION HELPERS 92--------------------------------- */ 93 94function isValidTime(time) { 95 96 if (!/^\d{2}:\d{2}$/.test(time)) { 97 return false; 98 } 99 100 var parts = time.split(':'); 101 102 var hours = parseInt(parts[0], 10); 103 var minutes = parseInt(parts[1], 10); 104 105 return ( 106 hours >= 0 && 107 hours <= 23 && 108 minutes >= 0 && 109 minutes <= 59 110 ); 111} 112 113function timeToMinutes(time) { 114 115 if (!isValidTime(time)) { 116 return null; 117 } 118 119 var parts = time.split(':'); 120 121 return ( 122 parseInt(parts[0], 10) * 60 + 123 parseInt(parts[1], 10) 124 ); 125} 126 127 128/* -------------------------------- 129 TIME INPUT 130 HH:MM 131--------------------------------- */ 132 133$(document).on('input', '.event-time', function () { 134 135 var input = this; 136 137 // Numbers only 138 var value = input.value.replace(/\D/g, ''); 139 140 // Maximum 4 digits 141 value = value.substring(0, 4); 142 143 // Add colon automatically 144 if (value.length >= 3) { 145 value = value.substring(0, 2) + ':' + value.substring(2); 146 } 147 148 input.value = value; 149}); 150 151 152/* -------------------------------- 153 VALIDATE TIME ON BLUR 154--------------------------------- */ 155 156$(document).on('blur', '.event-time', function () { 157 158 var input = this; 159 var value = input.value.trim(); 160 161 if (!value) { 162 return; 163 } 164 165 if (!isValidTime(value)) { 166 167 alert('Please enter a valid time between 00:00 and 23:59.'); 168 169 input.value = ''; 170 input.focus(); 171 172 return; 173 } 174 175 var row = $(input).closest('.erm-event-row'); 176 177 var start = row 178 .find('input[name="event_start[]"]') 179 .val(); 180 181 var end = row 182 .find('input[name="event_end[]"]') 183 .val(); 184 185 /* 186 * If both times are valid, 187 * make sure end is after start. 188 */ 189 if (isValidTime(start) && isValidTime(end)) { 190 191 if (timeToMinutes(end) <= timeToMinutes(start)) { 192 193 alert('End time must be after start time.'); 194 195 row.find('input[name="event_duration[]"]').val(''); 196 197 /* 198 * Clear only the end time if the
199 * user has entered an invalid range. 200 */ 201 if ($(input).attr('name') === 'event_end[]') { 202 input.value = ''; 203 input.focus(); 204 } 205 206 return; 207 } 208 } 209}); 210 211 212/* -------------------------------- 213 AUTO CALCULATE DURATION 214--------------------------------- */ 215 216$(document).on( 217 'input change blur', 218 'input[name="event_start[]"], input[name="event_end[]"]', 219 function () { 220 221 var row = $(this).closest('.erm-event-row'); 222 223 var start = row 224 .find('input[name="event_start[]"]') 225 .val(); 226 227 var end = row 228 .find('input[name="event_end[]"]') 229 .val(); 230 231 var startMinutes = timeToMinutes(start); 232 var endMinutes = timeToMinutes(end); 233 234 /* 235 * Don't calculate until both 236 * values are valid. 237 */ 238 if (startMinutes === null || endMinutes === null) { 239 240 row.find('input[name="event_duration[]"]').val(''); 241 242 return; 243 } 244 245 /* 246 * End must be later than start. 247 */ 248 if (endMinutes <= startMinutes) { 249 250 row.find('input[name="event_duration[]"]').val(''); 251 252 return; 253 } 254 255 var diff = endMinutes - startMinutes; 256 257 var hours = Math.floor(diff / 60); 258 var minutes = diff % 60; 259 260 var durationText = ''; 261 262 if (hours > 0) { 263 durationText += hours + 'h '; 264 } 265 266 if (minutes > 0) { 267 durationText += minutes + 'm'; 268 } 269 270 row.find('input[name="event_duration[]"]') 271 .val(durationText.trim()); 272}); 273 274 275 276 /* ------------------------------- 277 BLOCKED TIME VALIDATION 278 ---------------------------------*/ 279 280 // Convert mm/dd/yyyy â Date object safely 281function createDate(dateStr, timeStr) { 282 283 var parts = dateStr.split("/"); // mm/dd/yyyy 284 285 var month = parseInt(parts[0], 10) - 1; // JS month is 0-indexed 286 var day = parseInt(parts[1], 10); 287 var year = parseInt(parts[2], 10); 288 289 var timeParts = timeStr.split(":"); 290 var hours = parseInt(timeParts[0], 10); 291 var minutes = parseInt(timeParts[1], 10); 292 293 return new Date(year, month, day, hours, minutes, 0); 294} 295 296 297 298$(document).on('change', 'input[name="event_date[]"], input[name="event_start[]"], input[name="event_end[]"]', function () { 299 300 var row = $(this).closest('.erm-event-row'); 301 302 var selectedDate = row.find('input[name="event_date[]"]').val(); 303 var startTime = row.find('input[name="event_start[]"]').val(); 304 var endTime = row.find('input[name="event_end[]"]').val(); 305 306 if (!selectedDate || !startTime || !endTime) return; 307 308 var userStart = createDate(selectedDate, startTime); 309 var userEnd = createDate(selectedDate, endTime); 310 311 // Handle overnight user booking 312 if (userEnd <= userStart) { 313 userEnd.setDate(userEnd.getDate() + 1); 314 } 315 316 var isBlocked = false; 317 var reason = ''; 318 319 for (var i = 0; i < blockedSlots.length; i++) { 320 321 var slot = blockedSlots[i]; 322 323 // Convert blocked slot date (yyyy-mm-dd) properly 324 var blockDateParts = slot.date.split("-"); 325 var blockYear = parseInt(blockDateParts[0], 10); 326 var blockMonth = parseInt(blockDateParts[1], 10) - 1; 327 var blockDay = parseInt(blockDateParts[2], 10); 328 329 var fromParts = slot.from_time.split(":"); 330 var toParts = slot.to_time.split(":"); 331 332 var blockStart = new Date( 333 blockYear, 334 blockMonth, 335 blockDay, 336 parseInt(fromParts[0], 10), 337 parseInt(fromParts[1], 10) 338 ); 339 340 var blockEnd = new Date( 341 blockYear, 342 blockMonth, 343 blockDay, 344 parseInt(toParts[0], 10), 345 parseInt(toParts[1], 10) 346 ); 347 348 // Handle overnight block 349 if (blockEnd <= blockStart) { 350 blockEnd.setDate(blockEnd.getDate() + 1); 351 } 352 353 // Check overlap 354 if (userStart < blockEnd && userEnd > blockStart) { 355 isBlocked = true; 356 reason = slot.reason || "This time slot is unavailable."; 357 break; // stop loop immediately 358 } 359 } 360 361 if (isBlocked) { 362 363 alert("The chosen time conflicts with a blocked schedule. Please select another available time slot to complete your reservation.\nReason: " + reason); 364 365 row.find('input[name="event_start[]"]').val(''); 366 row.find('input[name="event_end[]"]').val(''); 367 row.find('input[name="event_duration[]"]').val(''); 368 } 369 370}); 371 372 /* ------------------------------- 373 FINAL FORM SUBMIT 374 ---------------------------------*/ 375 376 $('#ermForm').on('submit', function (e) { 377 378 e.preventDefault(); 379 380 var form = this; 381 382 // Trigger HTML5 validation 383 if (!form.checkValidity()) { 384 form.classList.add('was-validated'); 385 return; 386 } 387 388 // Extra custom validation for event rows 389 var isValid = true; 390 391 $('.erm-event-row').each(function () { 392 393 var date = $(this).find('input[name="event_date[]"]').val(); 394 var start = $(this).find('input[name="event_start[]"]').val(); 395 var end = $(this).find('input[name="event_end[]"]').val(); 396 397 if (!date || !start || !end) { 398 isValid = false;
399 alert('Please fill all event date and time fields.'); 400 return false; 401 } 402 403 if (!isValidTime(start) || !isValidTime(end)) { 404 isValid = false; 405 alert('Please enter time in HH:MM format, for example 09:00 or 17:30.'); 406 return false; 407 } 408 409 if (timeToMinutes(end) <= timeToMinutes(start)) { 410 isValid = false; 411 alert('End time must be after start time.'); 412 return false; 413 } 414 415 }); 416 417 if (!isValid) return; 418 419 var data = $(this).serializeArray(); 420 data.push({ name: 'erm_nonce', value: erm_ajax.nonce }); 421 data.push({ name: 'action', value: 'erm_submit' }); 422 423 $('#erm_response').html('<div class="alert alert-info">Submittingâ¦</div>'); 424 425 $.ajax({ 426 type: "POST", 427 url: erm_ajax.ajax_url, 428 data: data, 429 dataType: "json", 430 success: function (resp) { 431 432 if (resp.success) { 433 434 $('#erm_response').html('<div class="alert alert-success">' + resp.data + '</div>'); 435 436 $('#ermForm')[0].reset(); 437 $('#ermForm').removeClass('was-validated'); 438 439 } else { 440 441 $('#erm_response').html('<div class="alert alert-danger">' + resp.data + '</div>'); 442 443 } 444 }, 445 error: function () { 446 447 $('#erm_response').html('<div class="alert alert-danger">Something went wrong. Please try again.</div>'); 448 449 } 450 }); 451 452 }); 453 454 if ($('#erm-user-dashboard').length) { 455 456 $.post(erm_ajax.ajax_url, { 457 action: 'erm_user_dashboard', 458 nonce: erm_ajax.nonce 459 }, function(res){ 460 461 if(res.success){ 462 463 let stats = res.data.stats; 464 465 $('#erm-stats').html(` 466 <div class="col">Total: ${stats.total}</div> 467 <div class="col text-success">Approved: ${stats.approved}</div> 468 <div class="col text-warning">Pending: ${stats.pending}</div> 469 <div class="col text-danger">Rejected: ${stats.rejected}</div> 470 `); 471 472 let rows = ''; 473 474 res.data.list.forEach(item => { 475 rows += ` 476 <tr> 477 <td>${item.id}</td> 478 <td>${item.name}</td> 479 <td>${item.event}</td> 480 <td>${item.confirmed}</td> 481 <td>${item.status}</td> 482 <td>${item.submitted}</td> 483 </tr> 484 `; 485 }); 486 487 $('#erm-user-table').html(rows); 488 } 489 }); 490 491 } 492 493 494}); 495 496})(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.