|
Lines 922-927
$("#placeBookingModal").on("show.bs.modal", function (e) {
Link Here
|
| 922 |
); |
922 |
); |
| 923 |
} |
923 |
} |
| 924 |
|
924 |
|
|
|
925 |
// Create feedback message container below the calendar |
| 926 |
let feedbackDiv = periodPicker.calendarContainer.querySelector( |
| 927 |
".booking-conflict-feedback" |
| 928 |
); |
| 929 |
if (!feedbackDiv) { |
| 930 |
feedbackDiv = document.createElement("div"); |
| 931 |
feedbackDiv.className = |
| 932 |
"booking-conflict-feedback alert d-none"; |
| 933 |
periodPicker.calendarContainer.appendChild(feedbackDiv); |
| 934 |
} |
| 935 |
|
| 925 |
// Add hints for days before the start range and after the end range |
936 |
// Add hints for days before the start range and after the end range |
| 926 |
periodPicker.calendarContainer.addEventListener( |
937 |
periodPicker.calendarContainer.addEventListener( |
| 927 |
"mouseover", |
938 |
"mouseover", |
|
Lines 1058-1063
$("#placeBookingModal").on("show.bs.modal", function (e) {
Link Here
|
| 1058 |
} |
1069 |
} |
| 1059 |
}); |
1070 |
}); |
| 1060 |
|
1071 |
|
|
|
1072 |
// Work through all days in view and add classes appropraitely based on hovered date |
| 1061 |
periodPicker.calendarContainer |
1073 |
periodPicker.calendarContainer |
| 1062 |
.querySelectorAll(".flatpickr-day") |
1074 |
.querySelectorAll(".flatpickr-day") |
| 1063 |
.forEach(function (dayElem) { |
1075 |
.forEach(function (dayElem) { |
|
Lines 1085-1091
$("#placeBookingModal").on("show.bs.modal", function (e) {
Link Here
|
| 1085 |
"existingBookingTrailEnd" |
1097 |
"existingBookingTrailEnd" |
| 1086 |
); |
1098 |
); |
| 1087 |
|
1099 |
|
| 1088 |
// Apply new booking's lead/trail period classes |
1100 |
// Apply proposed booking's lead/trail period classes |
| 1089 |
dayElem.classList.toggle( |
1101 |
dayElem.classList.toggle( |
| 1090 |
"leadRangeStart", |
1102 |
"leadRangeStart", |
| 1091 |
elemDate.isSame(leadStart) |
1103 |
elemDate.isSame(leadStart) |
|
Lines 1113-1119
$("#placeBookingModal").on("show.bs.modal", function (e) {
Link Here
|
| 1113 |
elemDate.isSame(trailEnd) |
1125 |
elemDate.isSame(trailEnd) |
| 1114 |
); |
1126 |
); |
| 1115 |
|
1127 |
|
| 1116 |
// BIDIRECTIONAL: Show closest existing booking's trail period |
1128 |
// Show closest preceding existing booking's trail period |
| 1117 |
if (closestBeforeBooking && trailDays > 0) { |
1129 |
if (closestBeforeBooking && trailDays > 0) { |
| 1118 |
const existingTrailStart = |
1130 |
const existingTrailStart = |
| 1119 |
closestBeforeBooking.end.add( |
1131 |
closestBeforeBooking.end.add( |
|
Lines 1159-1165
$("#placeBookingModal").on("show.bs.modal", function (e) {
Link Here
|
| 1159 |
} |
1171 |
} |
| 1160 |
} |
1172 |
} |
| 1161 |
|
1173 |
|
| 1162 |
// BIDIRECTIONAL: Show closest existing booking's lead period |
1174 |
// Show closest following existing booking's lead period |
| 1163 |
if (closestAfterBooking && leadDays > 0) { |
1175 |
if (closestAfterBooking && leadDays > 0) { |
| 1164 |
const existingLeadStart = |
1176 |
const existingLeadStart = |
| 1165 |
closestAfterBooking.start.subtract( |
1177 |
closestAfterBooking.start.subtract( |
|
Lines 1203-1209
$("#placeBookingModal").on("show.bs.modal", function (e) {
Link Here
|
| 1203 |
} |
1215 |
} |
| 1204 |
} |
1216 |
} |
| 1205 |
|
1217 |
|
| 1206 |
// Check for conflicts with flatpickr-disabled dates (original behavior) |
1218 |
// Check for conflicts with flatpickr-disabled dates |
| 1207 |
if ( |
1219 |
if ( |
| 1208 |
dayElem.classList.contains( |
1220 |
dayElem.classList.contains( |
| 1209 |
"flatpickr-disabled" |
1221 |
"flatpickr-disabled" |
|
Lines 1238-1244
$("#placeBookingModal").on("show.bs.modal", function (e) {
Link Here
|
| 1238 |
} |
1250 |
} |
| 1239 |
} |
1251 |
} |
| 1240 |
|
1252 |
|
| 1241 |
// BIDIRECTIONAL: Check for conflicts with existing booking's trail period |
1253 |
// Check for conflicts with existing booking's trail period |
| 1242 |
if ( |
1254 |
if ( |
| 1243 |
!periodPicker.selectedDates[0] && |
1255 |
!periodPicker.selectedDates[0] && |
| 1244 |
dayElem.classList.contains( |
1256 |
dayElem.classList.contains( |
|
Lines 1254-1260
$("#placeBookingModal").on("show.bs.modal", function (e) {
Link Here
|
| 1254 |
} |
1266 |
} |
| 1255 |
} |
1267 |
} |
| 1256 |
|
1268 |
|
| 1257 |
// BIDIRECTIONAL: Check for conflicts with existing booking's lead period |
1269 |
// Check for conflicts with existing booking's lead period |
| 1258 |
if ( |
1270 |
if ( |
| 1259 |
dayElem.classList.contains( |
1271 |
dayElem.classList.contains( |
| 1260 |
"existingBookingLead" |
1272 |
"existingBookingLead" |
|
Lines 1278-1283
$("#placeBookingModal").on("show.bs.modal", function (e) {
Link Here
|
| 1278 |
); |
1290 |
); |
| 1279 |
}); |
1291 |
}); |
| 1280 |
|
1292 |
|
|
|
1293 |
// Additional check for hovering directly on existing booking's lead/trail periods |
| 1294 |
// If hovering on an existing booking's lead period when selecting start date, block selection |
| 1295 |
if ( |
| 1296 |
!periodPicker.selectedDates[0] && |
| 1297 |
target.classList.contains("existingBookingLead") |
| 1298 |
) { |
| 1299 |
leadDisable = true; |
| 1300 |
} |
| 1301 |
|
| 1302 |
// If hovering on an existing booking's trail period when selecting end date, block selection |
| 1303 |
if ( |
| 1304 |
periodPicker.selectedDates[0] && |
| 1305 |
target.classList.contains( |
| 1306 |
"existingBookingTrail" |
| 1307 |
) |
| 1308 |
) { |
| 1309 |
trailDisable = true; |
| 1310 |
} |
| 1311 |
|
| 1281 |
if (leadDisable) { |
1312 |
if (leadDisable) { |
| 1282 |
target.classList.add("leadDisable"); |
1313 |
target.classList.add("leadDisable"); |
| 1283 |
} |
1314 |
} |
|
Lines 1291-1296
$("#placeBookingModal").on("show.bs.modal", function (e) {
Link Here
|
| 1291 |
true |
1322 |
true |
| 1292 |
); |
1323 |
); |
| 1293 |
} |
1324 |
} |
|
|
1325 |
|
| 1326 |
// Helper function to detect what a period (lead or trail) conflicts with |
| 1327 |
// Returns an object with conflict type flags |
| 1328 |
function detectPeriodConflict( |
| 1329 |
startDate, |
| 1330 |
endDate, |
| 1331 |
calendarContainer |
| 1332 |
) { |
| 1333 |
const conflict = { |
| 1334 |
withTrail: false, |
| 1335 |
withLead: false, |
| 1336 |
withBooking: false, |
| 1337 |
}; |
| 1338 |
|
| 1339 |
for ( |
| 1340 |
let checkDate = startDate; |
| 1341 |
checkDate.isSameOrBefore(endDate); |
| 1342 |
checkDate = checkDate.add(1, "day") |
| 1343 |
) { |
| 1344 |
const dayElem = |
| 1345 |
calendarContainer.querySelector( |
| 1346 |
`.flatpickr-day[aria-label="${checkDate.format("MMMM D, YYYY")}"]` |
| 1347 |
); |
| 1348 |
if (dayElem) { |
| 1349 |
if ( |
| 1350 |
dayElem.classList.contains( |
| 1351 |
"existingBookingTrail" |
| 1352 |
) |
| 1353 |
) { |
| 1354 |
conflict.withTrail = true; |
| 1355 |
break; |
| 1356 |
} |
| 1357 |
if ( |
| 1358 |
dayElem.classList.contains( |
| 1359 |
"existingBookingLead" |
| 1360 |
) |
| 1361 |
) { |
| 1362 |
conflict.withLead = true; |
| 1363 |
break; |
| 1364 |
} |
| 1365 |
if ( |
| 1366 |
dayElem.classList.contains( |
| 1367 |
"flatpickr-disabled" |
| 1368 |
) |
| 1369 |
) { |
| 1370 |
conflict.withBooking = true; |
| 1371 |
break; |
| 1372 |
} |
| 1373 |
} |
| 1374 |
} |
| 1375 |
|
| 1376 |
return conflict; |
| 1377 |
} |
| 1378 |
|
| 1379 |
// Update feedback message |
| 1380 |
const feedbackDiv = |
| 1381 |
periodPicker.calendarContainer.querySelector( |
| 1382 |
".booking-conflict-feedback" |
| 1383 |
); |
| 1384 |
if (feedbackDiv) { |
| 1385 |
let message = ""; |
| 1386 |
let messageType = "info"; // info, warning, error |
| 1387 |
|
| 1388 |
// Determine what the hovered date is (needed for both error and info messages) |
| 1389 |
const today = dayjs().startOf("day"); |
| 1390 |
const isDisabled = |
| 1391 |
target.classList.contains( |
| 1392 |
"flatpickr-disabled" |
| 1393 |
); |
| 1394 |
const isInExistingLead = |
| 1395 |
target.classList.contains( |
| 1396 |
"existingBookingLead" |
| 1397 |
); |
| 1398 |
const isInExistingTrail = |
| 1399 |
target.classList.contains( |
| 1400 |
"existingBookingTrail" |
| 1401 |
); |
| 1402 |
|
| 1403 |
// Generate appropriate feedback messages based on conflicts |
| 1404 |
if (leadDisable || trailDisable) { |
| 1405 |
messageType = "error"; |
| 1406 |
|
| 1407 |
// CASE 1: Both lead and trail conflicts (selecting start between two bookings) |
| 1408 |
if ( |
| 1409 |
leadDisable && |
| 1410 |
trailDisable && |
| 1411 |
!periodPicker.selectedDates[0] |
| 1412 |
) { |
| 1413 |
const leadConflict = |
| 1414 |
detectPeriodConflict( |
| 1415 |
leadStart, |
| 1416 |
leadEnd, |
| 1417 |
periodPicker.calendarContainer |
| 1418 |
); |
| 1419 |
const trailConflict = |
| 1420 |
detectPeriodConflict( |
| 1421 |
trailStart.add(1, "day"), |
| 1422 |
trailEnd, |
| 1423 |
periodPicker.calendarContainer |
| 1424 |
); |
| 1425 |
|
| 1426 |
let leadMsg = ""; |
| 1427 |
let trailMsg = ""; |
| 1428 |
|
| 1429 |
if (leadConflict.withTrail) { |
| 1430 |
leadMsg = __( |
| 1431 |
"lead period conflicts with existing booking's trail period" |
| 1432 |
); |
| 1433 |
} else if (leadConflict.withLead) { |
| 1434 |
leadMsg = __( |
| 1435 |
"lead period conflicts with existing booking's lead period" |
| 1436 |
); |
| 1437 |
} else if (leadConflict.withBooking) { |
| 1438 |
leadMsg = __( |
| 1439 |
"lead period conflicts with existing booking" |
| 1440 |
); |
| 1441 |
} |
| 1442 |
|
| 1443 |
if (trailConflict.withLead) { |
| 1444 |
trailMsg = __( |
| 1445 |
"trail period conflicts with existing booking's lead period" |
| 1446 |
); |
| 1447 |
} else if (trailConflict.withTrail) { |
| 1448 |
trailMsg = __( |
| 1449 |
"trail period conflicts with existing booking's trail period" |
| 1450 |
); |
| 1451 |
} else if (trailConflict.withBooking) { |
| 1452 |
trailMsg = __( |
| 1453 |
"trail period conflicts with existing booking" |
| 1454 |
); |
| 1455 |
} |
| 1456 |
|
| 1457 |
if (leadMsg && trailMsg) { |
| 1458 |
message = |
| 1459 |
__("Cannot select") + |
| 1460 |
": " + |
| 1461 |
leadMsg + |
| 1462 |
"; " + |
| 1463 |
trailMsg; |
| 1464 |
} else if (leadMsg) { |
| 1465 |
message = |
| 1466 |
__("Cannot select") + |
| 1467 |
": " + |
| 1468 |
leadMsg; |
| 1469 |
} else if (trailMsg) { |
| 1470 |
message = |
| 1471 |
__("Cannot select") + |
| 1472 |
": " + |
| 1473 |
trailMsg; |
| 1474 |
} else { |
| 1475 |
message = __( |
| 1476 |
"Cannot select: Multiple conflicts with existing bookings" |
| 1477 |
); |
| 1478 |
} |
| 1479 |
} |
| 1480 |
// CASE 2: Lead period conflict only (selecting start date) |
| 1481 |
else if ( |
| 1482 |
leadDisable && |
| 1483 |
!periodPicker.selectedDates[0] |
| 1484 |
) { |
| 1485 |
if (hoverDate.isBefore(today)) { |
| 1486 |
message = __( |
| 1487 |
"Cannot select: Date is in the past" |
| 1488 |
); |
| 1489 |
} else if ( |
| 1490 |
leadDays > 0 && |
| 1491 |
leadStart.isSameOrBefore(today) |
| 1492 |
) { |
| 1493 |
message = |
| 1494 |
__("Cannot select") + |
| 1495 |
": " + |
| 1496 |
__( |
| 1497 |
"Insufficient lead time (%s days required before start)" |
| 1498 |
).format(leadDays); |
| 1499 |
} else if (isDisabled) { |
| 1500 |
message = __( |
| 1501 |
"Cannot select: This date is part of an existing booking" |
| 1502 |
); |
| 1503 |
} else if (isInExistingLead) { |
| 1504 |
message = __( |
| 1505 |
"Cannot select: This date is part of an existing booking's lead period" |
| 1506 |
); |
| 1507 |
} else if (isInExistingTrail) { |
| 1508 |
message = __( |
| 1509 |
"Cannot select: This date is part of an existing booking's trail period" |
| 1510 |
); |
| 1511 |
} else if (leadDays > 0) { |
| 1512 |
const conflict = |
| 1513 |
detectPeriodConflict( |
| 1514 |
leadStart, |
| 1515 |
leadEnd, |
| 1516 |
periodPicker.calendarContainer |
| 1517 |
); |
| 1518 |
|
| 1519 |
if (conflict.withTrail) { |
| 1520 |
message = |
| 1521 |
__("Cannot select") + |
| 1522 |
": " + |
| 1523 |
__( |
| 1524 |
"Lead period (%s days before start) conflicts with existing booking's trail period" |
| 1525 |
).format(leadDays); |
| 1526 |
} else if (conflict.withLead) { |
| 1527 |
message = |
| 1528 |
__("Cannot select") + |
| 1529 |
": " + |
| 1530 |
__( |
| 1531 |
"Lead period (%s days before start) conflicts with existing booking's lead period" |
| 1532 |
).format(leadDays); |
| 1533 |
} else if (conflict.withBooking) { |
| 1534 |
message = |
| 1535 |
__("Cannot select") + |
| 1536 |
": " + |
| 1537 |
__( |
| 1538 |
"Lead period (%s days before start) conflicts with existing booking" |
| 1539 |
).format(leadDays); |
| 1540 |
} else { |
| 1541 |
message = |
| 1542 |
__("Cannot select") + |
| 1543 |
": " + |
| 1544 |
__( |
| 1545 |
"Lead period (%s days before start) conflicts with existing booking" |
| 1546 |
).format(leadDays); |
| 1547 |
} |
| 1548 |
} else { |
| 1549 |
message = __( |
| 1550 |
"Cannot select: Conflicts with existing booking" |
| 1551 |
); |
| 1552 |
} |
| 1553 |
} |
| 1554 |
// CASE 3: Trail period conflict only (selecting end date) |
| 1555 |
else if (trailDisable) { |
| 1556 |
if (isDisabled) { |
| 1557 |
message = __( |
| 1558 |
"Cannot select: This date is part of an existing booking" |
| 1559 |
); |
| 1560 |
} else if (isInExistingLead) { |
| 1561 |
message = __( |
| 1562 |
"Cannot select: This date is part of an existing booking's lead period" |
| 1563 |
); |
| 1564 |
} else if (isInExistingTrail) { |
| 1565 |
message = __( |
| 1566 |
"Cannot select: This date is part of an existing booking's trail period" |
| 1567 |
); |
| 1568 |
} else if (trailDays > 0) { |
| 1569 |
const conflict = |
| 1570 |
detectPeriodConflict( |
| 1571 |
trailStart.add(1, "day"), |
| 1572 |
trailEnd, |
| 1573 |
periodPicker.calendarContainer |
| 1574 |
); |
| 1575 |
|
| 1576 |
if (conflict.withLead) { |
| 1577 |
message = |
| 1578 |
__("Cannot select") + |
| 1579 |
": " + |
| 1580 |
__( |
| 1581 |
"Trail period (%s days after return) conflicts with existing booking's lead period" |
| 1582 |
).format(trailDays); |
| 1583 |
} else if (conflict.withTrail) { |
| 1584 |
message = |
| 1585 |
__("Cannot select") + |
| 1586 |
": " + |
| 1587 |
__( |
| 1588 |
"Trail period (%s days after return) conflicts with existing booking's trail period" |
| 1589 |
).format(trailDays); |
| 1590 |
} else if (conflict.withBooking) { |
| 1591 |
message = |
| 1592 |
__("Cannot select") + |
| 1593 |
": " + |
| 1594 |
__( |
| 1595 |
"Trail period (%s days after return) conflicts with existing booking" |
| 1596 |
).format(trailDays); |
| 1597 |
} else { |
| 1598 |
message = |
| 1599 |
__("Cannot select") + |
| 1600 |
": " + |
| 1601 |
__( |
| 1602 |
"Trail period (%s days after return) conflicts with existing booking" |
| 1603 |
).format(trailDays); |
| 1604 |
} |
| 1605 |
} else { |
| 1606 |
message = __( |
| 1607 |
"Cannot select: Conflicts with existing booking" |
| 1608 |
); |
| 1609 |
} |
| 1610 |
} |
| 1611 |
} else { |
| 1612 |
// Show helpful info messages when no conflicts |
| 1613 |
if (!periodPicker.selectedDates[0]) { |
| 1614 |
if (leadDays > 0) { |
| 1615 |
message = |
| 1616 |
__("Selecting start date") + |
| 1617 |
". " + |
| 1618 |
__( |
| 1619 |
"Lead period: %s days before start" |
| 1620 |
).format(leadDays); |
| 1621 |
} else { |
| 1622 |
message = __( |
| 1623 |
"Selecting start date" |
| 1624 |
); |
| 1625 |
} |
| 1626 |
messageType = "info"; |
| 1627 |
} else { |
| 1628 |
if (trailDays > 0) { |
| 1629 |
message = |
| 1630 |
__("Selecting end date") + |
| 1631 |
". " + |
| 1632 |
__( |
| 1633 |
"Trail period: %s days after return" |
| 1634 |
).format(trailDays); |
| 1635 |
} else { |
| 1636 |
message = __("Selecting end date"); |
| 1637 |
} |
| 1638 |
messageType = "info"; |
| 1639 |
} |
| 1640 |
|
| 1641 |
// Show additional context if hovering over existing booking periods |
| 1642 |
if (isInExistingLead) { |
| 1643 |
message += |
| 1644 |
" • " + |
| 1645 |
__( |
| 1646 |
"Hovering existing booking's lead period" |
| 1647 |
); |
| 1648 |
} else if (isInExistingTrail) { |
| 1649 |
message += |
| 1650 |
" • " + |
| 1651 |
__( |
| 1652 |
"Hovering existing booking's trail period" |
| 1653 |
); |
| 1654 |
} |
| 1655 |
} |
| 1656 |
|
| 1657 |
feedbackDiv.textContent = message; |
| 1658 |
feedbackDiv.classList.remove( |
| 1659 |
"alert-danger", |
| 1660 |
"alert-warning", |
| 1661 |
"alert-info" |
| 1662 |
); |
| 1663 |
|
| 1664 |
if (message) { |
| 1665 |
feedbackDiv.classList.remove("d-none"); |
| 1666 |
// Apply appropriate Bootstrap alert class based on message type |
| 1667 |
if (messageType === "error") { |
| 1668 |
feedbackDiv.classList.add( |
| 1669 |
"alert-danger" |
| 1670 |
); |
| 1671 |
} else if (messageType === "warning") { |
| 1672 |
feedbackDiv.classList.add( |
| 1673 |
"alert-warning" |
| 1674 |
); |
| 1675 |
} else { |
| 1676 |
feedbackDiv.classList.add("alert-info"); |
| 1677 |
} |
| 1678 |
} else { |
| 1679 |
feedbackDiv.classList.add("d-none"); |
| 1680 |
} |
| 1681 |
} |
| 1294 |
} |
1682 |
} |
| 1295 |
} |
1683 |
} |
| 1296 |
); |
1684 |
); |
| 1297 |
- |
|
|