View | Details | Raw Unified | Return to bug 37707
Collapse All | Expand All

(-)a/koha-tmpl/intranet-tmpl/prog/css/src/bookings.scss (-1 / +1 lines)
Lines 10-13 Link Here
10
            );
10
            );
11
        }
11
        }
12
    }
12
    }
13
}
13
}
(-)a/koha-tmpl/intranet-tmpl/prog/css/src/staff-global.scss (+13 lines)
Lines 1103-1108 $leadClashTrail: #e5d4f5 !default; Link Here
1103
    }
1103
    }
1104
}
1104
}
1105
1105
1106
// Booking conflict feedback message styles
1107
.booking-conflict-feedback {
1108
    // Override Bootstrap's default alert padding and sizing for compact display
1109
    padding: 0.5rem 0.75rem;
1110
    margin-top: 0.5rem;
1111
    margin-bottom: 0 !important; // Override Bootstrap's default 1rem margin-bottom
1112
    min-height: 1.25rem;
1113
    text-align: center;
1114
1115
    // Bootstrap's .alert-danger, .alert-warning, and .alert-info classes
1116
    // provide the color schemes via CSS variables when combined with .alert
1117
}
1118
1106
.input-warning {
1119
.input-warning {
1107
    background-color: #ff9;
1120
    background-color: #ff9;
1108
    border-color: #900;
1121
    border-color: #900;
(-)a/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js (-7 / +394 lines)
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
- 

Return to bug 37707