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

(-)a/koha-tmpl/intranet-tmpl/prog/js/modals/place_booking.js (-42 / +42 lines)
Lines 93-121 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
93
            let $patron = $("<span></span>")
93
            let $patron = $("<span></span>")
94
                .append(
94
                .append(
95
                    "" +
95
                    "" +
96
                        (patron.surname
96
                    (patron.surname
97
                            ? escape_str(patron.surname) + ", "
97
                        ? escape_str(patron.surname) + ", "
98
                            : "") +
98
                        : "") +
99
                        (patron.firstname
99
                    (patron.firstname
100
                            ? escape_str(patron.firstname) + " "
100
                        ? escape_str(patron.firstname) + " "
101
                            : "") +
101
                        : "") +
102
                        (patron.cardnumber
102
                    (patron.cardnumber
103
                            ? " (" + escape_str(patron.cardnumber) + ")"
103
                        ? " (" + escape_str(patron.cardnumber) + ")"
104
                            : "") +
104
                        : "") +
105
                        "<small>" +
105
                    "<small>" +
106
                        (patron.date_of_birth
106
                    (patron.date_of_birth
107
                            ? ' <span class="age_years">' +
107
                        ? ' <span class="age_years">' +
108
                              $get_age(patron.date_of_birth) +
108
                        $get_age(patron.date_of_birth) +
109
                              " " +
109
                        " " +
110
                              __("years") +
110
                        __("years") +
111
                              "</span>"
111
                        "</span>"
112
                            : "") +
112
                        : "") +
113
                        (patron.library
113
                    (patron.library
114
                            ? ' <span class="ac-library">' +
114
                        ? ' <span class="ac-library">' +
115
                              escape_str(patron.library.name) +
115
                        escape_str(patron.library.name) +
116
                              "</span>"
116
                        "</span>"
117
                            : "") +
117
                        : "") +
118
                        "</small>"
118
                    "</small>"
119
                )
119
                )
120
                .addClass(loggedInClass);
120
                .addClass(loggedInClass);
121
            return $patron;
121
            return $patron;
Lines 211-220 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
211
            ) {
211
            ) {
212
                let option = $(
212
                let option = $(
213
                    '<option value="' +
213
                    '<option value="' +
214
                        pickup_location.library_id +
214
                    pickup_location.library_id +
215
                        '">' +
215
                    '">' +
216
                        pickup_location.name +
216
                    pickup_location.name +
217
                        "</option>"
217
                    "</option>"
218
                );
218
                );
219
219
220
                option.attr(
220
                option.attr(
Lines 413-420 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
413
                    if (
413
                    if (
414
                        !$("#booking_itemtype").find(
414
                        !$("#booking_itemtype").find(
415
                            "option[value='" +
415
                            "option[value='" +
416
                                item.item_type.item_type_id +
416
                            item.item_type.item_type_id +
417
                                "']"
417
                            "']"
418
                        ).length
418
                        ).length
419
                    ) {
419
                    ) {
420
                        // Create a DOM Option and de-select by default
420
                        // Create a DOM Option and de-select by default
Lines 752-758 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
752
                                for (let i = 0; i < renewalsAllowed; i++) {
752
                                for (let i = 0; i < renewalsAllowed; i++) {
753
                                    nextDate.setDate(
753
                                    nextDate.setDate(
754
                                        nextDate.getDate() +
754
                                        nextDate.getDate() +
755
                                            parseInt(renewalLength)
755
                                        parseInt(renewalLength)
756
                                    );
756
                                    );
757
                                    boldDates.push(new Date(nextDate));
757
                                    boldDates.push(new Date(nextDate));
758
                                }
758
                                }
Lines 932-939 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
932
                            );
932
                            );
933
                            const startDate = periodPicker.selectedDates[0]
933
                            const startDate = periodPicker.selectedDates[0]
934
                                ? dayjs(periodPicker.selectedDates[0]).startOf(
934
                                ? dayjs(periodPicker.selectedDates[0]).startOf(
935
                                      "day"
935
                                    "day"
936
                                  )
936
                                )
937
                                : null;
937
                                : null;
938
938
939
                            const leadStart = startDate
939
                            const leadStart = startDate
Lines 959-965 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
959
                                    dayElem.classList.toggle(
959
                                    dayElem.classList.toggle(
960
                                        "leadRange",
960
                                        "leadRange",
961
                                        elemDate.isSameOrAfter(leadStart) &&
961
                                        elemDate.isSameOrAfter(leadStart) &&
962
                                            elemDate.isBefore(leadEnd)
962
                                        elemDate.isBefore(leadEnd)
963
                                    );
963
                                    );
964
                                    dayElem.classList.toggle(
964
                                    dayElem.classList.toggle(
965
                                        "leadRangeEnd",
965
                                        "leadRangeEnd",
Lines 972-978 $("#placeBookingModal").on("show.bs.modal", function (e) { Link Here
972
                                    dayElem.classList.toggle(
972
                                    dayElem.classList.toggle(
973
                                        "trailRange",
973
                                        "trailRange",
974
                                        elemDate.isAfter(trailStart) &&
974
                                        elemDate.isAfter(trailStart) &&
975
                                            elemDate.isSameOrBefore(trailEnd)
975
                                        elemDate.isSameOrBefore(trailEnd)
976
                                    );
976
                                    );
977
                                    dayElem.classList.toggle(
977
                                    dayElem.classList.toggle(
978
                                        "trailRangeEnd",
978
                                        "trailRangeEnd",
Lines 1195-1202 $("#placeBookingForm").on("submit", function (e) { Link Here
1195
            // Set feedback
1195
            // Set feedback
1196
            $("#transient_result").replaceWith(
1196
            $("#transient_result").replaceWith(
1197
                '<div id="transient_result" class="alert alert-info">' +
1197
                '<div id="transient_result" class="alert alert-info">' +
1198
                    __("Booking successfully placed") +
1198
                __("Booking successfully placed") +
1199
                    "</div>"
1199
                "</div>"
1200
            );
1200
            );
1201
1201
1202
            // Close modal
1202
            // Close modal
Lines 1206-1213 $("#placeBookingForm").on("submit", function (e) { Link Here
1206
        posting.fail(function (data) {
1206
        posting.fail(function (data) {
1207
            $("#booking_result").replaceWith(
1207
            $("#booking_result").replaceWith(
1208
                '<div id="booking_result" class="alert alert-danger">' +
1208
                '<div id="booking_result" class="alert alert-danger">' +
1209
                    __("Failure") +
1209
                __("Failure") +
1210
                    "</div>"
1210
                "</div>"
1211
            );
1211
            );
1212
        });
1212
        });
1213
    } else {
1213
    } else {
Lines 1264-1271 $("#placeBookingForm").on("submit", function (e) { Link Here
1264
            // Set feedback
1264
            // Set feedback
1265
            $("#transient_result").replaceWith(
1265
            $("#transient_result").replaceWith(
1266
                '<div id="transient_result" class="alert alert-info">' +
1266
                '<div id="transient_result" class="alert alert-info">' +
1267
                    __("Booking successfully updated") +
1267
                __("Booking successfully updated") +
1268
                    "</div>"
1268
                "</div>"
1269
            );
1269
            );
1270
1270
1271
            // Close modal
1271
            // Close modal
Lines 1275-1282 $("#placeBookingForm").on("submit", function (e) { Link Here
1275
        putting.fail(function (data) {
1275
        putting.fail(function (data) {
1276
            $("#booking_result").replaceWith(
1276
            $("#booking_result").replaceWith(
1277
                '<div id="booking_result" class="alert alert-danger">' +
1277
                '<div id="booking_result" class="alert alert-danger">' +
1278
                    __("Failure") +
1278
                __("Failure") +
1279
                    "</div>"
1279
                "</div>"
1280
            );
1280
            );
1281
        });
1281
        });
1282
    }
1282
    }
(-)a/t/cypress/integration/Biblio/bookingsModal_spec.ts (-1 / +374 lines)
Line 0 Link Here
0
- 
1
describe("Booking Modal Tests", () => {
2
    // Test data setup
3
    const testData = {
4
        biblionumber: "134",
5
        patronId: "19",
6
        pickupLibraryId: "CPL",
7
        itemNumber: "287",
8
        itemTypeId: "BK",
9
        startDate: new Date(),
10
        endDate: new Date(new Date().setDate(new Date().getDate() + 7)), // 7 days from now
11
    };
12
13
    beforeEach(() => {
14
        cy.login();
15
        cy.title().should("eq", "Koha staff interface");
16
17
        // Visit the page with the booking modal
18
        cy.visit(
19
            "/cgi-bin/koha/catalogue/detail.pl?biblionumber=" +
20
                testData.biblionumber
21
        );
22
23
        // Intercept API calls and provide mock responses
24
        cy.intercept("GET", "/api/v1/biblios/*/items?bookable=1&_per_page=-1", {
25
            fixture: "bookings/bookable_items.json",
26
        }).as("getBookableItems");
27
28
        cy.intercept("GET", "/api/v1/bookings?biblio_id=*&_per_page=-1*", {
29
            fixture: "bookings/bookings.json",
30
        }).as("getBookings");
31
32
        cy.intercept("GET", "/api/v1/biblios/*/checkouts?_per_page=-1", {
33
            fixture: "bookings/checkouts.json",
34
        }).as("getCheckouts");
35
36
        cy.intercept("GET", "/api/v1/patrons/*", {
37
            fixture: "bookings/patron.json",
38
        }).as("getPatron");
39
40
        cy.intercept("GET", "/api/v1/biblios/*/pickup_locations*", {
41
            fixture: "bookings/pickup_locations.json",
42
        }).as("getPickupLocations");
43
44
        cy.intercept("GET", "/api/v1/circulation_rules*", {
45
            fixture: "bookings/circulation_rules.json",
46
        }).as("getCirculationRules");
47
48
        cy.intercept("POST", "/api/v1/bookings", {
49
            statusCode: 201,
50
            body: {
51
                booking_id: "1001",
52
                start_date: testData.startDate.toISOString(),
53
                end_date: testData.endDate.toISOString(),
54
                pickup_library_id: testData.pickupLibraryId,
55
                biblio_id: testData.biblionumber,
56
                item_id: testData.itemNumber,
57
                patron_id: testData.patronId,
58
            },
59
        }).as("createBooking");
60
61
        cy.intercept("PUT", "/api/v1/bookings/*", {
62
            statusCode: 200,
63
            body: {
64
                booking_id: "1001",
65
                start_date: testData.startDate.toISOString(),
66
                end_date: testData.endDate.toISOString(),
67
                pickup_library_id: testData.pickupLibraryId,
68
                biblio_id: testData.biblionumber,
69
                item_id: testData.itemNumber,
70
                patron_id: testData.patronId,
71
            },
72
        }).as("updateBooking");
73
74
        // Populate the select2 search results for patron search
75
        cy.intercept("GET", "/api/v1/patrons*", {
76
            body: [
77
                {
78
                    patron_id: testData.patronId,
79
                    surname: "Doe",
80
                    firstname: "John",
81
                    cardnumber: "12345",
82
                    library_id: "1",
83
                    library: {
84
                        name: "Main Library",
85
                    },
86
                    category_id: "1",
87
                    date_of_birth: "1990-01-01",
88
                },
89
            ],
90
            pagination: { more: false },
91
        }).as("searchPatrons");
92
93
        // Add clickable button
94
        cy.document().then(doc => {
95
            const button = doc.createElement("button");
96
            button.setAttribute("data-bs-toggle", "modal");
97
            button.setAttribute("data-bs-target", "#placeBookingModal");
98
            button.setAttribute("data-biblionumber", testData.biblionumber);
99
            button.setAttribute("id", "placebooking");
100
            doc.body.appendChild(button);
101
        });
102
103
        // Open the booking modal
104
        cy.get("#placebooking").click();
105
    });
106
107
    it("should load the booking modal correctly", () => {
108
        // Check modal title
109
        cy.get("#placeBookingLabel").should("contain", "Place booking");
110
111
        // Check form elements are present
112
        cy.get("#booking_patron_id").should("exist");
113
        cy.get("#pickup_library_id").should("exist");
114
        cy.get("#booking_itemtype").should("exist");
115
        cy.get("#booking_item_id").should("exist");
116
        cy.get("#period").should("exist");
117
118
        // Check hidden fields
119
        cy.get("#booking_biblio_id").should(
120
            "have.value",
121
            testData.biblionumber
122
        );
123
        cy.get("#booking_start_date").should("have.value", "");
124
        cy.get("#booking_end_date").should("have.value", "");
125
    });
126
127
    it("should enable fields in proper sequence", () => {
128
        // Initially only patron field should be enabled
129
        cy.get("#booking_patron_id").should("not.be.disabled");
130
        cy.get("#pickup_library_id").should("be.disabled");
131
        cy.get("#booking_itemtype").should("be.disabled");
132
        cy.get("#booking_item_id").should("be.disabled");
133
        cy.get("#period").should("be.disabled");
134
135
        // Select patron
136
        cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John");
137
        /*
138
        cy.getSelect2("#booking_patron_id")
139
            .select2({ search: "John" })
140
            .select2({ selectIndex: 0 });
141
        */
142
        cy.wait("@getPickupLocations");
143
144
        // After patron selection, pickup location, item type and item should be enabled
145
        cy.get("#pickup_library_id").should("not.be.disabled");
146
        cy.get("#booking_itemtype").should("not.be.disabled");
147
        cy.get("#booking_item_id").should("not.be.disabled");
148
        cy.get("#period").should("be.disabled");
149
150
        // Select pickup location
151
        cy.selectFromSelect2ByIndex("#pickup_library_id", 0);
152
153
        // Select item type, trigger circulation rules
154
        cy.selectFromSelect2ByIndex("#booking_itemtype", 0);
155
        cy.wait("@getCirculationRules");
156
157
        // After patron, pickup location and itemtype/item selection, date picker should be enabled
158
        cy.get("#period").should("not.be.disabled");
159
160
        // Clear item type and confirm period is disabled
161
        cy.clearSelect2("#booking_itemtype");
162
        cy.get("#period").should("be.disabled");
163
164
        // Select item, re-enable period
165
        cy.selectFromSelect2ByIndex("#booking_item_id", 1);
166
        cy.get("#period").should("not.be.disabled");
167
    });
168
169
    it("should handle item type and item dependencies correctly", () => {
170
        // Select patron and pickup location first
171
        cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John");
172
        cy.wait("@getPickupLocations");
173
        cy.selectFromSelect2ByIndex("#pickup_library_id", 0);
174
175
        // Select an item first
176
        cy.selectFromSelect2ByIndex("#booking_item_id", 1);
177
        cy.wait("@getCirculationRules");
178
179
        // Verify that item type gets selected automatically
180
        cy.get("#booking_itemtype").should("have.value", testData.itemTypeId);
181
182
        // Verify that item type gets disabled
183
        cy.get("#booking_itemtype").should("be.disabled");
184
185
        // Reset the modal
186
        cy.get('#placeBookingModal button[data-bs-dismiss="modal"]')
187
            .first()
188
            .click();
189
        cy.get("#placebooking").click();
190
191
        // Now select patron, pickup and item type first
192
        cy.selectFromSelect2ByIndex("#booking_patron_id", 0, "John");
193
        cy.wait("@getPickupLocations");
194
        cy.selectFromSelect2ByIndex("#pickup_library_id", 0);
195
        cy.selectFromSelect2ByIndex("#booking_itemtype", 0);
196
        cy.wait("@getCirculationRules");
197
        cy.wait(300);
198
199
        // Verify that only 'Any item' option and items of selected type are enabled
200
        cy.get("#booking_item_id > option").then($options => {
201
            const enabledOptions = $options.filter(":not(:disabled)");
202
            enabledOptions.each(function () {
203
                const $option = cy.wrap(this);
204
205
                // Get both the value and the data-itemtype attribute to make decisions
206
                $option.invoke("val").then(value => {
207
                    if (value === "0") {
208
                        // We need to re-wrap the element since invoke('val') changed the subject
209
                        cy.wrap(this).should("contain.text", "Any item");
210
                    } else {
211
                        // Re-wrap the element again for this assertion
212
                        cy.wrap(this).should(
213
                            "have.attr",
214
                            "data-itemtype",
215
                            testData.itemTypeId
216
                        );
217
                    }
218
                });
219
            });
220
        });
221
    });
222
223
    it("should handle date selection and availability", () => {
224
        // Select patron, pickup location, item type, and item
225
        cy.select2Select("booking_patron_id", "John", { index: 0 });
226
        cy.wait("@getPickupLocations");
227
        cy.get("#pickup_library_id").select(testData.pickupLibraryId);
228
        cy.wait("@getCirculationRules");
229
        cy.get("#booking_itemtype").select(testData.itemTypeId);
230
        cy.get("#booking_item_id").select(testData.itemNumber);
231
232
        // Get the flatpickr instance and select dates
233
        cy.window().then(win => {
234
            const picker = win.document.getElementById("period")._flatpickr;
235
            const startDate = new Date(testData.startDate);
236
            const endDate = new Date(testData.endDate);
237
238
            picker.setDate([startDate, endDate], true);
239
240
            // Verify the hidden fields are set correctly
241
            cy.get("#booking_start_date").should("not.have.value", "");
242
            cy.get("#booking_end_date").should("not.have.value", "");
243
244
            // Check for disabled dates due to other bookings
245
            cy.get(".flatpickr-day.flatpickr-disabled").should("exist");
246
247
            // Check for event dots indicating existing bookings
248
            cy.get(".flatpickr-day .event-dots").should("exist");
249
        });
250
    });
251
252
    it("should submit a new booking successfully", () => {
253
        // Fill out the booking form
254
        cy.select2Select("booking_patron_id", "John", { index: 0 });
255
        cy.wait("@getPickupLocations");
256
        cy.get("#pickup_library_id").select(testData.pickupLibraryId);
257
        cy.wait("@getCirculationRules");
258
        cy.get("#booking_itemtype").select(testData.itemTypeId);
259
        cy.get("#booking_item_id").select(testData.itemNumber);
260
261
        // Set dates with flatpickr
262
        cy.window().then(win => {
263
            const picker = win.document.getElementById("period")._flatpickr;
264
            const startDate = new Date(testData.startDate);
265
            const endDate = new Date(testData.endDate);
266
            picker.setDate([startDate, endDate], true);
267
        });
268
269
        // Submit the form
270
        cy.get("#placeBookingForm").submit();
271
        cy.wait("@createBooking");
272
273
        // Check success message
274
        cy.get("#transient_result").should(
275
            "contain",
276
            "Booking successfully placed"
277
        );
278
279
        // Check modal closes
280
        cy.get("#placeBookingModal").should("not.be.visible");
281
    });
282
283
    it("should edit an existing booking successfully", () => {
284
        // Open edit booking modal
285
        cy.get("#test-modal-btn")
286
            .invoke("attr", "data-booking", "1001")
287
            .click();
288
289
        // Check modal title for edit
290
        cy.get("#placeBookingLabel").should("contain", "Edit booking");
291
292
        // Verify booking ID is set
293
        cy.get("#booking_id").should("have.value", "1001");
294
295
        // Change pickup location
296
        cy.get("#pickup_library_id").select("2");
297
298
        // Submit the form
299
        cy.get("#placeBookingForm").submit();
300
        cy.wait("@updateBooking");
301
302
        // Check success message
303
        cy.get("#transient_result").should(
304
            "contain",
305
            "Booking successfully updated"
306
        );
307
308
        // Check modal closes
309
        cy.get("#placeBookingModal").should("not.be.visible");
310
    });
311
312
    it("should handle booking failure gracefully", () => {
313
        // Override the create booking intercept to return an error
314
        cy.intercept("POST", "/api/v1/bookings", {
315
            statusCode: 400,
316
            body: {
317
                error: "Booking failed",
318
            },
319
        }).as("failedBooking");
320
321
        // Fill out the booking form
322
        cy.select2Select("booking_patron_id", "John", { index: 0 });
323
        cy.wait("@getPickupLocations");
324
        cy.get("#pickup_library_id").select(testData.pickupLibraryId);
325
        cy.wait("@getCirculationRules");
326
        cy.get("#booking_itemtype").select(testData.itemTypeId);
327
        cy.get("#booking_item_id").select(testData.itemNumber);
328
329
        // Set dates with flatpickr
330
        cy.window().then(win => {
331
            const picker = win.document.getElementById("period")._flatpickr;
332
            const startDate = new Date(testData.startDate);
333
            const endDate = new Date(testData.endDate);
334
            picker.setDate([startDate, endDate], true);
335
        });
336
337
        // Submit the form
338
        cy.get("#placeBookingForm").submit();
339
        cy.wait("@failedBooking");
340
341
        // Check error message
342
        cy.get("#booking_result").should("contain", "Failure");
343
344
        // Modal should remain open
345
        cy.get("#placeBookingModal").should("be.visible");
346
    });
347
348
    it("should reset form when modal is closed", () => {
349
        // Fill out some form fields
350
        cy.getSelect2("#booking_patron_id")
351
            .select2({ search: "John" })
352
            .select2({ selectIndex: 0 });
353
        cy.wait("@getPickupLocations");
354
        cy.get("#pickup_library_id").select(testData.pickupLibraryId);
355
356
        // Close the modal
357
        cy.get("#placeBookingModal .modal-header .close").click();
358
359
        // Re-open the modal
360
        cy.get(
361
            'button[data-biblionumber="' + testData.biblionumber + '"]'
362
        ).click();
363
364
        // Check fields are reset
365
        cy.get("#booking_patron_id").should("have.value", "");
366
        cy.get("#pickup_library_id").should("be.disabled");
367
        cy.get("#booking_itemtype").should("be.disabled");
368
        cy.get("#booking_item_id").should("be.disabled");
369
        cy.get("#period").should("be.disabled");
370
        cy.get("#booking_start_date").should("have.value", "");
371
        cy.get("#booking_end_date").should("have.value", "");
372
        cy.get("#booking_id").should("have.value", "");
373
    });
374
});

Return to bug 39916