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 (-12 / +189 lines)
Lines 884-909 input { Link Here
884
884
885
$dayLeadBackground: #fad2cf !default;
885
$dayLeadBackground: #fad2cf !default;
886
$dayTrailBackground: #fcdcb3 !default;
886
$dayTrailBackground: #fcdcb3 !default;
887
$existingLeadBackground: #e8f0d4 !default;
888
$existingTrailBackground: #d0e6f5 !default;
889
$trailClashLead: #d0f0e0 !default;
890
$leadClashTrail: #e5d4f5 !default;
887
.flatpickr-day {
891
.flatpickr-day {
888
889
    &.leadRangeStart {
892
    &.leadRangeStart {
890
        border-radius: 50px 0 0 50px;
893
        border-radius: 50px 0 0 50px;
891
    }
894
    }
892
895
893
    &.leadRange {
896
    &.leadRange {
894
        box-shadow: -2.5 * $dayMargin 0 0 $dayLeadBackground, 2.5 * $dayMargin 0 0 $dayLeadBackground;
897
        box-shadow:
898
            -2.5 * $dayMargin 0 0 $dayLeadBackground,
899
            2.5 * $dayMargin 0 0 $dayLeadBackground;
895
        background: $dayLeadBackground;
900
        background: $dayLeadBackground;
896
        &.flatpickr-disabled {
901
        &.flatpickr-disabled {
897
            $darkLeadBackground: darken($dayLeadBackground, 10);
902
            $darkLeadBackground: darken($dayLeadBackground, 10);
898
            box-shadow: -2.5 * $dayMargin 0 0 $darkLeadBackground, 2.5 * $dayMargin 0 0 $darkLeadBackground;
903
            box-shadow:
904
                -2.5 * $dayMargin 0 0 $darkLeadBackground,
905
                2.5 * $dayMargin 0 0 $darkLeadBackground;
899
            background: $darkLeadBackground;
906
            background: $darkLeadBackground;
900
        }
907
        }
901
    }
908
    }
902
909
903
    &.selected {
910
    &.selected {
904
       &.leadRangeEnd {
911
        &.leadRangeEnd {
905
           border-radius: 0;
912
            border-radius: 0;
906
       }
913
        }
907
    }
914
    }
908
915
909
    &:hover,
916
    &:hover,
Lines 914-924 $dayTrailBackground: #fcdcb3 !default; Link Here
914
    }
921
    }
915
922
916
    &.trailRange {
923
    &.trailRange {
917
        box-shadow: -2.5 * $dayMargin 0 0 $dayTrailBackground, 2.5 * $dayMargin 0 0 $dayTrailBackground;
924
        box-shadow:
925
            -2.5 * $dayMargin 0 0 $dayTrailBackground,
926
            2.5 * $dayMargin 0 0 $dayTrailBackground;
918
        background: $dayTrailBackground;
927
        background: $dayTrailBackground;
919
        &.flatpickr-disabled {
928
        &.flatpickr-disabled {
920
            $darkTrailBackground: darken($dayTrailBackground, 10);
929
            $darkTrailBackground: darken($dayTrailBackground, 10);
921
            box-shadow: -2.5 * $dayMargin 0 0 $darkTrailBackground, 2.5 * $dayMargin 0 0 $darkTrailBackground;
930
            box-shadow:
931
                -2.5 * $dayMargin 0 0 $darkTrailBackground,
932
                2.5 * $dayMargin 0 0 $darkTrailBackground;
922
            background: $darkTrailBackground;
933
            background: $darkTrailBackground;
923
        }
934
        }
924
    }
935
    }
Lines 928-943 $dayTrailBackground: #fcdcb3 !default; Link Here
928
    }
939
    }
929
940
930
    &.leadDisable {
941
    &.leadDisable {
931
        color: #aaa !important;  /* Gray out the text */
942
        color: #aaa !important; /* Gray out the text */
932
        background: #f5f5f5 !important;  /* Light background */
943
        background: #f5f5f5 !important; /* Light background */
933
        cursor: not-allowed !important;
944
        cursor: not-allowed !important;
934
    }
945
    }
935
946
936
    &.trailDisable {
947
    &.trailDisable {
937
        color: #aaa !important;  /* Gray out the text */
948
        color: #aaa !important; /* Gray out the text */
938
        background: #f5f5f5 !important;  /* Light background */
949
        background: #f5f5f5 !important; /* Light background */
939
        cursor: not-allowed !important;
950
        cursor: not-allowed !important;
940
    }
951
    }
952
953
    /* BIDIRECTIONAL ENHANCEMENT: Visual indicators for existing bookings' protected periods */
954
955
    /* Existing booking's lead period (pastel green - preparation time before their booking) */
956
    &.existingBookingLeadStart {
957
        border-radius: 50px 0 0 50px;
958
959
        /* Remove right rounding if it meets new booking trail start */
960
        &.trailRange {
961
            border-radius: 0;
962
        }
963
964
        /* Special case: leadRangeEnd + trailRangeStart on existingBookingLeadStart */
965
        /* Left shadow should be pink to match the leadRange to the left */
966
        &.leadRangeEnd.trailRangeStart {
967
            box-shadow:
968
                -2.5 * $dayMargin 0 0 $dayLeadBackground,
969
                2.5 * $dayMargin 0 0 $existingLeadBackground !important;
970
        }
971
972
        /* Special case: trailRangeEnd on existingBookingLeadStart */
973
        /* Right shadow should be pastel green to match the existingBookingLead to the right */
974
        &.trailRangeEnd {
975
            background: $existingLeadBackground !important; /* Green base fills the corners */
976
            border-radius: 0 !important; /* Right-side radius */
977
            position: relative;
978
            z-index: 1;
979
980
            box-shadow:
981
                -2.5 * $dayMargin 0 0 $trailClashLead,
982
                2.5 * $dayMargin 0 0 $existingLeadBackground !important;
983
984
            &::before {
985
                content: "";
986
                position: absolute;
987
                top: 0;
988
                left: -1px;
989
                right: 0px;
990
                bottom: 0;
991
                background: $trailClashLead; /* Olive collision color */
992
                border-radius: 0 50px 50px 0;
993
                z-index: -1;
994
            }
995
996
            display: flex;
997
            align-items: center;
998
            justify-content: center;
999
        }
1000
    }
1001
1002
    &.existingBookingLead {
1003
        background: $existingLeadBackground; /* Pastel green */
1004
        box-shadow:
1005
            -2.5 * $dayMargin 0 0 $existingLeadBackground,
1006
            2.5 * $dayMargin 0 0 $existingLeadBackground;
1007
    }
1008
1009
    /* New booking trail overlaps with existing booking lead */
1010
    &.trailRange.existingBookingLead {
1011
        background: $trailClashLead !important; /* Light olive (orange + green merge) */
1012
        box-shadow:
1013
            -2.5 * $dayMargin 0 0 $trailClashLead,
1014
            2.5 * $dayMargin 0 0 $trailClashLead;
1015
    }
1016
1017
    &.trailRangeEnd.existingBookingLead {
1018
        background: $existingLeadBackground !important; /* Base fills corners with Existing Lead Green */
1019
        border-radius: 0 !important;
1020
        position: relative;
1021
        z-index: 1;
1022
1023
        /* Shadows: Left matches the previous zone, Right matches the Green zone */
1024
        box-shadow:
1025
            -2.5 * $dayMargin 0 0 $trailClashLead,
1026
            2.5 * $dayMargin 0 0 $existingLeadBackground !important;
1027
1028
        &::before {
1029
            content: "";
1030
            position: absolute;
1031
            top: 0;
1032
            right: 0;
1033
            left: -1px;
1034
            bottom: 0;
1035
            background: $trailClashLead; /* Light olive overlap color */
1036
            border-radius: 0 50px 50px 0;
1037
            z-index: -1;
1038
        }
1039
1040
        display: flex;
1041
        align-items: center;
1042
        justify-content: center;
1043
    }
1044
1045
    /* Existing booking's trail period (pastel blue - processing time after their booking) */
1046
    &.existingBookingTrailEnd {
1047
        border-radius: 0 50px 50px 0;
1048
1049
        /* Remove left rounding if it meets new booking lead end */
1050
        &.leadRange {
1051
            border-radius: 0;
1052
        }
1053
1054
        /* Special case: leadRangeEnd + trailRangeStart on existingBookingTrailEnd */
1055
        /* Left shadow should be lavender to match the leadRange.existingBookingTrail to the left */
1056
        &.leadRangeEnd.trailRangeStart {
1057
            box-shadow:
1058
                -2.5 * $dayMargin 0 0 $leadClashTrail,
1059
                2.5 * $dayMargin 0 0 $existingTrailBackground !important;
1060
        }
1061
    }
1062
1063
    &.existingBookingTrail {
1064
        background: $existingTrailBackground; /* Pastel blue */
1065
        box-shadow:
1066
            -2.5 * $dayMargin 0 0 $existingTrailBackground,
1067
            2.5 * $dayMargin 0 0 $existingTrailBackground;
1068
    }
1069
1070
    /* New booking lead overlaps with existing booking trail */
1071
    &.leadRange.existingBookingTrail {
1072
        background: $leadClashTrail !important; /* Lavender (pink + blue merge) */
1073
        box-shadow:
1074
            -2.5 * $dayMargin 0 0 $leadClashTrail,
1075
            2.5 * $dayMargin 0 0 $leadClashTrail;
1076
    }
1077
1078
    &.leadRangeStart.existingBookingTrail {
1079
        background: $existingTrailBackground !important; /* Blue base fills the top/bottom left corners */
1080
        border-radius: 0 !important; /* Base cell must stay square */
1081
        position: relative;
1082
        z-index: 1;
1083
1084
        /* Shadows: Left is Blue, Right is Purple */
1085
        box-shadow:
1086
            -2.5 * $dayMargin 0 0 $existingTrailBackground,
1087
            2.5 * $dayMargin 0 0 $leadClashTrail !important;
1088
1089
        &::before {
1090
            content: "";
1091
            position: absolute;
1092
            top: 0;
1093
            left: 0;
1094
            right: -1px;
1095
            bottom: 0;
1096
            background: $leadClashTrail; /* Lavender */
1097
            border-radius: 50px 0 0 50px;
1098
            z-index: -1;
1099
        }
1100
1101
        display: flex;
1102
        align-items: center;
1103
        justify-content: center;
1104
    }
1105
}
1106
1107
// Booking conflict feedback message styles
1108
.booking-conflict-feedback {
1109
    // Override Bootstrap's default alert padding and sizing for compact display
1110
    padding: 0.5rem 0.75rem;
1111
    margin-top: 0.5rem;
1112
    margin-bottom: 0 !important; // Override Bootstrap's default 1rem margin-bottom
1113
    min-height: 1.25rem;
1114
    text-align: center;
1115
1116
    // Bootstrap's .alert-danger, .alert-warning, and .alert-info classes
1117
    // provide the color schemes via CSS variables when combined with .alert
941
}
1118
}
942
1119
943
.input-warning {
1120
.input-warning {
(-)a/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js (-27 / +625 lines)
Lines 1072-1077 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1072
                    );
1072
                    );
1073
                }
1073
                }
1074
1074
1075
                // Create feedback message container below the calendar
1076
                let feedbackDiv = periodPicker.calendarContainer.querySelector(
1077
                    ".booking-conflict-feedback"
1078
                );
1079
                if (!feedbackDiv) {
1080
                    feedbackDiv = document.createElement("div");
1081
                    feedbackDiv.className =
1082
                        "booking-conflict-feedback alert d-none";
1083
                    periodPicker.calendarContainer.appendChild(feedbackDiv);
1084
                }
1085
1075
                // Add hints for days before the start range and after the end range
1086
                // Add hints for days before the start range and after the end range
1076
                periodPicker.calendarContainer.addEventListener(
1087
                periodPicker.calendarContainer.addEventListener(
1077
                    "mouseover",
1088
                    "mouseover",
Lines 1087-1101 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1087
                                  )
1098
                                  )
1088
                                : null;
1099
                                : null;
1089
1100
1101
                            // Calculate new booking's lead/trail periods
1090
                            const leadStart = startDate
1102
                            const leadStart = startDate
1091
                                ? startDate.subtract(leadDays, "day")
1103
                                ? startDate.subtract(leadDays, "day")
1092
                                : hoverDate.subtract(leadDays, "day");
1104
                                : hoverDate.subtract(leadDays, "day");
1093
                            const leadEnd = startDate ? startDate : hoverDate;
1105
                            const leadEnd = startDate
1094
                            const trailStart = hoverDate;
1106
                                ? startDate.subtract(1, "day")
1095
                            const trailEnd = hoverDate.add(trailDays, "day");
1107
                                : hoverDate.subtract(1, "day");
1108
                            const trailStart = startDate
1109
                                ? hoverDate.add(1, "day")
1110
                                : hoverDate.add(1, "day");
1111
                            const trailEnd = startDate
1112
                                ? hoverDate.add(trailDays, "day")
1113
                                : hoverDate.add(trailDays, "day");
1114
1115
                            // BIDIRECTIONAL ENHANCEMENT: Collect closest bookings for visual feedback
1116
                            // and check for mathematical conflicts in a single pass
1117
                            let closestBeforeBooking = null;
1118
                            let closestBeforeDistance = Infinity;
1119
1120
                            let closestAfterBooking = null;
1121
                            let closestAfterDistance = Infinity;
1096
1122
1097
                            let leadDisable = false;
1123
                            let leadDisable = false;
1098
                            let trailDisable = false;
1124
                            let trailDisable = false;
1125
1126
                            // Track conflict reasons for messaging
1127
                            let leadConflictReason = {
1128
                                withTrail: false,
1129
                                withLead: false,
1130
                                withBooking: false,
1131
                            };
1132
                            let trailConflictReason = {
1133
                                withTrail: false,
1134
                                withLead: false,
1135
                                withBooking: false,
1136
                            };
1137
1138
                            bookings.forEach(booking => {
1139
                                // Skip if we're editing this booking
1140
                                if (
1141
                                    booking_id &&
1142
                                    booking_id == booking.booking_id
1143
                                ) {
1144
                                    return;
1145
                                }
1146
1147
                                // Skip if not same item (for item-specific bookings)
1148
                                if (
1149
                                    booking.item_id &&
1150
                                    booking_item_id &&
1151
                                    booking.item_id != booking_item_id
1152
                                ) {
1153
                                    return;
1154
                                }
1155
1156
                                const bookingStart = dayjs(
1157
                                    booking.start_date
1158
                                ).startOf("day");
1159
                                const bookingEnd = dayjs(
1160
                                    booking.end_date
1161
                                ).startOf("day");
1162
1163
                                // BIDIRECTIONAL: Mathematical checks for conflicts (works across month boundaries)
1164
                                // Calculate this booking's full protected period
1165
                                const existingLeadStart = bookingStart.subtract(
1166
                                    leadDays,
1167
                                    "day"
1168
                                );
1169
                                const existingLeadEnd = bookingStart.subtract(
1170
                                    1,
1171
                                    "day"
1172
                                );
1173
                                const existingTrailStart = bookingEnd.add(
1174
                                    1,
1175
                                    "day"
1176
                                );
1177
                                const existingTrailEnd = bookingEnd.add(
1178
                                    trailDays,
1179
                                    "day"
1180
                                );
1181
1182
                                // Check if new booking's LEAD period overlaps with existing booking
1183
                                if (!periodPicker.selectedDates[0]) {
1184
                                    // Check overlap with existing booking's trail period
1185
                                    if (
1186
                                        leadStart.isSameOrBefore(
1187
                                            existingTrailEnd
1188
                                        ) &&
1189
                                        leadEnd.isSameOrAfter(
1190
                                            existingTrailStart
1191
                                        )
1192
                                    ) {
1193
                                        leadDisable = true;
1194
                                        leadConflictReason.withTrail = true;
1195
                                    }
1196
                                    // Check overlap with existing booking's lead period
1197
                                    else if (
1198
                                        leadStart.isSameOrBefore(
1199
                                            existingLeadEnd
1200
                                        ) &&
1201
                                        leadEnd.isSameOrAfter(existingLeadStart)
1202
                                    ) {
1203
                                        leadDisable = true;
1204
                                        leadConflictReason.withLead = true;
1205
                                    }
1206
                                    // Check overlap with existing booking itself
1207
                                    else if (
1208
                                        leadStart.isSameOrBefore(bookingEnd) &&
1209
                                        leadEnd.isSameOrAfter(bookingStart)
1210
                                    ) {
1211
                                        leadDisable = true;
1212
                                        leadConflictReason.withBooking = true;
1213
                                    }
1214
                                }
1215
1216
                                // Check if new booking's TRAIL period overlaps with existing booking
1217
                                if (periodPicker.selectedDates[0]) {
1218
                                    // Check overlap with existing booking's lead period
1219
                                    if (
1220
                                        trailStart.isSameOrBefore(
1221
                                            existingLeadEnd
1222
                                        ) &&
1223
                                        trailEnd.isSameOrAfter(
1224
                                            existingLeadStart
1225
                                        )
1226
                                    ) {
1227
                                        trailDisable = true;
1228
                                        trailConflictReason.withLead = true;
1229
                                    }
1230
                                    // Check overlap with existing booking's trail period
1231
                                    else if (
1232
                                        trailStart.isSameOrBefore(
1233
                                            existingTrailEnd
1234
                                        ) &&
1235
                                        trailEnd.isSameOrAfter(
1236
                                            existingTrailStart
1237
                                        )
1238
                                    ) {
1239
                                        trailDisable = true;
1240
                                        trailConflictReason.withTrail = true;
1241
                                    }
1242
                                    // Check overlap with existing booking itself
1243
                                    else if (
1244
                                        trailStart.isSameOrBefore(bookingEnd) &&
1245
                                        trailEnd.isSameOrAfter(bookingStart)
1246
                                    ) {
1247
                                        trailDisable = true;
1248
                                        trailConflictReason.withBooking = true;
1249
                                    }
1250
                                }
1251
1252
                                // Find closest bookings for visual feedback (when dates are in view)
1253
                                if (bookingEnd.isBefore(hoverDate)) {
1254
                                    const distance = hoverDate.diff(
1255
                                        bookingEnd,
1256
                                        "day"
1257
                                    );
1258
                                    if (distance < closestBeforeDistance) {
1259
                                        closestBeforeDistance = distance;
1260
                                        closestBeforeBooking = {
1261
                                            start: bookingStart,
1262
                                            end: bookingEnd,
1263
                                        };
1264
                                    }
1265
                                }
1266
1267
                                if (bookingStart.isAfter(hoverDate)) {
1268
                                    const distance = bookingStart.diff(
1269
                                        hoverDate,
1270
                                        "day"
1271
                                    );
1272
                                    if (distance < closestAfterDistance) {
1273
                                        closestAfterDistance = distance;
1274
                                        closestAfterBooking = {
1275
                                            start: bookingStart,
1276
                                            end: bookingEnd,
1277
                                        };
1278
                                    }
1279
                                }
1280
                            });
1281
1282
                            // Work through all days in view and add classes appropraitely based on hovered date
1099
                            periodPicker.calendarContainer
1283
                            periodPicker.calendarContainer
1100
                                .querySelectorAll(".flatpickr-day")
1284
                                .querySelectorAll(".flatpickr-day")
1101
                                .forEach(function (dayElem) {
1285
                                .forEach(function (dayElem) {
Lines 1103-1135 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1103
                                        dayElem.dateObj
1287
                                        dayElem.dateObj
1104
                                    ).startOf("day");
1288
                                    ).startOf("day");
1105
1289
1106
                                    dayElem.classList.toggle(
1290
                                    // Clear existing booking lead/trail classes (including start/end)
1107
                                        "leadRangeStart",
1291
                                    dayElem.classList.remove(
1108
                                        elemDate.isSame(leadStart)
1292
                                        "existingBookingLead"
1109
                                    );
1293
                                    );
1110
                                    dayElem.classList.toggle(
1294
                                    dayElem.classList.remove(
1111
                                        "leadRange",
1295
                                        "existingBookingLeadStart"
1112
                                        elemDate.isSameOrAfter(leadStart) &&
1113
                                            elemDate.isBefore(leadEnd)
1114
                                    );
1296
                                    );
1115
                                    dayElem.classList.toggle(
1297
                                    dayElem.classList.remove(
1116
                                        "leadRangeEnd",
1298
                                        "existingBookingLeadEnd"
1117
                                        elemDate.isSame(leadEnd)
1118
                                    );
1299
                                    );
1119
                                    dayElem.classList.toggle(
1300
                                    dayElem.classList.remove(
1120
                                        "trailRangeStart",
1301
                                        "existingBookingTrail"
1121
                                        elemDate.isSame(trailStart)
1122
                                    );
1302
                                    );
1123
                                    dayElem.classList.toggle(
1303
                                    dayElem.classList.remove(
1124
                                        "trailRange",
1304
                                        "existingBookingTrailStart"
1125
                                        elemDate.isAfter(trailStart) &&
1126
                                            elemDate.isSameOrBefore(trailEnd)
1127
                                    );
1305
                                    );
1128
                                    dayElem.classList.toggle(
1306
                                    dayElem.classList.remove(
1129
                                        "trailRangeEnd",
1307
                                        "existingBookingTrailEnd"
1130
                                        elemDate.isSame(trailEnd)
1131
                                    );
1308
                                    );
1132
                                    // If we're overlapping a disabled date, disable our hoverDate
1309
1310
                                    // Apply proposed booking's lead/trail period classes
1311
                                    // Only apply lead classes if lead period > 0
1312
                                    if (leadDays > 0) {
1313
                                        dayElem.classList.toggle(
1314
                                            "leadRangeStart",
1315
                                            elemDate.isSame(leadStart)
1316
                                        );
1317
                                        dayElem.classList.toggle(
1318
                                            "leadRange",
1319
                                            elemDate.isSameOrAfter(leadStart) &&
1320
                                                elemDate.isSameOrBefore(leadEnd)
1321
                                        );
1322
                                        dayElem.classList.toggle(
1323
                                            "leadRangeEnd",
1324
                                            elemDate.isSame(leadEnd)
1325
                                        );
1326
                                    }
1327
1328
                                    // Only apply trail classes if trail period > 0
1329
                                    if (trailDays > 0) {
1330
                                        dayElem.classList.toggle(
1331
                                            "trailRangeStart",
1332
                                            elemDate.isSame(trailStart)
1333
                                        );
1334
                                        dayElem.classList.toggle(
1335
                                            "trailRange",
1336
                                            elemDate.isSameOrAfter(
1337
                                                trailStart
1338
                                            ) &&
1339
                                                elemDate.isSameOrBefore(
1340
                                                    trailEnd
1341
                                                )
1342
                                        );
1343
                                        dayElem.classList.toggle(
1344
                                            "trailRangeEnd",
1345
                                            elemDate.isSame(trailEnd)
1346
                                        );
1347
                                    }
1348
1349
                                    // Show closest preceding existing booking's trail period
1350
                                    if (closestBeforeBooking && trailDays > 0) {
1351
                                        const existingTrailStart =
1352
                                            closestBeforeBooking.end.add(
1353
                                                1,
1354
                                                "day"
1355
                                            );
1356
                                        const existingTrailEnd =
1357
                                            closestBeforeBooking.end.add(
1358
                                                trailDays,
1359
                                                "day"
1360
                                            );
1361
1362
                                        if (
1363
                                            elemDate.isSameOrAfter(
1364
                                                existingTrailStart
1365
                                            ) &&
1366
                                            elemDate.isSameOrBefore(
1367
                                                existingTrailEnd
1368
                                            )
1369
                                        ) {
1370
                                            dayElem.classList.add(
1371
                                                "existingBookingTrail"
1372
                                            );
1373
                                            // Add start/end classes for rounded borders
1374
                                            if (
1375
                                                elemDate.isSame(
1376
                                                    existingTrailStart
1377
                                                )
1378
                                            ) {
1379
                                                dayElem.classList.add(
1380
                                                    "existingBookingTrailStart"
1381
                                                );
1382
                                            }
1383
                                            if (
1384
                                                elemDate.isSame(
1385
                                                    existingTrailEnd
1386
                                                )
1387
                                            ) {
1388
                                                dayElem.classList.add(
1389
                                                    "existingBookingTrailEnd"
1390
                                                );
1391
                                            }
1392
                                        }
1393
                                    }
1394
1395
                                    // Show closest following existing booking's lead period
1396
                                    if (closestAfterBooking && leadDays > 0) {
1397
                                        const existingLeadStart =
1398
                                            closestAfterBooking.start.subtract(
1399
                                                leadDays,
1400
                                                "day"
1401
                                            );
1402
                                        const existingLeadEnd =
1403
                                            closestAfterBooking.start.subtract(
1404
                                                1,
1405
                                                "day"
1406
                                            );
1407
1408
                                        if (
1409
                                            elemDate.isSameOrAfter(
1410
                                                existingLeadStart
1411
                                            ) &&
1412
                                            elemDate.isSameOrBefore(
1413
                                                existingLeadEnd
1414
                                            )
1415
                                        ) {
1416
                                            dayElem.classList.add(
1417
                                                "existingBookingLead"
1418
                                            );
1419
                                            // Add start/end classes for rounded borders
1420
                                            if (
1421
                                                elemDate.isSame(
1422
                                                    existingLeadStart
1423
                                                )
1424
                                            ) {
1425
                                                dayElem.classList.add(
1426
                                                    "existingBookingLeadStart"
1427
                                                );
1428
                                            }
1429
                                            if (
1430
                                                elemDate.isSame(existingLeadEnd)
1431
                                            ) {
1432
                                                dayElem.classList.add(
1433
                                                    "existingBookingLeadEnd"
1434
                                                );
1435
                                            }
1436
                                        }
1437
                                    }
1438
1439
                                    // Check for conflicts with flatpickr-disabled dates
1133
                                    if (
1440
                                    if (
1134
                                        dayElem.classList.contains(
1441
                                        dayElem.classList.contains(
1135
                                            "flatpickr-disabled"
1442
                                            "flatpickr-disabled"
Lines 1138-1149 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1138
                                        if (
1445
                                        if (
1139
                                            !periodPicker.selectedDates[0] &&
1446
                                            !periodPicker.selectedDates[0] &&
1140
                                            elemDate.isSameOrAfter(leadStart) &&
1447
                                            elemDate.isSameOrAfter(leadStart) &&
1141
                                            elemDate.isBefore(leadEnd)
1448
                                            elemDate.isSameOrBefore(leadEnd)
1142
                                        ) {
1449
                                        ) {
1143
                                            leadDisable = true;
1450
                                            leadDisable = true;
1144
                                        }
1451
                                        }
1145
                                        if (
1452
                                        if (
1146
                                            elemDate.isAfter(trailStart) &&
1453
                                            periodPicker.selectedDates[0] &&
1454
                                            elemDate.isSameOrAfter(
1455
                                                trailStart
1456
                                            ) &&
1147
                                            elemDate.isSameOrBefore(trailEnd)
1457
                                            elemDate.isSameOrBefore(trailEnd)
1148
                                        ) {
1458
                                        ) {
1149
                                            // Only consider this a conflict if the disabled date is within the max date range
1459
                                            // Only consider this a conflict if the disabled date is within the max date range
Lines 1163-1168 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1163
                                            }
1473
                                            }
1164
                                        }
1474
                                        }
1165
                                    }
1475
                                    }
1476
1477
                                    // Check for conflicts with existing booking's trail period
1478
                                    if (
1479
                                        !periodPicker.selectedDates[0] &&
1480
                                        dayElem.classList.contains(
1481
                                            "existingBookingTrail"
1482
                                        )
1483
                                    ) {
1484
                                        // New booking's lead period overlaps with existing booking's trail
1485
                                        if (
1486
                                            elemDate.isSameOrAfter(leadStart) &&
1487
                                            elemDate.isSameOrBefore(leadEnd)
1488
                                        ) {
1489
                                            leadDisable = true;
1490
                                        }
1491
                                    }
1492
1493
                                    // Check for conflicts with existing booking's lead period
1494
                                    if (
1495
                                        periodPicker.selectedDates[0] &&
1496
                                        dayElem.classList.contains(
1497
                                            "existingBookingLead"
1498
                                        )
1499
                                    ) {
1500
                                        // New booking's trail period overlaps with existing booking's lead
1501
                                        if (
1502
                                            elemDate.isSameOrAfter(
1503
                                                trailStart
1504
                                            ) &&
1505
                                            elemDate.isSameOrBefore(trailEnd)
1506
                                        ) {
1507
                                            trailDisable = true;
1508
                                        }
1509
                                    }
1510
1166
                                    dayElem.classList.remove("leadDisable");
1511
                                    dayElem.classList.remove("leadDisable");
1167
                                    dayElem.classList.remove("trailDisable");
1512
                                    dayElem.classList.remove("trailDisable");
1168
                                    dayElem.removeEventListener(
1513
                                    dayElem.removeEventListener(
Lines 1172-1177 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1172
                                    );
1517
                                    );
1173
                                });
1518
                                });
1174
1519
1520
                            // Additional check for hovering directly on existing booking's lead/trail periods
1521
                            // If hovering on an existing booking's lead period when selecting start date, block selection
1522
                            if (
1523
                                !periodPicker.selectedDates[0] &&
1524
                                target.classList.contains("existingBookingLead")
1525
                            ) {
1526
                                leadDisable = true;
1527
                            }
1528
1529
                            // If hovering on an existing booking's trail period when selecting end date, block selection
1530
                            if (
1531
                                periodPicker.selectedDates[0] &&
1532
                                target.classList.contains(
1533
                                    "existingBookingTrail"
1534
                                )
1535
                            ) {
1536
                                trailDisable = true;
1537
                            }
1538
1175
                            if (leadDisable) {
1539
                            if (leadDisable) {
1176
                                target.classList.add("leadDisable");
1540
                                target.classList.add("leadDisable");
1177
                            }
1541
                            }
Lines 1185-1190 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1185
                                    true
1549
                                    true
1186
                                );
1550
                                );
1187
                            }
1551
                            }
1552
1553
                            // Update feedback message
1554
                            const feedbackDiv =
1555
                                periodPicker.calendarContainer.querySelector(
1556
                                    ".booking-conflict-feedback"
1557
                                );
1558
                            if (feedbackDiv) {
1559
                                let message = "";
1560
                                let messageType = "info"; // info, warning, error
1561
1562
                                // Determine what the hovered date is (needed for both error and info messages)
1563
                                const today = dayjs().startOf("day");
1564
                                const isDisabled =
1565
                                    target.classList.contains(
1566
                                        "flatpickr-disabled"
1567
                                    );
1568
                                const isInExistingLead =
1569
                                    target.classList.contains(
1570
                                        "existingBookingLead"
1571
                                    );
1572
                                const isInExistingTrail =
1573
                                    target.classList.contains(
1574
                                        "existingBookingTrail"
1575
                                    );
1576
1577
                                // Generate appropriate feedback messages based on conflicts
1578
                                if (leadDisable || trailDisable) {
1579
                                    messageType = "error";
1580
1581
                                    // When selecting START date (no date selected yet)
1582
                                    if (!periodPicker.selectedDates[0]) {
1583
                                        // Check direct state first (what IS this date?)
1584
                                        if (hoverDate.isBefore(today)) {
1585
                                            message = __(
1586
                                                "Cannot select: date is in the past"
1587
                                            );
1588
                                        } else if (isDisabled) {
1589
                                            message = __(
1590
                                                "Cannot select: this date is part of an existing booking"
1591
                                            );
1592
                                        } else if (isInExistingLead) {
1593
                                            message = __(
1594
                                                "Cannot select: this date is part of an existing booking's lead period"
1595
                                            );
1596
                                        } else if (isInExistingTrail) {
1597
                                            message = __(
1598
                                                "Cannot select: this date is part of an existing booking's trail period"
1599
                                            );
1600
                                        }
1601
                                        // Then check calculated lead period conflicts
1602
                                        else if (
1603
                                            leadDays > 0 &&
1604
                                            leadStart.isSameOrBefore(today)
1605
                                        ) {
1606
                                            message =
1607
                                                __("Cannot select") +
1608
                                                ": " +
1609
                                                __(
1610
                                                    "insufficient lead time (%s days required before start)"
1611
                                                ).format(leadDays);
1612
                                        } else if (leadDays > 0) {
1613
                                            // Use mathematical conflict detection (works across month boundaries)
1614
                                            if (leadConflictReason.withTrail) {
1615
                                                message =
1616
                                                    __("Cannot select") +
1617
                                                    ": " +
1618
                                                    __(
1619
                                                        "lead period (%s days before start) conflicts with an existing booking's trail period"
1620
                                                    ).format(leadDays);
1621
                                            } else if (
1622
                                                leadConflictReason.withLead
1623
                                            ) {
1624
                                                message =
1625
                                                    __("Cannot select") +
1626
                                                    ": " +
1627
                                                    __(
1628
                                                        "lead period (%s days before start) conflicts with an existing booking's lead period"
1629
                                                    ).format(leadDays);
1630
                                            } else if (
1631
                                                leadConflictReason.withBooking
1632
                                            ) {
1633
                                                message =
1634
                                                    __("Cannot select") +
1635
                                                    ": " +
1636
                                                    __(
1637
                                                        "lead period (%s days before start) conflicts with an existing booking"
1638
                                                    ).format(leadDays);
1639
                                            }
1640
                                        } else {
1641
                                            message = __(
1642
                                                "Cannot select: conflicts with an existing booking"
1643
                                            );
1644
                                        }
1645
                                    }
1646
                                    // When selecting END date (start date already selected)
1647
                                    else if (periodPicker.selectedDates[0]) {
1648
                                        // Check direct state first (what IS this date?)
1649
                                        if (isDisabled) {
1650
                                            message = __(
1651
                                                "Cannot select: this date is part of an existing booking"
1652
                                            );
1653
                                        } else if (isInExistingLead) {
1654
                                            message = __(
1655
                                                "Cannot select: this date is part of an existing booking's lead period"
1656
                                            );
1657
                                        } else if (isInExistingTrail) {
1658
                                            message = __(
1659
                                                "Cannot select: this date is part of an existing booking's trail period"
1660
                                            );
1661
                                        }
1662
                                        // Then check calculated trail period conflicts
1663
                                        else if (trailDays > 0) {
1664
                                            // Use mathematical conflict detection (works across month boundaries)
1665
                                            if (trailConflictReason.withLead) {
1666
                                                message =
1667
                                                    __("Cannot select") +
1668
                                                    ": " +
1669
                                                    __(
1670
                                                        "trail period (%s days after return) conflicts with an existing booking's lead period"
1671
                                                    ).format(trailDays);
1672
                                            } else if (
1673
                                                trailConflictReason.withTrail
1674
                                            ) {
1675
                                                message =
1676
                                                    __("Cannot select") +
1677
                                                    ": " +
1678
                                                    __(
1679
                                                        "trail period (%s days after return) conflicts with an existing booking's trail period"
1680
                                                    ).format(trailDays);
1681
                                            } else if (
1682
                                                trailConflictReason.withBooking
1683
                                            ) {
1684
                                                message =
1685
                                                    __("Cannot select") +
1686
                                                    ": " +
1687
                                                    __(
1688
                                                        "trail period (%s days after return) conflicts with an existing booking"
1689
                                                    ).format(trailDays);
1690
                                            }
1691
                                        } else {
1692
                                            message = __(
1693
                                                "Cannot select: conflicts with existing an booking"
1694
                                            );
1695
                                        }
1696
                                    }
1697
                                } else {
1698
                                    // Show helpful info messages when no conflicts
1699
                                    if (!periodPicker.selectedDates[0]) {
1700
                                        // When selecting start date, show both lead and trail info
1701
                                        if (leadDays > 0 && trailDays > 0) {
1702
                                            message =
1703
                                                __("Selecting start date") +
1704
                                                ". " +
1705
                                                __(
1706
                                                    "Lead period: %s days before start"
1707
                                                ).format(leadDays) +
1708
                                                ". " +
1709
                                                __(
1710
                                                    "Trail period: %s days after return"
1711
                                                ).format(trailDays);
1712
                                        } else if (leadDays > 0) {
1713
                                            message =
1714
                                                __("Selecting start date") +
1715
                                                ". " +
1716
                                                __(
1717
                                                    "Lead period: %s days before start"
1718
                                                ).format(leadDays);
1719
                                        } else if (trailDays > 0) {
1720
                                            message =
1721
                                                __("Selecting start date") +
1722
                                                ". " +
1723
                                                __(
1724
                                                    "Trail period: %s days after return"
1725
                                                ).format(trailDays);
1726
                                        } else {
1727
                                            message = __(
1728
                                                "Selecting start date"
1729
                                            );
1730
                                        }
1731
                                        messageType = "info";
1732
                                    } else {
1733
                                        if (trailDays > 0) {
1734
                                            message =
1735
                                                __("Selecting end date") +
1736
                                                ". " +
1737
                                                __(
1738
                                                    "Trail period: %s days after return"
1739
                                                ).format(trailDays);
1740
                                        } else {
1741
                                            message = __("Selecting end date");
1742
                                        }
1743
                                        messageType = "info";
1744
                                    }
1745
1746
                                    // Show additional context if hovering over existing booking periods
1747
                                    if (isInExistingLead) {
1748
                                        message +=
1749
                                            " • " +
1750
                                            __(
1751
                                                "hovering existing booking's lead period"
1752
                                            );
1753
                                    } else if (isInExistingTrail) {
1754
                                        message +=
1755
                                            " • " +
1756
                                            __(
1757
                                                "hovering existing booking's trail period"
1758
                                            );
1759
                                    }
1760
                                }
1761
1762
                                feedbackDiv.textContent = message;
1763
                                feedbackDiv.classList.remove(
1764
                                    "alert-danger",
1765
                                    "alert-warning",
1766
                                    "alert-info"
1767
                                );
1768
1769
                                if (message) {
1770
                                    feedbackDiv.classList.remove("d-none");
1771
                                    // Apply appropriate Bootstrap alert class based on message type
1772
                                    if (messageType === "error") {
1773
                                        feedbackDiv.classList.add(
1774
                                            "alert-danger"
1775
                                        );
1776
                                    } else if (messageType === "warning") {
1777
                                        feedbackDiv.classList.add(
1778
                                            "alert-warning"
1779
                                        );
1780
                                    } else {
1781
                                        feedbackDiv.classList.add("alert-info");
1782
                                    }
1783
                                } else {
1784
                                    feedbackDiv.classList.add("d-none");
1785
                                }
1786
                            }
1188
                        }
1787
                        }
1189
                    }
1788
                    }
1190
                );
1789
                );
1191
- 

Return to bug 37707