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

(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/BookingModal.vue (-60 / +88 lines)
Lines 1-14 Link Here
1
<template>
1
<template>
2
    <div
2
    <div ref="modalElement" class="modal fade" tabindex="-1" role="dialog">
3
        ref="modalElement"
3
        <div class="modal-dialog modal-lg" role="document">
4
        class="modal fade"
5
        tabindex="-1"
6
        role="dialog"
7
    >
8
        <div
9
            class="modal-dialog modal-lg"
10
            role="document"
11
        >
12
            <div class="modal-content">
4
            <div class="modal-content">
13
                <div class="modal-header">
5
                <div class="modal-header">
14
                    <h1 class="modal-title fs-5">
6
                    <h1 class="modal-title fs-5">
Lines 164-170 import { Modal } from "bootstrap"; Link Here
164
import { appendHiddenInputs } from "./lib/adapters/form.mjs";
156
import { appendHiddenInputs } from "./lib/adapters/form.mjs";
165
import { calculateStepNumbers } from "./lib/ui/steps.mjs";
157
import { calculateStepNumbers } from "./lib/ui/steps.mjs";
166
import { useBookingValidation } from "./composables/useBookingValidation.mjs";
158
import { useBookingValidation } from "./composables/useBookingValidation.mjs";
167
import { calculateMaxBookingPeriod, getAvailableItemsForPeriod } from "./lib/booking/availability.mjs";
159
import {
160
    calculateMaxBookingPeriod,
161
    getAvailableItemsForPeriod,
162
} from "./lib/booking/availability.mjs";
168
import { useFormDefaults } from "./composables/useFormDefaults.mjs";
163
import { useFormDefaults } from "./composables/useFormDefaults.mjs";
169
import { buildNoItemsAvailableMessage } from "./lib/ui/selection-message.mjs";
164
import { buildNoItemsAvailableMessage } from "./lib/ui/selection-message.mjs";
170
import { useRulesFetcher } from "./composables/useRulesFetcher.mjs";
165
import { useRulesFetcher } from "./composables/useRulesFetcher.mjs";
Lines 191-197 interface AdditionalFieldsInstance { Link Here
191
    destroy?: () => void;
186
    destroy?: () => void;
192
}
187
}
193
188
194
type DateRangeConstraintType = "issuelength" | "issuelength_with_renewals" | "custom" | null;
189
type DateRangeConstraintType =
190
    | "issuelength"
191
    | "issuelength_with_renewals"
192
    | "custom"
193
    | null;
195
type SubmitType = "api" | "form-submission";
194
type SubmitType = "api" | "form-submission";
196
195
197
const props = withDefaults(
196
const props = withDefaults(
Lines 217-223 const props = withDefaults( Link Here
217
        authorizedValues?: Record<string, unknown> | null;
216
        authorizedValues?: Record<string, unknown> | null;
218
        showAdditionalFields?: boolean;
217
        showAdditionalFields?: boolean;
219
        dateRangeConstraint?: DateRangeConstraintType;
218
        dateRangeConstraint?: DateRangeConstraintType;
220
        customDateRangeFormula?: ((rules: CirculationRule) => number | null) | null;
219
        customDateRangeFormula?:
220
            | ((rules: CirculationRule) => number | null)
221
            | null;
221
        opacDefaultBookingLibraryEnabled?: boolean | string | null;
222
        opacDefaultBookingLibraryEnabled?: boolean | string | null;
222
        opacDefaultBookingLibrary?: string | null;
223
        opacDefaultBookingLibrary?: string | null;
223
    }>(),
224
    }>(),
Lines 288-294 const modalTitle = computed( Link Here
288
289
289
const showPickupLocationSelect = computed(() => {
290
const showPickupLocationSelect = computed(() => {
290
    if (props.opacDefaultBookingLibraryEnabled !== null) {
291
    if (props.opacDefaultBookingLibraryEnabled !== null) {
291
        const enabled = props.opacDefaultBookingLibraryEnabled === true ||
292
        const enabled =
293
            props.opacDefaultBookingLibraryEnabled === true ||
292
            String(props.opacDefaultBookingLibraryEnabled) === "1";
294
            String(props.opacDefaultBookingLibraryEnabled) === "1";
293
        return !enabled;
295
        return !enabled;
294
    }
296
    }
Lines 297-303 const showPickupLocationSelect = computed(() => { Link Here
297
299
298
const { canSubmit: canSubmitReactive } = useBookingValidation(store, {
300
const { canSubmit: canSubmitReactive } = useBookingValidation(store, {
299
    showPatronSelect: computed(() => props.showPatronSelect),
301
    showPatronSelect: computed(() => props.showPatronSelect),
300
    showItemDetailsSelects: computed(() => props.showItemDetailsSelects),
301
    showPickupLocationSelect,
302
    showPickupLocationSelect,
302
});
303
});
303
304
Lines 315-323 const submitLabel = computed(() => Link Here
315
    bookingId.value ? $__("Update booking") : $__("Place booking")
316
    bookingId.value ? $__("Update booking") : $__("Place booking")
316
);
317
);
317
318
318
const isFormSubmission = computed(
319
const isFormSubmission = computed(() => props.submitType === "form-submission");
319
    () => props.submitType === "form-submission"
320
);
321
320
322
const constraints = computed(() => {
321
const constraints = computed(() => {
323
    const pickup = constrainPickupLocations(
322
    const pickup = constrainPickupLocations(
Lines 353-365 const constraints = computed(() => { Link Here
353
});
352
});
354
353
355
const constrainedFlags = computed(() => constraints.value.flags);
354
const constrainedFlags = computed(() => constraints.value.flags);
356
const constrainedPickupLocations = computed(() => constraints.value.pickupLocations.filtered);
355
const constrainedPickupLocations = computed(
357
const constrainedBookableItems = computed(() => constraints.value.bookableItems.filtered);
356
    () => constraints.value.pickupLocations.filtered
358
const constrainedItemTypes = computed(() => constraints.value.itemTypes.filtered);
357
);
359
const pickupLocationsFilteredOut = computed(() => constraints.value.pickupLocations.filteredOutCount);
358
const constrainedBookableItems = computed(
360
const pickupLocationsTotal = computed(() => constraints.value.pickupLocations.total);
359
    () => constraints.value.bookableItems.filtered
361
const bookableItemsFilteredOut = computed(() => constraints.value.bookableItems.filteredOutCount);
360
);
362
const bookableItemsTotal = computed(() => constraints.value.bookableItems.total);
361
const constrainedItemTypes = computed(
362
    () => constraints.value.itemTypes.filtered
363
);
364
const pickupLocationsFilteredOut = computed(
365
    () => constraints.value.pickupLocations.filteredOutCount
366
);
367
const pickupLocationsTotal = computed(
368
    () => constraints.value.pickupLocations.total
369
);
370
const bookableItemsFilteredOut = computed(
371
    () => constraints.value.bookableItems.filteredOutCount
372
);
373
const bookableItemsTotal = computed(
374
    () => constraints.value.bookableItems.total
375
);
363
376
364
const maxBookingPeriod = computed(() =>
377
const maxBookingPeriod = computed(() =>
365
    calculateMaxBookingPeriod(
378
    calculateMaxBookingPeriod(
Lines 397-417 const dataReady = computed( Link Here
397
        !loading.value.checkouts &&
410
        !loading.value.checkouts &&
398
        (bookableItems.value?.length ?? 0) > 0
411
        (bookableItems.value?.length ?? 0) > 0
399
);
412
);
400
const formPrefilterValid = computed(() => {
413
const formPrefilterValid = computed(
401
    const requireTypeOrItem = !!props.showItemDetailsSelects;
414
    () => !props.showPatronSelect || !!bookingPatron.value
402
    const hasTypeOrItem =
415
);
403
        !!bookingItemId.value || !!bookingItemtypeId.value;
404
    const patronOk = !props.showPatronSelect || !!bookingPatron.value;
405
    return patronOk && (requireTypeOrItem ? hasTypeOrItem : true);
406
});
407
const hasAvailableItems = computed(
416
const hasAvailableItems = computed(
408
    () => constrainedBookableItems.value.length > 0
417
    () => constrainedBookableItems.value.length > 0
409
);
418
);
410
419
411
const isCalendarReady = computed(() => {
420
const isCalendarReady = computed(() => {
412
    const basicReady = dataReady.value &&
421
    const basicReady =
413
        formPrefilterValid.value &&
422
        dataReady.value && formPrefilterValid.value && hasAvailableItems.value;
414
        hasAvailableItems.value;
415
    if (!basicReady) return false;
423
    if (!basicReady) return false;
416
    if (loading.value.circulationRules) return true;
424
    if (loading.value.circulationRules) return true;
417
425
Lines 471-485 watch( Link Here
471
479
472
            if (props.patronId) {
480
            if (props.patronId) {
473
                const patron = await store.fetchPatron(props.patronId);
481
                const patron = await store.fetchPatron(props.patronId);
474
                await store.fetchPickupLocations(
482
                await store.fetchPickupLocations(biblionumber, props.patronId);
475
                    biblionumber,
476
                    props.patronId
477
                );
478
483
479
                bookingPatron.value = patron;
484
                bookingPatron.value = patron;
480
            }
485
            }
481
486
482
            bookingItemId.value = (props.itemId != null) ? normalizeIdType(bookableItems.value?.[0]?.item_id, props.itemId) : null;
487
            bookingItemId.value =
488
                props.itemId != null
489
                    ? normalizeIdType(
490
                          bookableItems.value?.[0]?.item_id,
491
                          props.itemId
492
                      )
493
                    : null;
483
            if (props.itemtypeId) {
494
            if (props.itemtypeId) {
484
                bookingItemtypeId.value = props.itemtypeId;
495
                bookingItemtypeId.value = props.itemtypeId;
485
            }
496
            }
Lines 552-558 watch( Link Here
552
        () => loading.value.pickupLocations,
563
        () => loading.value.pickupLocations,
553
    ],
564
    ],
554
    ([availableItems, patron, pickupLibrary, itemtypeId, isDataReady]) => {
565
    ([availableItems, patron, pickupLibrary, itemtypeId, isDataReady]) => {
555
        const pickupLocationsReady = !pickupLibrary || (!loading.value.pickupLocations && pickupLocations.value.length > 0);
566
        const pickupLocationsReady =
567
            !pickupLibrary ||
568
            (!loading.value.pickupLocations &&
569
                pickupLocations.value.length > 0);
556
        const circulationRulesReady = !loading.value.circulationRules;
570
        const circulationRulesReady = !loading.value.circulationRules;
557
571
558
        if (
572
        if (
Lines 625-631 function clearErrors() { Link Here
625
    store.resetErrors();
639
    store.resetErrors();
626
}
640
}
627
641
628
629
function resetModalState() {
642
function resetModalState() {
630
    bookingPatron.value = null;
643
    bookingPatron.value = null;
631
    pickupLibraryId.value = null;
644
    pickupLibraryId.value = null;
Lines 654-660 async function handleSubmit(event) { Link Here
654
    const selectedDates = selectedDateRange.value;
667
    const selectedDates = selectedDateRange.value;
655
668
656
    if (!selectedDates || selectedDates.length === 0) {
669
    if (!selectedDates || selectedDates.length === 0) {
657
        store.setUiError($__("Please select a valid date range"), "invalid_date_range");
670
        store.setUiError(
671
            $__("Please select a valid date range"),
672
            "invalid_date_range"
673
        );
658
        return;
674
        return;
659
    }
675
    }
660
676
Lines 663-669 async function handleSubmit(event) { Link Here
663
    const endDateRaw =
679
    const endDateRaw =
664
        selectedDates.length >= 2 ? selectedDates[1] : selectedDates[0];
680
        selectedDates.length >= 2 ? selectedDates[1] : selectedDates[0];
665
    // Apply endOf("day") to end date to match upstream storage format (23:59:59)
681
    // Apply endOf("day") to end date to match upstream storage format (23:59:59)
666
    const end = BookingDate.from(endDateRaw, { preserveTime: true }).toDayjs().endOf("day").toISOString();
682
    const end = BookingDate.from(endDateRaw, { preserveTime: true })
683
        .toDayjs()
684
        .endOf("day")
685
        .toISOString();
667
    const bookingData: Record<string, unknown> = {
686
    const bookingData: Record<string, unknown> = {
668
        booking_id: props.bookingId ?? undefined,
687
        booking_id: props.bookingId ?? undefined,
669
        start_date: start,
688
        start_date: start,
Lines 709-715 async function handleSubmit(event) { Link Here
709
728
710
    if (isFormSubmission.value) {
729
    if (isFormSubmission.value) {
711
        const form = event.target as HTMLFormElement;
730
        const form = event.target as HTMLFormElement;
712
        const csrfToken = document.querySelector('[name="csrf_token"]') as HTMLInputElement | null;
731
        const csrfToken = document.querySelector(
732
            '[name="csrf_token"]'
733
        ) as HTMLInputElement | null;
713
734
714
        const dataToSubmit: Record<string, unknown> = { ...bookingData };
735
        const dataToSubmit: Record<string, unknown> = { ...bookingData };
715
        if (dataToSubmit.extended_attributes) {
736
        if (dataToSubmit.extended_attributes) {
Lines 718-731 async function handleSubmit(event) { Link Here
718
            );
739
            );
719
        }
740
        }
720
741
721
        appendHiddenInputs(
742
        appendHiddenInputs(form, [
722
            form,
743
            ...Object.entries(dataToSubmit),
723
            [
744
            [csrfToken?.name, csrfToken?.value],
724
                ...Object.entries(dataToSubmit),
745
            ["op", "cud-add"],
725
                [csrfToken?.name, csrfToken?.value],
746
        ]);
726
                ['op', 'cud-add'],
727
            ]
728
        );
729
        form.submit();
747
        form.submit();
730
        return;
748
        return;
731
    }
749
    }
Lines 733-740 async function handleSubmit(event) { Link Here
733
    try {
751
    try {
734
        // Remove extended_attributes before API call — not yet supported upstream
752
        // Remove extended_attributes before API call — not yet supported upstream
735
        const { extended_attributes, ...apiData } = bookingData;
753
        const { extended_attributes, ...apiData } = bookingData;
736
        const result = await store.saveOrUpdateBooking(apiData)
754
        const result = await store.saveOrUpdateBooking(apiData);
737
        updateExternalDependents(result, bookingPatron.value, !!props.bookingId);
755
        updateExternalDependents(
756
            result,
757
            bookingPatron.value,
758
            !!props.bookingId
759
        );
738
        handleClose();
760
        handleClose();
739
    } catch (errorObj) {
761
    } catch (errorObj) {
740
        store.setUiError(processApiError(errorObj), "api");
762
        store.setUiError(processApiError(errorObj), "api");
Lines 763-769 onUnmounted(() => { Link Here
763
    }
785
    }
764
    bsModal?.dispose();
786
    bsModal?.dispose();
765
});
787
});
766
767
</script>
788
</script>
768
789
769
<style>
790
<style>
Lines 907-913 hr { Link Here
907
    border: var(--booking-border-width) solid var(--booking-neutral-300);
928
    border: var(--booking-border-width) solid var(--booking-neutral-300);
908
    border-radius: var(--booking-border-radius-sm);
929
    border-radius: var(--booking-border-radius-sm);
909
    font-size: var(--booking-text-base);
930
    font-size: var(--booking-text-base);
910
    transition: border-color var(--booking-transition-fast),
931
    transition:
932
        border-color var(--booking-transition-fast),
911
        box-shadow var(--booking-transition-fast);
933
        box-shadow var(--booking-transition-fast);
912
}
934
}
913
935
Lines 1075-1085 hr { Link Here
1075
    overflow: hidden;
1097
    overflow: hidden;
1076
    margin-top: 0;
1098
    margin-top: 0;
1077
    margin-bottom: 0;
1099
    margin-bottom: 0;
1078
    border-radius: 0 0 var(--booking-border-radius-sm) var(--booking-border-radius-sm);
1100
    border-radius: 0 0 var(--booking-border-radius-sm)
1101
        var(--booking-border-radius-sm);
1079
    font-size: var(--booking-text-sm);
1102
    font-size: var(--booking-text-sm);
1080
    text-align: center;
1103
    text-align: center;
1081
    transition: max-height 100ms ease, opacity 100ms ease, padding 100ms ease,
1104
    transition:
1082
        margin-top 100ms ease, background-color 100ms ease, color 100ms ease;
1105
        max-height 100ms ease,
1106
        opacity 100ms ease,
1107
        padding 100ms ease,
1108
        margin-top 100ms ease,
1109
        background-color 100ms ease,
1110
        color 100ms ease;
1083
}
1111
}
1084
1112
1085
.booking-hover-feedback--visible {
1113
.booking-hover-feedback--visible {
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/composables/useBookingValidation.mjs (-8 / +1 lines)
Lines 10-25 import { canSubmitBooking } from "../lib/booking/validation.mjs"; Link Here
10
/**
10
/**
11
 * Composable for booking validation with reactive state
11
 * Composable for booking validation with reactive state
12
 * @param {Object} store - Pinia booking store instance
12
 * @param {Object} store - Pinia booking store instance
13
 * @param {{ showPatronSelect: import('vue').Ref<boolean>|boolean, showItemDetailsSelects: import('vue').Ref<boolean>|boolean, showPickupLocationSelect: import('vue').Ref<boolean>|boolean }} uiFlags - Reactive UI visibility flags (props or refs)
13
 * @param {{ showPatronSelect: import('vue').Ref<boolean>|boolean, showPickupLocationSelect: import('vue').Ref<boolean>|boolean }} uiFlags - Reactive UI visibility flags (props or refs)
14
 * @returns {{ canSubmit: import('vue').ComputedRef<boolean> }}
14
 * @returns {{ canSubmit: import('vue').ComputedRef<boolean> }}
15
 */
15
 */
16
export function useBookingValidation(store, uiFlags = {}) {
16
export function useBookingValidation(store, uiFlags = {}) {
17
    const {
17
    const {
18
        bookingPatron,
18
        bookingPatron,
19
        pickupLibraryId,
19
        pickupLibraryId,
20
        bookingItemtypeId,
21
        itemTypes,
22
        bookingItemId,
23
        bookableItems,
20
        bookableItems,
24
        selectedDateRange,
21
        selectedDateRange,
25
    } = storeToRefs(store);
22
    } = storeToRefs(store);
Lines 28-39 export function useBookingValidation(store, uiFlags = {}) { Link Here
28
        const validationData = {
25
        const validationData = {
29
            showPatronSelect: uiFlags.showPatronSelect?.value ?? uiFlags.showPatronSelect ?? false,
26
            showPatronSelect: uiFlags.showPatronSelect?.value ?? uiFlags.showPatronSelect ?? false,
30
            bookingPatron: bookingPatron.value,
27
            bookingPatron: bookingPatron.value,
31
            showItemDetailsSelects: uiFlags.showItemDetailsSelects?.value ?? uiFlags.showItemDetailsSelects ?? false,
32
            showPickupLocationSelect: uiFlags.showPickupLocationSelect?.value ?? uiFlags.showPickupLocationSelect ?? false,
28
            showPickupLocationSelect: uiFlags.showPickupLocationSelect?.value ?? uiFlags.showPickupLocationSelect ?? false,
33
            pickupLibraryId: pickupLibraryId.value,
29
            pickupLibraryId: pickupLibraryId.value,
34
            bookingItemtypeId: bookingItemtypeId.value,
35
            itemtypeOptions: itemTypes.value,
36
            bookingItemId: bookingItemId.value,
37
            bookableItems: bookableItems.value,
30
            bookableItems: bookableItems.value,
38
        };
31
        };
39
        return canSubmitBooking(validationData, selectedDateRange.value);
32
        return canSubmitBooking(validationData, selectedDateRange.value);
(-)a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/booking/validation.mjs (-21 / +2 lines)
Lines 1-6 Link Here
1
/**
1
/**
2
 * Pure functions for booking validation logic
2
 * Pure functions for booking validation logic
3
 * Extracted from BookingValidationService to eliminate store coupling
4
 */
3
 */
5
4
6
/**
5
/**
Lines 8-19 Link Here
8
 * @param {Object} validationData - All required data for validation
7
 * @param {Object} validationData - All required data for validation
9
 * @param {boolean} validationData.showPatronSelect - Whether patron selection is required
8
 * @param {boolean} validationData.showPatronSelect - Whether patron selection is required
10
 * @param {Object} validationData.bookingPatron - Selected booking patron
9
 * @param {Object} validationData.bookingPatron - Selected booking patron
11
 * @param {boolean} validationData.showItemDetailsSelects - Whether item details are required
12
 * @param {boolean} validationData.showPickupLocationSelect - Whether pickup location is required
10
 * @param {boolean} validationData.showPickupLocationSelect - Whether pickup location is required
13
 * @param {string} validationData.pickupLibraryId - Selected pickup library ID
11
 * @param {string} validationData.pickupLibraryId - Selected pickup library ID
14
 * @param {string} validationData.bookingItemtypeId - Selected item type ID
15
 * @param {Array} validationData.itemtypeOptions - Available item type options
16
 * @param {string} validationData.bookingItemId - Selected item ID
17
 * @param {Array} validationData.bookableItems - Available bookable items
12
 * @param {Array} validationData.bookableItems - Available bookable items
18
 * @returns {boolean} Whether the user can proceed to step 3
13
 * @returns {boolean} Whether the user can proceed to step 3
19
 */
14
 */
Lines 21-32 export function canProceedToStep3(validationData) { Link Here
21
    const {
16
    const {
22
        showPatronSelect,
17
        showPatronSelect,
23
        bookingPatron,
18
        bookingPatron,
24
        showItemDetailsSelects,
25
        showPickupLocationSelect,
19
        showPickupLocationSelect,
26
        pickupLibraryId,
20
        pickupLibraryId,
27
        bookingItemtypeId,
28
        itemtypeOptions,
29
        bookingItemId,
30
        bookableItems,
21
        bookableItems,
31
    } = validationData;
22
    } = validationData;
32
23
Lines 34-51 export function canProceedToStep3(validationData) { Link Here
34
        return false;
25
        return false;
35
    }
26
    }
36
27
37
    if (showItemDetailsSelects || showPickupLocationSelect) {
28
    if (showPickupLocationSelect && !pickupLibraryId) {
38
        if (showPickupLocationSelect && !pickupLibraryId) {
29
        return false;
39
            return false;
40
        }
41
        if (showItemDetailsSelects) {
42
            if (!bookingItemtypeId && itemtypeOptions.length > 0) {
43
                return false;
44
            }
45
            if (!bookingItemId && bookableItems.length > 0) {
46
                return false;
47
            }
48
        }
49
    }
30
    }
50
31
51
    if (!bookableItems || bookableItems.length === 0) {
32
    if (!bookableItems || bookableItems.length === 0) {
(-)a/t/cypress/integration/Circulation/bookingsModalBasic_spec.ts (-55 / +84 lines)
Lines 25-40 describe("Booking Modal Basic Tests", () => { Link Here
25
                testData = objects;
25
                testData = objects;
26
26
27
                // Update items to be bookable with different itemtypes
27
                // Update items to be bookable with different itemtypes
28
                return cy.task("query", {
28
                return cy
29
                    sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = 'A', dateaccessioned = '2024-12-03' WHERE itemnumber = ?",
29
                    .task("query", {
30
                    values: [objects.items[0].item_id],
30
                        sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = 'A', dateaccessioned = '2024-12-03' WHERE itemnumber = ?",
31
                }).then(() => cy.task("query", {
31
                        values: [objects.items[0].item_id],
32
                    sql: "UPDATE items SET bookable = 1, itype = 'CF', homebranch = 'CPL', enumchron = 'B', dateaccessioned = '2024-12-02' WHERE itemnumber = ?",
32
                    })
33
                    values: [objects.items[1].item_id],
33
                    .then(() =>
34
                })).then(() => cy.task("query", {
34
                        cy.task("query", {
35
                    sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = 'C', dateaccessioned = '2024-12-01' WHERE itemnumber = ?",
35
                            sql: "UPDATE items SET bookable = 1, itype = 'CF', homebranch = 'CPL', enumchron = 'B', dateaccessioned = '2024-12-02' WHERE itemnumber = ?",
36
                    values: [objects.items[2].item_id],
36
                            values: [objects.items[1].item_id],
37
                }));
37
                        })
38
                    )
39
                    .then(() =>
40
                        cy.task("query", {
41
                            sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = 'C', dateaccessioned = '2024-12-01' WHERE itemnumber = ?",
42
                            values: [objects.items[2].item_id],
43
                        })
44
                    );
38
            })
45
            })
39
            .then(() => {
46
            .then(() => {
40
                // Create a test patron using upstream pattern
47
                // Create a test patron using upstream pattern
Lines 92-104 describe("Booking Modal Basic Tests", () => { Link Here
92
        cy.get("#catalog_detail").should("be.visible");
99
        cy.get("#catalog_detail").should("be.visible");
93
100
94
        // The "Place booking" button should appear for bookable items
101
        // The "Place booking" button should appear for bookable items
95
        cy.get("[data-booking-modal]")
102
        cy.get("[data-booking-modal]").should("exist").and("be.visible");
96
            .should("exist")
97
            .and("be.visible");
98
103
99
        // Click to open the booking modal
104
        // Click to open the booking modal
100
        cy.get("booking-modal-island .modal").should("exist");
105
        cy.get("booking-modal-island .modal").should("exist");
101
        cy.get("[data-booking-modal]").first().then($btn => $btn[0].click());
106
        cy.get("[data-booking-modal]")
107
            .first()
108
            .then($btn => $btn[0].click());
102
109
103
        // Wait for modal to appear
110
        // Wait for modal to appear
104
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
111
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
Lines 122-134 describe("Booking Modal Basic Tests", () => { Link Here
122
        cy.vueSelectShouldBeDisabled("booking_item_id");
129
        cy.vueSelectShouldBeDisabled("booking_item_id");
123
130
124
        // Period should be disabled initially
131
        // Period should be disabled initially
125
        cy.get("#booking_period")
132
        cy.get("#booking_period").should("exist").and("be.disabled");
126
            .should("exist")
127
            .and("be.disabled");
128
133
129
        // Verify form and submit button exist
134
        // Verify form and submit button exist
130
        cy.get('button[form="form-booking"][type="submit"]')
135
        cy.get('button[form="form-booking"][type="submit"]').should("exist");
131
            .should("exist");
132
136
133
        cy.get(".btn-close").should("exist");
137
        cy.get(".btn-close").should("exist");
134
    });
138
    });
Lines 149-155 describe("Booking Modal Basic Tests", () => { Link Here
149
153
150
        // Open the modal
154
        // Open the modal
151
        cy.get("booking-modal-island .modal").should("exist");
155
        cy.get("booking-modal-island .modal").should("exist");
152
        cy.get("[data-booking-modal]").first().then($btn => $btn[0].click());
156
        cy.get("[data-booking-modal]")
157
            .first()
158
            .then($btn => $btn[0].click());
153
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
159
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
154
            "be.visible"
160
            "be.visible"
155
        );
161
        );
Lines 175-207 describe("Booking Modal Basic Tests", () => { Link Here
175
        cy.vueSelectShouldBeEnabled("pickup_library_id");
181
        cy.vueSelectShouldBeEnabled("pickup_library_id");
176
        cy.vueSelectShouldBeEnabled("booking_itemtype");
182
        cy.vueSelectShouldBeEnabled("booking_itemtype");
177
        cy.vueSelectShouldBeEnabled("booking_item_id");
183
        cy.vueSelectShouldBeEnabled("booking_item_id");
178
        cy.get("#booking_period").should("be.disabled"); // Still disabled until itemtype/item selected
179
180
        // Step 4: Select pickup location
181
        cy.vueSelectByIndex("pickup_library_id", 0);
182
183
        // Step 5: Select item type - this triggers circulation rules API call
184
        cy.vueSelectByIndex("booking_itemtype", 0); // Select first available itemtype
185
184
186
        // Wait for circulation rules API call to complete
185
        // Wait for circulation rules API call to complete
187
        cy.wait("@getCirculationRules");
186
        cy.wait("@getCirculationRules");
188
187
189
        // After itemtype selection and circulation rules load, period should be enabled
188
        // "Any item" is a valid default — period enables without requiring
189
        // a specific item type or item selection
190
        cy.get("#booking_period").should("not.be.disabled");
190
        cy.get("#booking_period").should("not.be.disabled");
191
191
192
        // Step 6: Test clearing item type disables period again (comprehensive workflow)
192
        // Step 4: Select pickup location
193
        cy.vueSelectClear("booking_itemtype");
193
        cy.vueSelectByIndex("pickup_library_id", 0);
194
        cy.get("#booking_period").should("be.disabled");
195
194
196
        // Step 7: Select item instead of itemtype - this also triggers circulation rules
195
        // Step 5: Select item type
197
        cy.vueSelectByIndex("booking_item_id", 1); // Skip "Any item" option
196
        cy.vueSelectByIndex("booking_itemtype", 0);
198
197
199
        // Wait for circulation rules API call (item selection also triggers this)
198
        // Period remains enabled after itemtype selection
200
        cy.wait("@getCirculationRules");
199
        cy.get("#booking_period").should("not.be.disabled");
201
200
202
        // Period should be enabled after item selection and circulation rules load
201
        // Step 6: Clearing item type keeps period enabled ("any item" still valid)
202
        cy.vueSelectClear("booking_itemtype");
203
        cy.get("#booking_period").should("not.be.disabled");
203
        cy.get("#booking_period").should("not.be.disabled");
204
204
205
        // Step 7: Select item instead of itemtype
206
        cy.vueSelectByIndex("booking_item_id", 1);
207
208
        // Period stays enabled after item selection
209
        cy.get("#booking_period").should("not.be.disabled");
205
    });
210
    });
206
211
207
    it("should handle item type and item dependencies correctly", () => {
212
    it("should handle item type and item dependencies correctly", () => {
Lines 220-226 describe("Booking Modal Basic Tests", () => { Link Here
220
225
221
        // Open the modal
226
        // Open the modal
222
        cy.get("booking-modal-island .modal").should("exist");
227
        cy.get("booking-modal-island .modal").should("exist");
223
        cy.get("[data-booking-modal]").first().then($btn => $btn[0].click());
228
        cy.get("[data-booking-modal]")
229
            .first()
230
            .then($btn => $btn[0].click());
224
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
231
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
225
            "be.visible"
232
            "be.visible"
226
        );
233
        );
Lines 327-333 describe("Booking Modal Basic Tests", () => { Link Here
327
334
328
        // Open the modal
335
        // Open the modal
329
        cy.get("booking-modal-island .modal").should("exist");
336
        cy.get("booking-modal-island .modal").should("exist");
330
        cy.get("[data-booking-modal]").first().then($btn => $btn[0].click());
337
        cy.get("[data-booking-modal]")
338
            .first()
339
            .then($btn => $btn[0].click());
331
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
340
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
332
            "be.visible"
341
            "be.visible"
333
        );
342
        );
Lines 348-354 describe("Booking Modal Basic Tests", () => { Link Here
348
357
349
        // Open the modal
358
        // Open the modal
350
        cy.get("booking-modal-island .modal").should("exist");
359
        cy.get("booking-modal-island .modal").should("exist");
351
        cy.get("[data-booking-modal]").first().then($btn => $btn[0].click());
360
        cy.get("[data-booking-modal]")
361
            .first()
362
            .then($btn => $btn[0].click());
352
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
363
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
353
            "be.visible"
364
            "be.visible"
354
        );
365
        );
Lines 416-422 describe("Booking Modal Basic Tests", () => { Link Here
416
427
417
        // Open the modal
428
        // Open the modal
418
        cy.get("booking-modal-island .modal").should("exist");
429
        cy.get("booking-modal-island .modal").should("exist");
419
        cy.get("[data-booking-modal]").first().then($btn => $btn[0].click());
430
        cy.get("[data-booking-modal]")
431
            .first()
432
            .then($btn => $btn[0].click());
420
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
433
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
421
            "be.visible"
434
            "be.visible"
422
        );
435
        );
Lines 505-511 describe("Booking Modal Basic Tests", () => { Link Here
505
518
506
        // Open the modal
519
        // Open the modal
507
        cy.get("booking-modal-island .modal").should("exist");
520
        cy.get("booking-modal-island .modal").should("exist");
508
        cy.get("[data-booking-modal]").first().then($btn => $btn[0].click());
521
        cy.get("[data-booking-modal]")
522
            .first()
523
            .then($btn => $btn[0].click());
509
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
524
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
510
            "be.visible"
525
            "be.visible"
511
        );
526
        );
Lines 562-568 describe("Booking Modal Basic Tests", () => { Link Here
562
577
563
        // Open booking modal
578
        // Open booking modal
564
        cy.get("booking-modal-island .modal").should("exist");
579
        cy.get("booking-modal-island .modal").should("exist");
565
        cy.get("[data-booking-modal]").first().then($btn => $btn[0].click());
580
        cy.get("[data-booking-modal]")
581
            .first()
582
            .then($btn => $btn[0].click());
566
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
583
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
567
            "be.visible"
584
            "be.visible"
568
        );
585
        );
Lines 596-603 describe("Booking Modal Basic Tests", () => { Link Here
596
        cy.get("#booking_period").should($el => {
613
        cy.get("#booking_period").should($el => {
597
            const fp = $el[0]._flatpickr;
614
            const fp = $el[0]._flatpickr;
598
            expect(fp.selectedDates.length).to.eq(2);
615
            expect(fp.selectedDates.length).to.eq(2);
599
            expect(dayjs(fp.selectedDates[0]).format("YYYY-MM-DD")).to.eq(startDate.format("YYYY-MM-DD"));
616
            expect(dayjs(fp.selectedDates[0]).format("YYYY-MM-DD")).to.eq(
600
            expect(dayjs(fp.selectedDates[1]).format("YYYY-MM-DD")).to.eq(endDate.format("YYYY-MM-DD"));
617
                startDate.format("YYYY-MM-DD")
618
            );
619
            expect(dayjs(fp.selectedDates[1]).format("YYYY-MM-DD")).to.eq(
620
                endDate.format("YYYY-MM-DD")
621
            );
601
        });
622
        });
602
623
603
        // Verify the period field is populated
624
        // Verify the period field is populated
Lines 686-695 describe("Booking Modal Basic Tests", () => { Link Here
686
        cy.log("✓ Edit modal opened with pre-populated data");
707
        cy.log("✓ Edit modal opened with pre-populated data");
687
708
688
        // Verify core edit fields are pre-populated
709
        // Verify core edit fields are pre-populated
689
        cy.vueSelectShouldHaveValue(
710
        cy.vueSelectShouldHaveValue("booking_patron", testData.patron.surname);
690
            "booking_patron",
691
            testData.patron.surname
692
        );
693
        cy.log("✓ Patron field pre-populated correctly");
711
        cy.log("✓ Patron field pre-populated correctly");
694
712
695
        // Test that the booking can be retrieved via the real API
713
        // Test that the booking can be retrieved via the real API
Lines 799-805 describe("Booking Modal Basic Tests", () => { Link Here
799
            `/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testData.biblio.biblio_id}`
817
            `/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testData.biblio.biblio_id}`
800
        );
818
        );
801
        cy.get("booking-modal-island .modal").should("exist");
819
        cy.get("booking-modal-island .modal").should("exist");
802
        cy.get("[data-booking-modal]").first().then($btn => $btn[0].click());
820
        cy.get("[data-booking-modal]")
821
            .first()
822
            .then($btn => $btn[0].click());
803
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
823
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
804
            "be.visible"
824
            "be.visible"
805
        );
825
        );
Lines 899-905 describe("Booking Modal Basic Tests", () => { Link Here
899
        );
919
        );
900
920
901
        cy.get("booking-modal-island .modal").should("exist");
921
        cy.get("booking-modal-island .modal").should("exist");
902
        cy.get("[data-booking-modal]").first().then($btn => $btn[0].click());
922
        cy.get("[data-booking-modal]")
923
            .first()
924
            .then($btn => $btn[0].click());
903
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
925
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
904
            "be.visible"
926
            "be.visible"
905
        );
927
        );
Lines 919-925 describe("Booking Modal Basic Tests", () => { Link Here
919
        cy.get("body").should("not.have.class", "modal-open");
941
        cy.get("body").should("not.have.class", "modal-open");
920
942
921
        // Reopen and verify state is reset
943
        // Reopen and verify state is reset
922
        cy.get("[data-booking-modal]").first().then($btn => $btn[0].click());
944
        cy.get("[data-booking-modal]")
945
            .first()
946
            .then($btn => $btn[0].click());
923
        cy.get("booking-modal-island .modal.show", { timeout: 10000 }).should(
947
        cy.get("booking-modal-island .modal.show", { timeout: 10000 }).should(
924
            "be.visible"
948
            "be.visible"
925
        );
949
        );
Lines 960-966 describe("Booking Modal Basic Tests", () => { Link Here
960
        );
984
        );
961
985
962
        cy.get("booking-modal-island .modal").should("exist");
986
        cy.get("booking-modal-island .modal").should("exist");
963
        cy.get("[data-booking-modal]").first().then($btn => $btn[0].click());
987
        cy.get("[data-booking-modal]")
988
            .first()
989
            .then($btn => $btn[0].click());
964
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
990
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
965
            "be.visible"
991
            "be.visible"
966
        );
992
        );
Lines 1007-1013 describe("Booking Modal Basic Tests", () => { Link Here
1007
        );
1033
        );
1008
1034
1009
        cy.get("booking-modal-island .modal").should("exist");
1035
        cy.get("booking-modal-island .modal").should("exist");
1010
        cy.get("[data-booking-modal]").first().then($btn => $btn[0].click());
1036
        cy.get("[data-booking-modal]")
1037
            .first()
1038
            .then($btn => $btn[0].click());
1011
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
1039
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
1012
            "be.visible"
1040
            "be.visible"
1013
        );
1041
        );
Lines 1058-1064 describe("Booking Modal Basic Tests", () => { Link Here
1058
        );
1086
        );
1059
1087
1060
        cy.get("booking-modal-island .modal").should("exist");
1088
        cy.get("booking-modal-island .modal").should("exist");
1061
        cy.get("[data-booking-modal]").first().then($btn => $btn[0].click());
1089
        cy.get("[data-booking-modal]")
1090
            .first()
1091
            .then($btn => $btn[0].click());
1062
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
1092
        cy.get("booking-modal-island .modal", { timeout: 10000 }).should(
1063
            "be.visible"
1093
            "be.visible"
1064
        );
1094
        );
1065
- 

Return to bug 41129