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 1050-1055 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1050
                    );
1050
                    );
1051
                }
1051
                }
1052
1052
1053
                // Create feedback message container below the calendar
1054
                let feedbackDiv = periodPicker.calendarContainer.querySelector(
1055
                    ".booking-conflict-feedback"
1056
                );
1057
                if (!feedbackDiv) {
1058
                    feedbackDiv = document.createElement("div");
1059
                    feedbackDiv.className =
1060
                        "booking-conflict-feedback alert d-none";
1061
                    periodPicker.calendarContainer.appendChild(feedbackDiv);
1062
                }
1063
1053
                // Add hints for days before the start range and after the end range
1064
                // Add hints for days before the start range and after the end range
1054
                periodPicker.calendarContainer.addEventListener(
1065
                periodPicker.calendarContainer.addEventListener(
1055
                    "mouseover",
1066
                    "mouseover",
Lines 1065-1079 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1065
                                  )
1076
                                  )
1066
                                : null;
1077
                                : null;
1067
1078
1079
                            // Calculate new booking's lead/trail periods
1068
                            const leadStart = startDate
1080
                            const leadStart = startDate
1069
                                ? startDate.subtract(leadDays, "day")
1081
                                ? startDate.subtract(leadDays, "day")
1070
                                : hoverDate.subtract(leadDays, "day");
1082
                                : hoverDate.subtract(leadDays, "day");
1071
                            const leadEnd = startDate ? startDate : hoverDate;
1083
                            const leadEnd = startDate
1072
                            const trailStart = hoverDate;
1084
                                ? startDate.subtract(1, "day")
1073
                            const trailEnd = hoverDate.add(trailDays, "day");
1085
                                : hoverDate.subtract(1, "day");
1086
                            const trailStart = startDate
1087
                                ? hoverDate.add(1, "day")
1088
                                : hoverDate.add(1, "day");
1089
                            const trailEnd = startDate
1090
                                ? hoverDate.add(trailDays, "day")
1091
                                : hoverDate.add(trailDays, "day");
1092
1093
                            // BIDIRECTIONAL ENHANCEMENT: Collect closest bookings for visual feedback
1094
                            // and check for mathematical conflicts in a single pass
1095
                            let closestBeforeBooking = null;
1096
                            let closestBeforeDistance = Infinity;
1097
1098
                            let closestAfterBooking = null;
1099
                            let closestAfterDistance = Infinity;
1074
1100
1075
                            let leadDisable = false;
1101
                            let leadDisable = false;
1076
                            let trailDisable = false;
1102
                            let trailDisable = false;
1103
1104
                            // Track conflict reasons for messaging
1105
                            let leadConflictReason = {
1106
                                withTrail: false,
1107
                                withLead: false,
1108
                                withBooking: false,
1109
                            };
1110
                            let trailConflictReason = {
1111
                                withTrail: false,
1112
                                withLead: false,
1113
                                withBooking: false,
1114
                            };
1115
1116
                            bookings.forEach(booking => {
1117
                                // Skip if we're editing this booking
1118
                                if (
1119
                                    booking_id &&
1120
                                    booking_id == booking.booking_id
1121
                                ) {
1122
                                    return;
1123
                                }
1124
1125
                                // Skip if not same item (for item-specific bookings)
1126
                                if (
1127
                                    booking.item_id &&
1128
                                    booking_item_id &&
1129
                                    booking.item_id != booking_item_id
1130
                                ) {
1131
                                    return;
1132
                                }
1133
1134
                                const bookingStart = dayjs(
1135
                                    booking.start_date
1136
                                ).startOf("day");
1137
                                const bookingEnd = dayjs(
1138
                                    booking.end_date
1139
                                ).startOf("day");
1140
1141
                                // BIDIRECTIONAL: Mathematical checks for conflicts (works across month boundaries)
1142
                                // Calculate this booking's full protected period
1143
                                const existingLeadStart = bookingStart.subtract(
1144
                                    leadDays,
1145
                                    "day"
1146
                                );
1147
                                const existingLeadEnd = bookingStart.subtract(
1148
                                    1,
1149
                                    "day"
1150
                                );
1151
                                const existingTrailStart = bookingEnd.add(
1152
                                    1,
1153
                                    "day"
1154
                                );
1155
                                const existingTrailEnd = bookingEnd.add(
1156
                                    trailDays,
1157
                                    "day"
1158
                                );
1159
1160
                                // Check if new booking's LEAD period overlaps with existing booking
1161
                                if (!periodPicker.selectedDates[0]) {
1162
                                    // Check overlap with existing booking's trail period
1163
                                    if (
1164
                                        leadStart.isSameOrBefore(
1165
                                            existingTrailEnd
1166
                                        ) &&
1167
                                        leadEnd.isSameOrAfter(
1168
                                            existingTrailStart
1169
                                        )
1170
                                    ) {
1171
                                        leadDisable = true;
1172
                                        leadConflictReason.withTrail = true;
1173
                                    }
1174
                                    // Check overlap with existing booking's lead period
1175
                                    else if (
1176
                                        leadStart.isSameOrBefore(
1177
                                            existingLeadEnd
1178
                                        ) &&
1179
                                        leadEnd.isSameOrAfter(existingLeadStart)
1180
                                    ) {
1181
                                        leadDisable = true;
1182
                                        leadConflictReason.withLead = true;
1183
                                    }
1184
                                    // Check overlap with existing booking itself
1185
                                    else if (
1186
                                        leadStart.isSameOrBefore(bookingEnd) &&
1187
                                        leadEnd.isSameOrAfter(bookingStart)
1188
                                    ) {
1189
                                        leadDisable = true;
1190
                                        leadConflictReason.withBooking = true;
1191
                                    }
1192
                                }
1193
1194
                                // Check if new booking's TRAIL period overlaps with existing booking
1195
                                if (periodPicker.selectedDates[0]) {
1196
                                    // Check overlap with existing booking's lead period
1197
                                    if (
1198
                                        trailStart.isSameOrBefore(
1199
                                            existingLeadEnd
1200
                                        ) &&
1201
                                        trailEnd.isSameOrAfter(
1202
                                            existingLeadStart
1203
                                        )
1204
                                    ) {
1205
                                        trailDisable = true;
1206
                                        trailConflictReason.withLead = true;
1207
                                    }
1208
                                    // Check overlap with existing booking's trail period
1209
                                    else if (
1210
                                        trailStart.isSameOrBefore(
1211
                                            existingTrailEnd
1212
                                        ) &&
1213
                                        trailEnd.isSameOrAfter(
1214
                                            existingTrailStart
1215
                                        )
1216
                                    ) {
1217
                                        trailDisable = true;
1218
                                        trailConflictReason.withTrail = true;
1219
                                    }
1220
                                    // Check overlap with existing booking itself
1221
                                    else if (
1222
                                        trailStart.isSameOrBefore(bookingEnd) &&
1223
                                        trailEnd.isSameOrAfter(bookingStart)
1224
                                    ) {
1225
                                        trailDisable = true;
1226
                                        trailConflictReason.withBooking = true;
1227
                                    }
1228
                                }
1229
1230
                                // Find closest bookings for visual feedback (when dates are in view)
1231
                                if (bookingEnd.isBefore(hoverDate)) {
1232
                                    const distance = hoverDate.diff(
1233
                                        bookingEnd,
1234
                                        "day"
1235
                                    );
1236
                                    if (distance < closestBeforeDistance) {
1237
                                        closestBeforeDistance = distance;
1238
                                        closestBeforeBooking = {
1239
                                            start: bookingStart,
1240
                                            end: bookingEnd,
1241
                                        };
1242
                                    }
1243
                                }
1244
1245
                                if (bookingStart.isAfter(hoverDate)) {
1246
                                    const distance = bookingStart.diff(
1247
                                        hoverDate,
1248
                                        "day"
1249
                                    );
1250
                                    if (distance < closestAfterDistance) {
1251
                                        closestAfterDistance = distance;
1252
                                        closestAfterBooking = {
1253
                                            start: bookingStart,
1254
                                            end: bookingEnd,
1255
                                        };
1256
                                    }
1257
                                }
1258
                            });
1259
1260
                            // Work through all days in view and add classes appropraitely based on hovered date
1077
                            periodPicker.calendarContainer
1261
                            periodPicker.calendarContainer
1078
                                .querySelectorAll(".flatpickr-day")
1262
                                .querySelectorAll(".flatpickr-day")
1079
                                .forEach(function (dayElem) {
1263
                                .forEach(function (dayElem) {
Lines 1081-1113 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1081
                                        dayElem.dateObj
1265
                                        dayElem.dateObj
1082
                                    ).startOf("day");
1266
                                    ).startOf("day");
1083
1267
1084
                                    dayElem.classList.toggle(
1268
                                    // Clear existing booking lead/trail classes (including start/end)
1085
                                        "leadRangeStart",
1269
                                    dayElem.classList.remove(
1086
                                        elemDate.isSame(leadStart)
1270
                                        "existingBookingLead"
1087
                                    );
1271
                                    );
1088
                                    dayElem.classList.toggle(
1272
                                    dayElem.classList.remove(
1089
                                        "leadRange",
1273
                                        "existingBookingLeadStart"
1090
                                        elemDate.isSameOrAfter(leadStart) &&
1091
                                            elemDate.isBefore(leadEnd)
1092
                                    );
1274
                                    );
1093
                                    dayElem.classList.toggle(
1275
                                    dayElem.classList.remove(
1094
                                        "leadRangeEnd",
1276
                                        "existingBookingLeadEnd"
1095
                                        elemDate.isSame(leadEnd)
1096
                                    );
1277
                                    );
1097
                                    dayElem.classList.toggle(
1278
                                    dayElem.classList.remove(
1098
                                        "trailRangeStart",
1279
                                        "existingBookingTrail"
1099
                                        elemDate.isSame(trailStart)
1100
                                    );
1280
                                    );
1101
                                    dayElem.classList.toggle(
1281
                                    dayElem.classList.remove(
1102
                                        "trailRange",
1282
                                        "existingBookingTrailStart"
1103
                                        elemDate.isAfter(trailStart) &&
1104
                                            elemDate.isSameOrBefore(trailEnd)
1105
                                    );
1283
                                    );
1106
                                    dayElem.classList.toggle(
1284
                                    dayElem.classList.remove(
1107
                                        "trailRangeEnd",
1285
                                        "existingBookingTrailEnd"
1108
                                        elemDate.isSame(trailEnd)
1109
                                    );
1286
                                    );
1110
                                    // If we're overlapping a disabled date, disable our hoverDate
1287
1288
                                    // Apply proposed booking's lead/trail period classes
1289
                                    // Only apply lead classes if lead period > 0
1290
                                    if (leadDays > 0) {
1291
                                        dayElem.classList.toggle(
1292
                                            "leadRangeStart",
1293
                                            elemDate.isSame(leadStart)
1294
                                        );
1295
                                        dayElem.classList.toggle(
1296
                                            "leadRange",
1297
                                            elemDate.isSameOrAfter(leadStart) &&
1298
                                                elemDate.isSameOrBefore(leadEnd)
1299
                                        );
1300
                                        dayElem.classList.toggle(
1301
                                            "leadRangeEnd",
1302
                                            elemDate.isSame(leadEnd)
1303
                                        );
1304
                                    }
1305
1306
                                    // Only apply trail classes if trail period > 0
1307
                                    if (trailDays > 0) {
1308
                                        dayElem.classList.toggle(
1309
                                            "trailRangeStart",
1310
                                            elemDate.isSame(trailStart)
1311
                                        );
1312
                                        dayElem.classList.toggle(
1313
                                            "trailRange",
1314
                                            elemDate.isSameOrAfter(
1315
                                                trailStart
1316
                                            ) &&
1317
                                                elemDate.isSameOrBefore(
1318
                                                    trailEnd
1319
                                                )
1320
                                        );
1321
                                        dayElem.classList.toggle(
1322
                                            "trailRangeEnd",
1323
                                            elemDate.isSame(trailEnd)
1324
                                        );
1325
                                    }
1326
1327
                                    // Show closest preceding existing booking's trail period
1328
                                    if (closestBeforeBooking && trailDays > 0) {
1329
                                        const existingTrailStart =
1330
                                            closestBeforeBooking.end.add(
1331
                                                1,
1332
                                                "day"
1333
                                            );
1334
                                        const existingTrailEnd =
1335
                                            closestBeforeBooking.end.add(
1336
                                                trailDays,
1337
                                                "day"
1338
                                            );
1339
1340
                                        if (
1341
                                            elemDate.isSameOrAfter(
1342
                                                existingTrailStart
1343
                                            ) &&
1344
                                            elemDate.isSameOrBefore(
1345
                                                existingTrailEnd
1346
                                            )
1347
                                        ) {
1348
                                            dayElem.classList.add(
1349
                                                "existingBookingTrail"
1350
                                            );
1351
                                            // Add start/end classes for rounded borders
1352
                                            if (
1353
                                                elemDate.isSame(
1354
                                                    existingTrailStart
1355
                                                )
1356
                                            ) {
1357
                                                dayElem.classList.add(
1358
                                                    "existingBookingTrailStart"
1359
                                                );
1360
                                            }
1361
                                            if (
1362
                                                elemDate.isSame(
1363
                                                    existingTrailEnd
1364
                                                )
1365
                                            ) {
1366
                                                dayElem.classList.add(
1367
                                                    "existingBookingTrailEnd"
1368
                                                );
1369
                                            }
1370
                                        }
1371
                                    }
1372
1373
                                    // Show closest following existing booking's lead period
1374
                                    if (closestAfterBooking && leadDays > 0) {
1375
                                        const existingLeadStart =
1376
                                            closestAfterBooking.start.subtract(
1377
                                                leadDays,
1378
                                                "day"
1379
                                            );
1380
                                        const existingLeadEnd =
1381
                                            closestAfterBooking.start.subtract(
1382
                                                1,
1383
                                                "day"
1384
                                            );
1385
1386
                                        if (
1387
                                            elemDate.isSameOrAfter(
1388
                                                existingLeadStart
1389
                                            ) &&
1390
                                            elemDate.isSameOrBefore(
1391
                                                existingLeadEnd
1392
                                            )
1393
                                        ) {
1394
                                            dayElem.classList.add(
1395
                                                "existingBookingLead"
1396
                                            );
1397
                                            // Add start/end classes for rounded borders
1398
                                            if (
1399
                                                elemDate.isSame(
1400
                                                    existingLeadStart
1401
                                                )
1402
                                            ) {
1403
                                                dayElem.classList.add(
1404
                                                    "existingBookingLeadStart"
1405
                                                );
1406
                                            }
1407
                                            if (
1408
                                                elemDate.isSame(existingLeadEnd)
1409
                                            ) {
1410
                                                dayElem.classList.add(
1411
                                                    "existingBookingLeadEnd"
1412
                                                );
1413
                                            }
1414
                                        }
1415
                                    }
1416
1417
                                    // Check for conflicts with flatpickr-disabled dates
1111
                                    if (
1418
                                    if (
1112
                                        dayElem.classList.contains(
1419
                                        dayElem.classList.contains(
1113
                                            "flatpickr-disabled"
1420
                                            "flatpickr-disabled"
Lines 1116-1127 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1116
                                        if (
1423
                                        if (
1117
                                            !periodPicker.selectedDates[0] &&
1424
                                            !periodPicker.selectedDates[0] &&
1118
                                            elemDate.isSameOrAfter(leadStart) &&
1425
                                            elemDate.isSameOrAfter(leadStart) &&
1119
                                            elemDate.isBefore(leadEnd)
1426
                                            elemDate.isSameOrBefore(leadEnd)
1120
                                        ) {
1427
                                        ) {
1121
                                            leadDisable = true;
1428
                                            leadDisable = true;
1122
                                        }
1429
                                        }
1123
                                        if (
1430
                                        if (
1124
                                            elemDate.isAfter(trailStart) &&
1431
                                            periodPicker.selectedDates[0] &&
1432
                                            elemDate.isSameOrAfter(
1433
                                                trailStart
1434
                                            ) &&
1125
                                            elemDate.isSameOrBefore(trailEnd)
1435
                                            elemDate.isSameOrBefore(trailEnd)
1126
                                        ) {
1436
                                        ) {
1127
                                            // Only consider this a conflict if the disabled date is within the max date range
1437
                                            // Only consider this a conflict if the disabled date is within the max date range
Lines 1141-1146 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1141
                                            }
1451
                                            }
1142
                                        }
1452
                                        }
1143
                                    }
1453
                                    }
1454
1455
                                    // Check for conflicts with existing booking's trail period
1456
                                    if (
1457
                                        !periodPicker.selectedDates[0] &&
1458
                                        dayElem.classList.contains(
1459
                                            "existingBookingTrail"
1460
                                        )
1461
                                    ) {
1462
                                        // New booking's lead period overlaps with existing booking's trail
1463
                                        if (
1464
                                            elemDate.isSameOrAfter(leadStart) &&
1465
                                            elemDate.isSameOrBefore(leadEnd)
1466
                                        ) {
1467
                                            leadDisable = true;
1468
                                        }
1469
                                    }
1470
1471
                                    // Check for conflicts with existing booking's lead period
1472
                                    if (
1473
                                        periodPicker.selectedDates[0] &&
1474
                                        dayElem.classList.contains(
1475
                                            "existingBookingLead"
1476
                                        )
1477
                                    ) {
1478
                                        // New booking's trail period overlaps with existing booking's lead
1479
                                        if (
1480
                                            elemDate.isSameOrAfter(
1481
                                                trailStart
1482
                                            ) &&
1483
                                            elemDate.isSameOrBefore(trailEnd)
1484
                                        ) {
1485
                                            trailDisable = true;
1486
                                        }
1487
                                    }
1488
1144
                                    dayElem.classList.remove("leadDisable");
1489
                                    dayElem.classList.remove("leadDisable");
1145
                                    dayElem.classList.remove("trailDisable");
1490
                                    dayElem.classList.remove("trailDisable");
1146
                                    dayElem.removeEventListener(
1491
                                    dayElem.removeEventListener(
Lines 1150-1155 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1150
                                    );
1495
                                    );
1151
                                });
1496
                                });
1152
1497
1498
                            // Additional check for hovering directly on existing booking's lead/trail periods
1499
                            // If hovering on an existing booking's lead period when selecting start date, block selection
1500
                            if (
1501
                                !periodPicker.selectedDates[0] &&
1502
                                target.classList.contains("existingBookingLead")
1503
                            ) {
1504
                                leadDisable = true;
1505
                            }
1506
1507
                            // If hovering on an existing booking's trail period when selecting end date, block selection
1508
                            if (
1509
                                periodPicker.selectedDates[0] &&
1510
                                target.classList.contains(
1511
                                    "existingBookingTrail"
1512
                                )
1513
                            ) {
1514
                                trailDisable = true;
1515
                            }
1516
1153
                            if (leadDisable) {
1517
                            if (leadDisable) {
1154
                                target.classList.add("leadDisable");
1518
                                target.classList.add("leadDisable");
1155
                            }
1519
                            }
Lines 1163-1168 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
1163
                                    true
1527
                                    true
1164
                                );
1528
                                );
1165
                            }
1529
                            }
1530
1531
                            // Update feedback message
1532
                            const feedbackDiv =
1533
                                periodPicker.calendarContainer.querySelector(
1534
                                    ".booking-conflict-feedback"
1535
                                );
1536
                            if (feedbackDiv) {
1537
                                let message = "";
1538
                                let messageType = "info"; // info, warning, error
1539
1540
                                // Determine what the hovered date is (needed for both error and info messages)
1541
                                const today = dayjs().startOf("day");
1542
                                const isDisabled =
1543
                                    target.classList.contains(
1544
                                        "flatpickr-disabled"
1545
                                    );
1546
                                const isInExistingLead =
1547
                                    target.classList.contains(
1548
                                        "existingBookingLead"
1549
                                    );
1550
                                const isInExistingTrail =
1551
                                    target.classList.contains(
1552
                                        "existingBookingTrail"
1553
                                    );
1554
1555
                                // Generate appropriate feedback messages based on conflicts
1556
                                if (leadDisable || trailDisable) {
1557
                                    messageType = "error";
1558
1559
                                    // When selecting START date (no date selected yet)
1560
                                    if (!periodPicker.selectedDates[0]) {
1561
                                        // Check direct state first (what IS this date?)
1562
                                        if (hoverDate.isBefore(today)) {
1563
                                            message = __(
1564
                                                "Cannot select: date is in the past"
1565
                                            );
1566
                                        } else if (isDisabled) {
1567
                                            message = __(
1568
                                                "Cannot select: this date is part of an existing booking"
1569
                                            );
1570
                                        } else if (isInExistingLead) {
1571
                                            message = __(
1572
                                                "Cannot select: this date is part of an existing booking's lead period"
1573
                                            );
1574
                                        } else if (isInExistingTrail) {
1575
                                            message = __(
1576
                                                "Cannot select: this date is part of an existing booking's trail period"
1577
                                            );
1578
                                        }
1579
                                        // Then check calculated lead period conflicts
1580
                                        else if (
1581
                                            leadDays > 0 &&
1582
                                            leadStart.isSameOrBefore(today)
1583
                                        ) {
1584
                                            message =
1585
                                                __("Cannot select") +
1586
                                                ": " +
1587
                                                __(
1588
                                                    "insufficient lead time (%s days required before start)"
1589
                                                ).format(leadDays);
1590
                                        } else if (leadDays > 0) {
1591
                                            // Use mathematical conflict detection (works across month boundaries)
1592
                                            if (leadConflictReason.withTrail) {
1593
                                                message =
1594
                                                    __("Cannot select") +
1595
                                                    ": " +
1596
                                                    __(
1597
                                                        "lead period (%s days before start) conflicts with an existing booking's trail period"
1598
                                                    ).format(leadDays);
1599
                                            } else if (
1600
                                                leadConflictReason.withLead
1601
                                            ) {
1602
                                                message =
1603
                                                    __("Cannot select") +
1604
                                                    ": " +
1605
                                                    __(
1606
                                                        "lead period (%s days before start) conflicts with an existing booking's lead period"
1607
                                                    ).format(leadDays);
1608
                                            } else if (
1609
                                                leadConflictReason.withBooking
1610
                                            ) {
1611
                                                message =
1612
                                                    __("Cannot select") +
1613
                                                    ": " +
1614
                                                    __(
1615
                                                        "lead period (%s days before start) conflicts with an existing booking"
1616
                                                    ).format(leadDays);
1617
                                            }
1618
                                        } else {
1619
                                            message = __(
1620
                                                "Cannot select: conflicts with an existing booking"
1621
                                            );
1622
                                        }
1623
                                    }
1624
                                    // When selecting END date (start date already selected)
1625
                                    else if (periodPicker.selectedDates[0]) {
1626
                                        // Check direct state first (what IS this date?)
1627
                                        if (isDisabled) {
1628
                                            message = __(
1629
                                                "Cannot select: this date is part of an existing booking"
1630
                                            );
1631
                                        } else if (isInExistingLead) {
1632
                                            message = __(
1633
                                                "Cannot select: this date is part of an existing booking's lead period"
1634
                                            );
1635
                                        } else if (isInExistingTrail) {
1636
                                            message = __(
1637
                                                "Cannot select: this date is part of an existing booking's trail period"
1638
                                            );
1639
                                        }
1640
                                        // Then check calculated trail period conflicts
1641
                                        else if (trailDays > 0) {
1642
                                            // Use mathematical conflict detection (works across month boundaries)
1643
                                            if (trailConflictReason.withLead) {
1644
                                                message =
1645
                                                    __("Cannot select") +
1646
                                                    ": " +
1647
                                                    __(
1648
                                                        "trail period (%s days after return) conflicts with an existing booking's lead period"
1649
                                                    ).format(trailDays);
1650
                                            } else if (
1651
                                                trailConflictReason.withTrail
1652
                                            ) {
1653
                                                message =
1654
                                                    __("Cannot select") +
1655
                                                    ": " +
1656
                                                    __(
1657
                                                        "trail period (%s days after return) conflicts with an existing booking's trail period"
1658
                                                    ).format(trailDays);
1659
                                            } else if (
1660
                                                trailConflictReason.withBooking
1661
                                            ) {
1662
                                                message =
1663
                                                    __("Cannot select") +
1664
                                                    ": " +
1665
                                                    __(
1666
                                                        "trail period (%s days after return) conflicts with an existing booking"
1667
                                                    ).format(trailDays);
1668
                                            }
1669
                                        } else {
1670
                                            message = __(
1671
                                                "Cannot select: conflicts with existing an booking"
1672
                                            );
1673
                                        }
1674
                                    }
1675
                                } else {
1676
                                    // Show helpful info messages when no conflicts
1677
                                    if (!periodPicker.selectedDates[0]) {
1678
                                        // When selecting start date, show both lead and trail info
1679
                                        if (leadDays > 0 && trailDays > 0) {
1680
                                            message =
1681
                                                __("Selecting start date") +
1682
                                                ". " +
1683
                                                __(
1684
                                                    "Lead period: %s days before start"
1685
                                                ).format(leadDays) +
1686
                                                ". " +
1687
                                                __(
1688
                                                    "Trail period: %s days after return"
1689
                                                ).format(trailDays);
1690
                                        } else if (leadDays > 0) {
1691
                                            message =
1692
                                                __("Selecting start date") +
1693
                                                ". " +
1694
                                                __(
1695
                                                    "Lead period: %s days before start"
1696
                                                ).format(leadDays);
1697
                                        } else if (trailDays > 0) {
1698
                                            message =
1699
                                                __("Selecting start date") +
1700
                                                ". " +
1701
                                                __(
1702
                                                    "Trail period: %s days after return"
1703
                                                ).format(trailDays);
1704
                                        } else {
1705
                                            message = __(
1706
                                                "Selecting start date"
1707
                                            );
1708
                                        }
1709
                                        messageType = "info";
1710
                                    } else {
1711
                                        if (trailDays > 0) {
1712
                                            message =
1713
                                                __("Selecting end date") +
1714
                                                ". " +
1715
                                                __(
1716
                                                    "Trail period: %s days after return"
1717
                                                ).format(trailDays);
1718
                                        } else {
1719
                                            message = __("Selecting end date");
1720
                                        }
1721
                                        messageType = "info";
1722
                                    }
1723
1724
                                    // Show additional context if hovering over existing booking periods
1725
                                    if (isInExistingLead) {
1726
                                        message +=
1727
                                            " • " +
1728
                                            __(
1729
                                                "hovering existing booking's lead period"
1730
                                            );
1731
                                    } else if (isInExistingTrail) {
1732
                                        message +=
1733
                                            " • " +
1734
                                            __(
1735
                                                "hovering existing booking's trail period"
1736
                                            );
1737
                                    }
1738
                                }
1739
1740
                                feedbackDiv.textContent = message;
1741
                                feedbackDiv.classList.remove(
1742
                                    "alert-danger",
1743
                                    "alert-warning",
1744
                                    "alert-info"
1745
                                );
1746
1747
                                if (message) {
1748
                                    feedbackDiv.classList.remove("d-none");
1749
                                    // Apply appropriate Bootstrap alert class based on message type
1750
                                    if (messageType === "error") {
1751
                                        feedbackDiv.classList.add(
1752
                                            "alert-danger"
1753
                                        );
1754
                                    } else if (messageType === "warning") {
1755
                                        feedbackDiv.classList.add(
1756
                                            "alert-warning"
1757
                                        );
1758
                                    } else {
1759
                                        feedbackDiv.classList.add("alert-info");
1760
                                    }
1761
                                } else {
1762
                                    feedbackDiv.classList.add("d-none");
1763
                                }
1764
                            }
1166
                        }
1765
                        }
1167
                    }
1766
                    }
1168
                );
1767
                );
1169
- 

Return to bug 37707