Bugzilla – Attachment 193927 Details for
Bug 41129
Migrate place_booking.js to a Vue island.
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 41129: Clean up unused code and consolidate styles
Bug-41129-Clean-up-unused-code-and-consolidate-sty.patch (text/plain), 70.00 KB, created by
Paul Derscheid
on 2026-02-25 17:20:50 UTC
(
hide
)
Description:
Bug 41129: Clean up unused code and consolidate styles
Filename:
MIME Type:
Creator:
Paul Derscheid
Created:
2026-02-25 17:20:50 UTC
Size:
70.00 KB
patch
obsolete
>From 0aa3eb09d19e6cec35949d0053052af25a8d04b7 Mon Sep 17 00:00:00 2001 >From: Paul Derscheid <paul.derscheid@lmscloud.de> >Date: Wed, 25 Feb 2026 13:21:51 +0100 >Subject: [PATCH] Bug 41129: Clean up unused code and consolidate styles >MIME-Version: 1.0 >Content-Type: text/plain; charset=UTF-8 >Content-Transfer-Encoding: 8bit > >Code: >- Remove uncalled SweepLineProcessor methods (~155 lines) >- Remove unused constants, exports, and wrapper functions >- Remove unused unavailableByDateRef from useAvailability >- Simplify fixTargetEndDateAvailability wrapper in highlighting > >CSS: >- Unify two :root blocks and two <style> blocks in BookingModal.vue >- Centralise marker dot color variants in BookingModal.vue; remove > duplicates from BookingPeriodStep and BookingTooltip >- Replace hardcoded #28a745 with --booking-constraint-marker variable > >Tests: >- Add test cases for modal state reset, zero-day circulation rule > warning, and error handling (409 conflict, 500 server error) >- Remove unused flatpickr helper commands and assertion functions > >Misc: >- Scope _feedbackHideTimer per createOnDayCreate instance >- Document catch blocks in staff-interface.js >- Upgrade debug console.log to console.warn in list.tt > >Sponsored-by: Büchereizentrale Schleswig-Holstein <https://www.bz-sh.de/> >--- > .../prog/en/modules/bookings/list.tt | 4 +- > .../vue/components/Bookings/BookingModal.vue | 67 ++-- > .../components/Bookings/BookingPeriodStep.vue | 43 +-- > .../components/Bookings/BookingTooltip.vue | 27 +- > .../Bookings/composables/useAvailability.mjs | 5 +- > .../lib/adapters/api/staff-interface.js | 12 +- > .../Bookings/lib/adapters/calendar/events.mjs | 84 ++--- > .../lib/adapters/calendar/highlighting.mjs | 19 +- > .../algorithms/sweep-line-processor.mjs | 166 --------- > .../Bookings/lib/booking/constants.mjs | 4 - > .../prog/js/vue/utils/functions.mjs | 20 -- > .../Circulation/bookingsModalBasic_spec.ts | 198 +++++++++++ > .../bookingsModalDatePicker_spec.ts | 322 ++++++++++-------- > t/cypress/support/flatpickr.js | 204 ++--------- > 14 files changed, 490 insertions(+), 685 deletions(-) > >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/bookings/list.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/bookings/list.tt >index 1b4a07ca33b..24b355d707f 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/bookings/list.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/bookings/list.tt >@@ -66,7 +66,7 @@ > [% Asset.js("js/combobox.js") | $raw %] > [% Asset.js("js/additional-filters.js") | $raw %] > <script> >- var cancel_success = 0; >+ var cancel_success = 0; > var bookings_table; > var timeline; > let biblionumber = "[% biblionumber | uri %]"; >@@ -205,7 +205,7 @@ > timeline = new vis.Timeline(container, bookingsSet, itemsSet, options); > }, > function (jqXHR, textStatus, errorThrown) { >- console.log("Fetch failed"); >+ console.warn("Timeline data fetch failed:", textStatus, errorThrown); > } > ); > >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/BookingModal.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/BookingModal.vue >index 3158577372e..a31e4fba0cf 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/BookingModal.vue >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/BookingModal.vue >@@ -767,7 +767,7 @@ onUnmounted(() => { > </script> > > <style> >-/* Global variables for external libraries (flatpickr) and cross-block usage */ >+/* Design System: CSS Custom Properties */ > :root { > /* Success colors for constraint highlighting */ > --booking-success-hue: 134; >@@ -776,62 +776,54 @@ onUnmounted(() => { > --booking-success-border: hsl(var(--booking-success-hue), 70%, 40%); > --booking-success-border-hover: hsl(var(--booking-success-hue), 75%, 30%); > --booking-success-text: hsl(var(--booking-success-hue), 80%, 20%); >+ --booking-constraint-marker: hsl(var(--booking-success-hue), 61%, 41%); > >- /* Border width used by flatpickr */ >+ /* Border width */ > --booking-border-width: 1px; > >- /* Variables used by second style block (booking markers, calendar states) */ >+ /* Booking markers */ > --booking-marker-size: max(4px, 0.25em); > --booking-marker-grid-gap: 0.25rem; > --booking-marker-grid-offset: -0.75rem; > >- /* Color hues used in second style block */ >+ /* Color hues */ > --booking-warning-hue: 45; > --booking-danger-hue: 354; > --booking-info-hue: 195; > --booking-neutral-hue: 210; > --booking-holiday-hue: 0; > >- /* Colors derived from hues (used in second style block) */ >+ /* Colors derived from hues */ > --booking-warning-bg: hsl(var(--booking-warning-hue), 100%, 85%); >- --booking-neutral-600: hsl(var(--booking-neutral-hue), 10%, 45%); >- --booking-holiday-bg: hsl(var(--booking-holiday-hue), 0%, 85%); >- --booking-holiday-text: hsl(var(--booking-holiday-hue), 0%, 40%); >- >- /* Spacing used in second style block */ >- --booking-space-xs: 0.125rem; >- >- /* Typography used in second style block */ >- --booking-text-xs: 0.7rem; >- >- /* Border radius used in second style block and other components */ >- --booking-border-radius-sm: 0.25rem; >- --booking-border-radius-md: 0.5rem; >- --booking-border-radius-full: 50%; >-} >- >-/* Design System: CSS Custom Properties (First Style Block Only) */ >-:root { >- /* Colors not used in second style block */ > --booking-warning-bg-hover: hsl(var(--booking-warning-hue), 100%, 70%); > --booking-neutral-100: hsl(var(--booking-neutral-hue), 15%, 92%); > --booking-neutral-300: hsl(var(--booking-neutral-hue), 15%, 75%); > --booking-neutral-500: hsl(var(--booking-neutral-hue), 10%, 55%); >+ --booking-neutral-600: hsl(var(--booking-neutral-hue), 10%, 45%); >+ --booking-holiday-bg: hsl(var(--booking-holiday-hue), 0%, 85%); >+ --booking-holiday-text: hsl(var(--booking-holiday-hue), 0%, 40%); > >- /* Spacing Scale (first block only) */ >+ /* Spacing Scale */ >+ --booking-space-xs: 0.125rem; > --booking-space-sm: 0.25rem; /* 4px */ > --booking-space-md: 0.5rem; /* 8px */ > --booking-space-lg: 1rem; /* 16px */ > --booking-space-xl: 1.5rem; /* 24px */ > --booking-space-2xl: 2rem; /* 32px */ > >- /* Typography Scale (first block only) */ >+ /* Typography Scale */ >+ --booking-text-xs: 0.7rem; > --booking-text-sm: 0.8125rem; > --booking-text-base: 1rem; > --booking-text-lg: 1.1rem; > --booking-text-xl: 1.3rem; > --booking-text-2xl: 2rem; > >+ /* Border radius */ >+ --booking-border-radius-sm: 0.25rem; >+ --booking-border-radius-md: 0.5rem; >+ --booking-border-radius-full: 50%; >+ > /* Layout */ > --booking-modal-max-height: calc(100vh - var(--booking-space-2xl)); > --booking-input-min-width: 15rem; >@@ -939,25 +931,8 @@ hr { > margin-left: var(--booking-space-lg); > } > >-/* Legend colors match actual calendar markers exactly */ >-.calendar-legend .booking-marker-dot--booked { >- background: var(--booking-warning-bg) !important; >-} >- >-.calendar-legend .booking-marker-dot--checked-out { >- background: hsl(var(--booking-danger-hue), 60%, 85%) !important; >-} >- >-.calendar-legend .booking-marker-dot--lead { >- background: hsl(var(--booking-info-hue), 60%, 85%) !important; >-} >+/* === Date Picker & Marker System === */ > >-.calendar-legend .booking-marker-dot--trail { >- background: var(--booking-warning-bg) !important; >-} >-</style> >- >-<style> > .booking-date-picker { > display: flex; > align-items: stretch; >@@ -1053,6 +1028,10 @@ hr { > background: var(--booking-holiday-bg); > } > >+.booking-marker-dot--constraint { >+ background: var(--booking-constraint-marker); >+} >+ > /* Calendar Day States */ > .booked { > background: var(--booking-warning-bg) !important; >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/BookingPeriodStep.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/BookingPeriodStep.vue >index 2c8f1807fe0..f6c0342bbf0 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/BookingPeriodStep.vue >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/BookingPeriodStep.vue >@@ -68,8 +68,7 @@ > {{ $__("Closed") }} > <span > v-if="dateRangeConstraint && hasSelectedDates" >- class="booking-marker-dot ml-3" >- style="background-color: #28a745" >+ class="booking-marker-dot booking-marker-dot--constraint ml-3" > ></span> > <span v-if="dateRangeConstraint && hasSelectedDates" class="ml-1"> > {{ $__("Required end date") }} >@@ -167,9 +166,9 @@ const constraintHelpText = computed((): string => { > ).format(props.maxBookingPeriod) > : $__("Booking period limited to checkout length with renewals"), > default: props.maxBookingPeriod >- ? $__("Booking period limited by circulation rules (%s days)").format( >- props.maxBookingPeriod >- ) >+ ? $__( >+ "Booking period limited by circulation rules (%s days)" >+ ).format(props.maxBookingPeriod) > : $__("Booking period limited by circulation rules"), > }; > >@@ -222,7 +221,7 @@ const { clear } = useFlatpickr(inputEl as { value: HTMLInputElement | null }, { > > watch( > () => availability.value?.unavailableByDate, >- (map) => { >+ map => { > try { > store.setUnavailableByDate(map ?? {}); > } catch (e) { >@@ -245,7 +244,7 @@ const debouncedExtendHolidays = debounce( > > watch( > () => visibleRangeRef.value, >- (newRange) => { >+ newRange => { > const libraryId = pickupLibraryId.value; > if ( > !libraryId || >@@ -302,35 +301,6 @@ const clearDateRange = (): void => { > margin-top: var(--booking-space-lg); > } > >-.booking-marker-dot { >- display: inline-block; >- width: calc(var(--booking-marker-size) * 3); >- height: calc(var(--booking-marker-size) * 3); >- border-radius: var(--booking-border-radius-full); >- margin-right: var(--booking-space-sm); >- border: var(--booking-border-width) solid hsla(0, 0%, 0%, 0.15); >-} >- >-.booking-marker-dot--booked { >- background-color: var(--booking-warning-bg); >-} >- >-.booking-marker-dot--lead { >- background-color: hsl(var(--booking-info-hue), 60%, 85%); >-} >- >-.booking-marker-dot--trail { >- background-color: var(--booking-warning-bg); >-} >- >-.booking-marker-dot--checked-out { >- background-color: hsl(var(--booking-danger-hue), 60%, 85%); >-} >- >-.booking-marker-dot--holiday { >- background-color: var(--booking-holiday-bg); >-} >- > .alert { > padding: calc(var(--booking-space-lg) * 0.75) var(--booking-space-lg); > border: var(--booking-border-width) solid transparent; >@@ -348,5 +318,4 @@ const clearDateRange = (): void => { > background-color: hsl(var(--booking-danger-hue), 40%, 90%); > border-color: hsl(var(--booking-danger-hue), 40%, 70%); > } >- > </style> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/BookingTooltip.vue b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/BookingTooltip.vue >index b3d615769fa..5dacd4bc36e 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/BookingTooltip.vue >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/BookingTooltip.vue >@@ -31,7 +31,6 @@ > </template> > > <script setup lang="ts"> >-import { defineProps, withDefaults } from "vue"; > import { $__ } from "../../i18n"; > import { getMarkerTypeLabel } from "./lib/ui/marker-labels.mjs"; > import type { CalendarMarker } from "./types/bookings"; >@@ -56,9 +55,11 @@ withDefaults( > .booking-tooltip { > background: hsl(var(--booking-warning-hue), 100%, 95%); > color: hsl(var(--booking-neutral-hue), 20%, 20%); >- border: var(--booking-border-width) solid hsl(var(--booking-neutral-hue), 15%, 75%); >+ border: var(--booking-border-width) solid >+ hsl(var(--booking-neutral-hue), 15%, 75%); > border-radius: var(--booking-border-radius-md); >- box-shadow: 0 0.125rem 0.5rem hsla(var(--booking-neutral-hue), 10%, 0%, 0.08); >+ box-shadow: 0 0.125rem 0.5rem >+ hsla(var(--booking-neutral-hue), 10%, 0%, 0.08); > padding: calc(var(--booking-space-xs) * 3) calc(var(--booking-space-xs) * 5); > font-size: var(--booking-text-lg); > pointer-events: none; >@@ -72,24 +73,4 @@ withDefaults( > margin: 0 var(--booking-space-xs) 0 0; > vertical-align: middle; > } >- >-.booking-marker-dot--booked { >- background: var(--booking-warning-bg); >-} >- >-.booking-marker-dot--checked-out { >- background: hsl(var(--booking-danger-hue), 60%, 85%); >-} >- >-.booking-marker-dot--lead { >- background: hsl(var(--booking-info-hue), 60%, 85%); >-} >- >-.booking-marker-dot--trail { >- background: var(--booking-warning-bg); >-} >- >-.booking-marker-dot--holiday { >- background: var(--booking-holiday-bg); >-} > </style> >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/composables/useAvailability.mjs b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/composables/useAvailability.mjs >index ae60ee8de64..2671b841add 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/composables/useAvailability.mjs >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/composables/useAvailability.mjs >@@ -87,9 +87,6 @@ export function useAvailability(storeRefs, optionsRef) { > const disableFnRef = computed( > () => availability.value.disable || (() => false) > ); >- const unavailableByDateRef = computed( >- () => availability.value.unavailableByDate || {} >- ); > >- return { availability, disableFnRef, unavailableByDateRef }; >+ return { availability, disableFnRef }; > } >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/adapters/api/staff-interface.js b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/adapters/api/staff-interface.js >index 2c97e2a450e..226fb74df87 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/adapters/api/staff-interface.js >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/adapters/api/staff-interface.js >@@ -161,7 +161,9 @@ export async function fetchPatrons(term, page = 1) { > if (errorData.error) { > error.message += ` - ${errorData.error}`; > } >- } catch (e) {} >+ } catch (_) { >+ /* response body may not be JSON */ >+ } > > throw error; > } >@@ -349,7 +351,9 @@ export async function createBooking(bookingData) { > if (errorData.error) { > errorMessage += ` - ${errorData.error}`; > } >- } catch (e) {} >+ } catch (_) { >+ /* response body may not be JSON */ >+ } > /** @type {Error & { status?: number }} */ > const error = Object.assign(new Error(errorMessage), { > status: response.status, >@@ -405,7 +409,9 @@ export async function updateBooking(bookingId, bookingData) { > if (errorData.error) { > errorMessage += ` - ${errorData.error}`; > } >- } catch (e) {} >+ } catch (_) { >+ /* response body may not be JSON */ >+ } > /** @type {Error & { status?: number }} */ > const error = Object.assign(new Error(errorMessage), { > status: response.status, >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/adapters/calendar/events.mjs b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/adapters/calendar/events.mjs >index 28b5171a57d..7bcfe000c69 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/adapters/calendar/events.mjs >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/adapters/calendar/events.mjs >@@ -413,48 +413,6 @@ function ensureFeedbackBar(fp) { > return /** @type {HTMLDivElement} */ (bar); > } > >-/** @type {number|null} */ >-let _feedbackHideTimer = null; >- >-/** >- * Update the feedback bar inside the flatpickr calendar container. >- * Hides are deferred so that rapid mouseoutâmouseover between adjacent >- * days doesn't trigger a visible flicker. >- * >- * @param {HTMLDivElement} bar >- * @param {{ message: string, variant: string } | null} feedback >- */ >-function updateFeedbackBar(bar, feedback) { >- if (!feedback) { >- if (_feedbackHideTimer == null) { >- _feedbackHideTimer = setTimeout(() => { >- _feedbackHideTimer = null; >- bar.classList.remove( >- "booking-hover-feedback--visible", >- "booking-hover-feedback--info", >- "booking-hover-feedback--warning", >- "booking-hover-feedback--danger" >- ); >- }, 16); >- } >- return; >- } >- if (_feedbackHideTimer != null) { >- clearTimeout(_feedbackHideTimer); >- _feedbackHideTimer = null; >- } >- bar.textContent = feedback.message; >- bar.classList.remove( >- "booking-hover-feedback--info", >- "booking-hover-feedback--warning", >- "booking-hover-feedback--danger" >- ); >- bar.classList.add( >- "booking-hover-feedback--visible", >- `booking-hover-feedback--${feedback.variant}` >- ); >-} >- > /** > * Create Flatpickr `onDayCreate` handler. > * >@@ -478,6 +436,48 @@ export function createOnDayCreate( > tooltipX, > tooltipY > ) { >+ /** @type {number|null} */ >+ let _feedbackHideTimer = null; >+ >+ /** >+ * Update the feedback bar inside the flatpickr calendar container. >+ * Hides are deferred so that rapid mouseoutâmouseover between adjacent >+ * days doesn't trigger a visible flicker. >+ * >+ * @param {HTMLDivElement} bar >+ * @param {{ message: string, variant: string } | null} feedback >+ */ >+ function updateFeedbackBar(bar, feedback) { >+ if (!feedback) { >+ if (_feedbackHideTimer == null) { >+ _feedbackHideTimer = setTimeout(() => { >+ _feedbackHideTimer = null; >+ bar.classList.remove( >+ "booking-hover-feedback--visible", >+ "booking-hover-feedback--info", >+ "booking-hover-feedback--warning", >+ "booking-hover-feedback--danger" >+ ); >+ }, 16); >+ } >+ return; >+ } >+ if (_feedbackHideTimer != null) { >+ clearTimeout(_feedbackHideTimer); >+ _feedbackHideTimer = null; >+ } >+ bar.textContent = feedback.message; >+ bar.classList.remove( >+ "booking-hover-feedback--info", >+ "booking-hover-feedback--warning", >+ "booking-hover-feedback--danger" >+ ); >+ bar.classList.add( >+ "booking-hover-feedback--visible", >+ `booking-hover-feedback--${feedback.variant}` >+ ); >+ } >+ > return function ( > ...[ > , >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/adapters/calendar/highlighting.mjs b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/adapters/calendar/highlighting.mjs >index 18057b51255..3cf6d744261 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/adapters/calendar/highlighting.mjs >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/adapters/calendar/highlighting.mjs >@@ -122,19 +122,6 @@ export function fixDateAvailability( > } > } > >-/** >- * Fix incorrect target-end unavailability via a CSS-based override. >- * Wrapper for backward compatibility. >- * >- * @param {import('flatpickr/dist/types/instance').Instance} _instance >- * @param {NodeListOf<Element>|Element[]} dayElements >- * @param {Date} targetEndDate >- * @returns {void} >- */ >-function fixTargetEndDateAvailability(_instance, dayElements, targetEndDate) { >- fixDateAvailability(dayElements, targetEndDate, "target end date"); >-} >- > /** > * Apply constraint highlighting to the Flatpickr calendar. > * >@@ -259,10 +246,10 @@ export function applyCalendarHighlighting(instance, highlightingData) { > > if (highlightingData.constraintMode === CONSTRAINT_MODE_END_DATE_ONLY) { > applyClickPrevention(instance); >- fixTargetEndDateAvailability( >- instance, >+ fixDateAvailability( > dayElements, >- highlightingData.targetEndDate >+ highlightingData.targetEndDate, >+ "target end date" > ); > > const targetEndElem = Array.from(dayElements).find( >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/booking/algorithms/sweep-line-processor.mjs b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/booking/algorithms/sweep-line-processor.mjs >index 805f1ae125c..476a5b6c812 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/booking/algorithms/sweep-line-processor.mjs >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/booking/algorithms/sweep-line-processor.mjs >@@ -6,7 +6,6 @@ > */ > > import { BookingDate, startOfDayTs, endOfDayTs } from "../BookingDate.mjs"; >-import { MAX_SEARCH_DAYS } from "../constants.mjs"; > > /** > * Event types for the sweep line algorithm >@@ -161,169 +160,4 @@ export class SweepLineProcessor { > > return unavailableByDate; > } >- >- /** >- * Process intervals and return aggregated statistics >- * @param {Array} intervals >- * @param {Date|import("dayjs").Dayjs} viewStart >- * @param {Date|import("dayjs").Dayjs} viewEnd >- * @returns {Object} Statistics about the date range >- */ >- getDateRangeStatistics(intervals, viewStart, viewEnd) { >- const stats = { >- totalDays: 0, >- daysWithBookings: 0, >- daysWithCheckouts: 0, >- fullyBookedDays: 0, >- peakBookingCount: 0, >- peakDate: null, >- itemUtilization: new Map(), >- }; >- >- const startDate = BookingDate.from(viewStart).toDayjs(); >- const endDate = BookingDate.from(viewEnd, { preserveTime: true }).toDayjs().endOf("day"); >- >- stats.totalDays = endDate.diff(startDate, "day") + 1; >- >- for ( >- let date = startDate; >- date.isSameOrBefore(endDate, "day"); >- date = date.add(1, "day") >- ) { >- const dayStart = date.valueOf(); >- const dayEnd = date.endOf("day").valueOf(); >- >- let bookingCount = 0; >- let checkoutCount = 0; >- const itemsInUse = new Set(); >- >- intervals.forEach(interval => { >- if (interval.start <= dayEnd && interval.end >= dayStart) { >- if (interval.type === "booking") { >- bookingCount++; >- itemsInUse.add(interval.itemId); >- } else if (interval.type === "checkout") { >- checkoutCount++; >- itemsInUse.add(interval.itemId); >- } >- } >- }); >- >- if (bookingCount > 0) stats.daysWithBookings++; >- if (checkoutCount > 0) stats.daysWithCheckouts++; >- >- const totalCount = bookingCount + checkoutCount; >- if (totalCount > stats.peakBookingCount) { >- stats.peakBookingCount = totalCount; >- stats.peakDate = date.format("YYYY-MM-DD"); >- } >- >- itemsInUse.forEach(itemId => { >- if (!stats.itemUtilization.has(itemId)) { >- stats.itemUtilization.set(itemId, 0); >- } >- stats.itemUtilization.set( >- itemId, >- stats.itemUtilization.get(itemId) + 1 >- ); >- }); >- } >- >- return stats; >- } >- >- /** >- * Find the next available date for a specific item >- * @param {Array} intervals >- * @param {string} itemId >- * @param {Date|import('dayjs').Dayjs} startDate >- * @param {number} maxDaysToSearch >- * @returns {Date|null} >- */ >- findNextAvailableDate( >- intervals, >- itemId, >- startDate, >- maxDaysToSearch = MAX_SEARCH_DAYS >- ) { >- const start = BookingDate.from(startDate).toDayjs(); >- const itemIntervals = intervals.filter( >- interval => interval.itemId === itemId >- ); >- >- itemIntervals.sort((a, b) => a.start - b.start); >- >- for (let i = 0; i < maxDaysToSearch; i++) { >- const checkDate = start.add(i, "day"); >- const dateStart = checkDate.valueOf(); >- const dateEnd = checkDate.endOf("day").valueOf(); >- >- const isAvailable = !itemIntervals.some( >- interval => >- interval.start <= dateEnd && interval.end >= dateStart >- ); >- >- if (isAvailable) { >- return checkDate.toDate(); >- } >- } >- >- return null; >- } >- >- /** >- * Find gaps (available periods) for an item >- * @param {Array} intervals >- * @param {string} itemId >- * @param {Date|import('dayjs').Dayjs} viewStart >- * @param {Date|import('dayjs').Dayjs} viewEnd >- * @param {number} minGapDays - Minimum gap size to report >- * @returns {Array<{start: Date, end: Date, days: number}>} >- */ >- findAvailableGaps(intervals, itemId, viewStart, viewEnd, minGapDays = 1) { >- const gaps = []; >- const itemIntervals = intervals >- .filter(interval => interval.itemId === itemId) >- .sort((a, b) => a.start - b.start); >- >- const rangeStart = BookingDate.from(viewStart).valueOf(); >- const rangeEnd = BookingDate.from(viewEnd, { preserveTime: true }).toDayjs().endOf("day").valueOf(); >- >- let lastEnd = rangeStart; >- >- itemIntervals.forEach(interval => { >- if (interval.end < rangeStart || interval.start > rangeEnd) { >- return; >- } >- >- const gapStart = Math.max(lastEnd, rangeStart); >- const gapEnd = Math.min(interval.start, rangeEnd); >- >- if (gapEnd > gapStart) { >- const gapDays = BookingDate.from(gapEnd).diff(BookingDate.from(gapStart), "day"); >- if (gapDays >= minGapDays) { >- gaps.push({ >- start: new Date(gapStart), >- end: new Date(gapEnd - 1), // End of previous day >- days: gapDays, >- }); >- } >- } >- >- lastEnd = Math.max(lastEnd, interval.end + 1); // Start of next day >- }); >- >- if (lastEnd < rangeEnd) { >- const gapDays = BookingDate.from(rangeEnd).diff(BookingDate.from(lastEnd), "day"); >- if (gapDays >= minGapDays) { >- gaps.push({ >- start: new Date(lastEnd), >- end: new Date(rangeEnd), >- days: gapDays, >- }); >- } >- } >- >- return gaps; >- } > } >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/booking/constants.mjs b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/booking/constants.mjs >index 979b83b0463..474151c5f2b 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/booking/constants.mjs >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/components/Bookings/lib/booking/constants.mjs >@@ -7,10 +7,6 @@ > export const CONSTRAINT_MODE_END_DATE_ONLY = "end_date_only"; > export const CONSTRAINT_MODE_NORMAL = "normal"; > >-// Selection semantics (logging, diagnostics) >-export const SELECTION_ANY_AVAILABLE = "ANY_AVAILABLE"; >-export const SELECTION_SPECIFIC_ITEM = "SPECIFIC_ITEM"; >- > // UI class names (used across calendar/adapters/composables) > export const CLASS_BOOKING_CONSTRAINED_RANGE_MARKER = > "booking-constrained-range-marker"; >diff --git a/koha-tmpl/intranet-tmpl/prog/js/vue/utils/functions.mjs b/koha-tmpl/intranet-tmpl/prog/js/vue/utils/functions.mjs >index 9f0cb3fea5e..3b8ec913fb6 100644 >--- a/koha-tmpl/intranet-tmpl/prog/js/vue/utils/functions.mjs >+++ b/koha-tmpl/intranet-tmpl/prog/js/vue/utils/functions.mjs >@@ -18,23 +18,3 @@ export function debounce(fn, delay) { > timeout = setTimeout(() => fn.apply(this, args), delay); > }; > } >- >-/** >- * Creates a throttled version of a function that only invokes >- * at most once per `limit` milliseconds. >- * >- * @template {(...args: any[]) => any} T >- * @param {T} fn - The function to throttle >- * @param {number} limit - Minimum time between invocations in milliseconds >- * @returns {(...args: Parameters<T>) => void} >- */ >-export function throttle(fn, limit) { >- let inThrottle; >- return function (...args) { >- if (!inThrottle) { >- fn.apply(this, args); >- inThrottle = true; >- setTimeout(() => (inThrottle = false), limit); >- } >- }; >-} >diff --git a/t/cypress/integration/Circulation/bookingsModalBasic_spec.ts b/t/cypress/integration/Circulation/bookingsModalBasic_spec.ts >index 6a1eb3b6db6..ceb370e3b45 100644 >--- a/t/cypress/integration/Circulation/bookingsModalBasic_spec.ts >+++ b/t/cypress/integration/Circulation/bookingsModalBasic_spec.ts >@@ -892,4 +892,202 @@ describe("Booking Modal Basic Tests", () => { > "â CONFIRMED: Error handling and recovery workflow working correctly" > ); > }); >+ >+ it("should reset modal state after canceling", () => { >+ cy.visit( >+ `/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testData.biblio.biblio_id}` >+ ); >+ >+ cy.get("booking-modal-island .modal").should("exist"); >+ cy.get("[data-booking-modal]").first().then($btn => $btn[0].click()); >+ cy.get("booking-modal-island .modal", { timeout: 10000 }).should( >+ "be.visible" >+ ); >+ >+ // Fill some fields >+ cy.vueSelect( >+ "booking_patron", >+ testData.patron.cardnumber, >+ `${testData.patron.surname} ${testData.patron.firstname}` >+ ); >+ cy.vueSelectShouldBeEnabled("pickup_library_id"); >+ cy.vueSelectByIndex("pickup_library_id", 0); >+ >+ // Close modal and wait for Bootstrap transition to fully complete >+ cy.get("booking-modal-island .modal .btn-close").first().click(); >+ cy.get("booking-modal-island .modal").should("not.be.visible"); >+ cy.get("body").should("not.have.class", "modal-open"); >+ >+ // Reopen and verify state is reset >+ cy.get("[data-booking-modal]").first().then($btn => $btn[0].click()); >+ cy.get("booking-modal-island .modal.show", { timeout: 10000 }).should( >+ "be.visible" >+ ); >+ >+ cy.vueSelectShouldBeEnabled("booking_patron"); >+ cy.vueSelectShouldBeDisabled("pickup_library_id"); >+ cy.vueSelectShouldBeDisabled("booking_itemtype"); >+ cy.vueSelectShouldBeDisabled("booking_item_id"); >+ cy.get("#booking_period").should("be.disabled"); >+ cy.get('button[form="form-booking"][type="submit"]').should( >+ "be.disabled" >+ ); >+ }); >+ >+ it("should show capacity warning for zero-day circulation rules", () => { >+ cy.intercept( >+ "GET", >+ `/api/v1/biblios/${testData.biblio.biblio_id}/pickup_locations*` >+ ).as("getPickupLocations"); >+ cy.intercept("GET", "/api/v1/circulation_rules*", { >+ body: [ >+ { >+ library_id: testData.libraries[0].library_id, >+ item_type_id: "BK", >+ patron_category_id: testData.patron.category_id, >+ issuelength: 0, >+ renewalsallowed: 0, >+ renewalperiod: 0, >+ bookings_lead_period: 0, >+ bookings_trail_period: 0, >+ calculated_period_days: 0, >+ }, >+ ], >+ }).as("getCirculationRules"); >+ >+ cy.visit( >+ `/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testData.biblio.biblio_id}` >+ ); >+ >+ cy.get("booking-modal-island .modal").should("exist"); >+ cy.get("[data-booking-modal]").first().then($btn => $btn[0].click()); >+ cy.get("booking-modal-island .modal", { timeout: 10000 }).should( >+ "be.visible" >+ ); >+ >+ cy.vueSelect( >+ "booking_patron", >+ testData.patron.cardnumber, >+ `${testData.patron.surname} ${testData.patron.firstname}` >+ ); >+ cy.wait("@getPickupLocations"); >+ >+ cy.vueSelectShouldBeEnabled("pickup_library_id"); >+ cy.vueSelectByIndex("pickup_library_id", 0); >+ >+ cy.vueSelectShouldBeEnabled("booking_item_id"); >+ cy.vueSelectByIndex("booking_item_id", 1); >+ cy.wait("@getCirculationRules"); >+ >+ cy.get("booking-modal-island .modal .alert-warning") >+ .scrollIntoView() >+ .should("be.visible") >+ .and("contain", "Bookings are not permitted"); >+ cy.get("#booking_period").should("be.disabled"); >+ cy.get('button[form="form-booking"][type="submit"]').should( >+ "be.disabled" >+ ); >+ }); >+ >+ it("should show error on 409 conflict response", () => { >+ cy.intercept( >+ "GET", >+ `/api/v1/biblios/${testData.biblio.biblio_id}/pickup_locations*` >+ ).as("getPickupLocations"); >+ cy.intercept("GET", "/api/v1/circulation_rules*").as( >+ "getCirculationRules" >+ ); >+ cy.intercept("POST", "/api/v1/bookings", { >+ statusCode: 409, >+ body: { error: "Booking conflict detected" }, >+ }).as("conflictBooking"); >+ >+ cy.visit( >+ `/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testData.biblio.biblio_id}` >+ ); >+ >+ cy.get("booking-modal-island .modal").should("exist"); >+ cy.get("[data-booking-modal]").first().then($btn => $btn[0].click()); >+ cy.get("booking-modal-island .modal", { timeout: 10000 }).should( >+ "be.visible" >+ ); >+ >+ cy.vueSelect( >+ "booking_patron", >+ testData.patron.cardnumber, >+ `${testData.patron.surname} ${testData.patron.firstname}` >+ ); >+ cy.wait("@getPickupLocations"); >+ >+ cy.vueSelectShouldBeEnabled("pickup_library_id"); >+ cy.vueSelectByIndex("pickup_library_id", 0); >+ >+ cy.vueSelectShouldBeEnabled("booking_item_id"); >+ cy.vueSelectByIndex("booking_item_id", 1); >+ cy.wait("@getCirculationRules"); >+ >+ cy.get("#booking_period").should("not.be.disabled"); >+ const startDate = dayjs().add(5, "day"); >+ const endDate = dayjs().add(10, "day"); >+ cy.get("#booking_period").selectFlatpickrDateRange(startDate, endDate); >+ >+ cy.get('button[form="form-booking"][type="submit"]') >+ .should("not.be.disabled") >+ .click(); >+ cy.wait("@conflictBooking"); >+ >+ cy.get("booking-modal-island .modal .alert-danger").should("exist"); >+ cy.get("booking-modal-island .modal").should("be.visible"); >+ }); >+ >+ it("should show error on 500 server error response", () => { >+ cy.intercept( >+ "GET", >+ `/api/v1/biblios/${testData.biblio.biblio_id}/pickup_locations*` >+ ).as("getPickupLocations"); >+ cy.intercept("GET", "/api/v1/circulation_rules*").as( >+ "getCirculationRules" >+ ); >+ cy.intercept("POST", "/api/v1/bookings", { >+ statusCode: 500, >+ body: { error: "Internal server error" }, >+ }).as("serverError"); >+ >+ cy.visit( >+ `/cgi-bin/koha/catalogue/detail.pl?biblionumber=${testData.biblio.biblio_id}` >+ ); >+ >+ cy.get("booking-modal-island .modal").should("exist"); >+ cy.get("[data-booking-modal]").first().then($btn => $btn[0].click()); >+ cy.get("booking-modal-island .modal", { timeout: 10000 }).should( >+ "be.visible" >+ ); >+ >+ cy.vueSelect( >+ "booking_patron", >+ testData.patron.cardnumber, >+ `${testData.patron.surname} ${testData.patron.firstname}` >+ ); >+ cy.wait("@getPickupLocations"); >+ >+ cy.vueSelectShouldBeEnabled("pickup_library_id"); >+ cy.vueSelectByIndex("pickup_library_id", 0); >+ >+ cy.vueSelectShouldBeEnabled("booking_item_id"); >+ cy.vueSelectByIndex("booking_item_id", 1); >+ cy.wait("@getCirculationRules"); >+ >+ cy.get("#booking_period").should("not.be.disabled"); >+ const startDate = dayjs().add(5, "day"); >+ const endDate = dayjs().add(10, "day"); >+ cy.get("#booking_period").selectFlatpickrDateRange(startDate, endDate); >+ >+ cy.get('button[form="form-booking"][type="submit"]') >+ .should("not.be.disabled") >+ .click(); >+ cy.wait("@serverError"); >+ >+ cy.get("booking-modal-island .modal .alert-danger").should("exist"); >+ cy.get("booking-modal-island .modal").should("be.visible"); >+ }); > }); >diff --git a/t/cypress/integration/Circulation/bookingsModalDatePicker_spec.ts b/t/cypress/integration/Circulation/bookingsModalDatePicker_spec.ts >index c1eb23a0999..2b7cc1a33af 100644 >--- a/t/cypress/integration/Circulation/bookingsModalDatePicker_spec.ts >+++ b/t/cypress/integration/Circulation/bookingsModalDatePicker_spec.ts >@@ -27,13 +27,17 @@ describe("Booking Modal Date Picker Tests", () => { > testData = objects; > > // Update items to be bookable with predictable itemtypes >- return cy.task("query", { >- sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = 'A', dateaccessioned = '2024-12-03' WHERE itemnumber = ?", >- values: [objects.items[0].item_id], >- }).then(() => cy.task("query", { >- sql: "UPDATE items SET bookable = 1, itype = 'CF', homebranch = 'CPL', enumchron = 'B', dateaccessioned = '2024-12-02' WHERE itemnumber = ?", >- values: [objects.items[1].item_id], >- })); >+ return cy >+ .task("query", { >+ sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = 'A', dateaccessioned = '2024-12-03' WHERE itemnumber = ?", >+ values: [objects.items[0].item_id], >+ }) >+ .then(() => >+ cy.task("query", { >+ sql: "UPDATE items SET bookable = 1, itype = 'CF', homebranch = 'CPL', enumchron = 'B', dateaccessioned = '2024-12-02' WHERE itemnumber = ?", >+ values: [objects.items[1].item_id], >+ }) >+ ); > }) > .then(() => { > // Create a test patron using upstream pattern >@@ -98,7 +102,9 @@ describe("Booking Modal Date Picker Tests", () => { > > // Open the modal > cy.get("booking-modal-island .modal").should("exist"); >- cy.get("[data-booking-modal]").first().then($btn => $btn[0].click()); >+ cy.get("[data-booking-modal]") >+ .first() >+ .then($btn => $btn[0].click()); > cy.get("booking-modal-island .modal", { timeout: 10000 }).should( > "be.visible" > ); >@@ -195,7 +201,11 @@ describe("Booking Modal Date Picker Tests", () => { > > // Select the specific item that has the existing bookings (by barcode, not index) > cy.vueSelectShouldBeEnabled("booking_item_id"); >- cy.vueSelect("booking_item_id", testData.items[0].external_id, testData.items[0].external_id); >+ cy.vueSelect( >+ "booking_item_id", >+ testData.items[0].external_id, >+ testData.items[0].external_id >+ ); > cy.wait("@getCirculationRules"); > > // Verify date picker is now enabled >@@ -380,9 +390,7 @@ describe("Booking Modal Date Picker Tests", () => { > > // Should either succeed (modal closes) or show specific validation error > cy.get("body").then($body => { >- if ( >- $body.find("booking-modal-island .modal:visible").length > 0 >- ) { >+ if ($body.find("booking-modal-island .modal:visible").length > 0) { > // If modal is still visible, check for validation messages > cy.log( > "Modal still visible - checking for validation feedback" >@@ -458,10 +466,7 @@ describe("Booking Modal Date Picker Tests", () => { > dateTestCirculationRules.issuelength + > dateTestCirculationRules.renewalsallowed * > dateTestCirculationRules.renewalperiod; // 10 + 3*5 = 25 >- const calculatedMaxDate = clearZoneStart.add( >- maxPeriod - 1, >- "day" >- ); // Day 50 + 24 = Day 74 >+ const calculatedMaxDate = clearZoneStart.add(maxPeriod - 1, "day"); // Day 50 + 24 = Day 74 > > const beyondMaxDate = calculatedMaxDate.add(1, "day"); // Day 75 > >@@ -554,26 +559,50 @@ describe("Booking Modal Date Picker Tests", () => { > }); > }); > >- // For dates in the currently visible month, also verify the DOM class >- expectedBoldDates >- .filter(d => d.month() === clearZoneStart.month()) >- .forEach(boldDate => { >- cy.get("@dateTestFlatpickr") >- .getFlatpickrDate(boldDate.toDate()) >- .should("have.class", "booking-loan-boundary"); >- cy.log( >- `â Day ${boldDate.format("YYYY-MM-DD")}: Has 'booking-loan-boundary' class (bold)` >- ); >- }); >+ // For dates in the currently visible month, also verify the DOM class. >+ // Query calendarContainer directly to avoid navigation races (the >+ // booking modal's onMonthChange handler can jump the calendar away). >+ cy.get("@dateTestFlatpickr").should($el => { >+ const fp = $el[0]._flatpickr; >+ // Ensure calendar shows the start date's month >+ if ( >+ fp.currentMonth !== clearZoneStart.month() || >+ fp.currentYear !== clearZoneStart.year() >+ ) { >+ fp.jumpToDate(clearZoneStart.toDate()); >+ throw new Error("Jumped to target month, retrying assertion"); >+ } >+ >+ expectedBoldDates >+ .filter(d => d.month() === clearZoneStart.month()) >+ .forEach(boldDate => { >+ const label = `${boldDate.format("MMMM D, YYYY")}`; >+ const el = fp.calendarContainer.querySelector( >+ `.flatpickr-day[aria-label="${label}"]` >+ ); >+ expect( >+ el, >+ `Day ${boldDate.format("YYYY-MM-DD")} should exist` >+ ).to.not.be.null; >+ expect( >+ el.classList.contains("booking-loan-boundary"), >+ `Day ${boldDate.format("YYYY-MM-DD")} should have booking-loan-boundary class` >+ ).to.be.true; >+ }); > >- // Verify that only expected dates are bold in the current view >- cy.get(".flatpickr-day.booking-loan-boundary").each($el => { >- const ariaLabel = $el.attr("aria-label"); >- const date = dayjs(ariaLabel, "MMMM D, YYYY"); >- const isExpected = expectedBoldDates.some(expected => >- date.isSame(expected, "day") >+ // Verify no unexpected bold dates in the current view >+ const boldElements = fp.calendarContainer.querySelectorAll( >+ ".flatpickr-day.booking-loan-boundary" > ); >- expect(isExpected, `Unexpected bold date: ${ariaLabel}`).to.be.true; >+ boldElements.forEach(boldEl => { >+ const ariaLabel = boldEl.getAttribute("aria-label"); >+ const date = dayjs(ariaLabel, "MMMM D, YYYY"); >+ const isExpected = expectedBoldDates.some(expected => >+ date.isSame(expected, "day") >+ ); >+ expect(isExpected, `Unexpected bold date: ${ariaLabel}`).to.be >+ .true; >+ }); > }); > > cy.log( >@@ -704,8 +733,20 @@ describe("Booking Modal Date Picker Tests", () => { > cy.get("@fp").openFlatpickr(); > > // Helpers that use native events to avoid detached DOM errors from Vue re-renders >- const monthNames = ["January", "February", "March", "April", "May", "June", >- "July", "August", "September", "October", "November", "December"]; >+ const monthNames = [ >+ "January", >+ "February", >+ "March", >+ "April", >+ "May", >+ "June", >+ "July", >+ "August", >+ "September", >+ "October", >+ "November", >+ "December", >+ ]; > const getDateSelector = (isoDate: string) => { > const d = dayjs(isoDate); > return `.flatpickr-day[aria-label="${monthNames[d.month()]} ${d.date()}, ${d.year()}"]`; >@@ -714,7 +755,9 @@ describe("Booking Modal Date Picker Tests", () => { > cy.get(getDateSelector(isoDate)) > .should("be.visible") > .then($el => { >- $el[0].dispatchEvent(new MouseEvent("mouseover", { bubbles: true })); >+ $el[0].dispatchEvent( >+ new MouseEvent("mouseover", { bubbles: true }) >+ ); > }); > }; > const clickDateByISO = (isoDate: string) => { >@@ -780,10 +823,7 @@ describe("Booking Modal Date Picker Tests", () => { > > // Hover June 29 - Lead period (June 27-28), June 27 is in blocker booking > hoverDateByISO("2026-06-29"); >- getDateByISO("2026-06-29").should( >- "have.class", >- "flatpickr-disabled" >- ); >+ getDateByISO("2026-06-29").should("have.class", "flatpickr-disabled"); > > // ======================================================================== > // PHASE 3c: BIDIRECTIONAL - Lead Period Conflicts with Existing Booking TRAIL >@@ -791,17 +831,11 @@ describe("Booking Modal Date Picker Tests", () => { > > // July 1: Lead June 29-30 â overlaps blocker trail (June 28-30) â DISABLED > hoverDateByISO("2026-07-01"); >- getDateByISO("2026-07-01").should( >- "have.class", >- "flatpickr-disabled" >- ); >+ getDateByISO("2026-07-01").should("have.class", "flatpickr-disabled"); > > // July 2: Lead June 30-July 1 â June 30 in blocker trail â DISABLED > hoverDateByISO("2026-07-02"); >- getDateByISO("2026-07-02").should( >- "have.class", >- "flatpickr-disabled" >- ); >+ getDateByISO("2026-07-02").should("have.class", "flatpickr-disabled"); > > // ======================================================================== > // PHASE 3d: First Clear Start Date After Blocker's Protected Period >@@ -823,10 +857,7 @@ describe("Booking Modal Date Picker Tests", () => { > > // Hover June 23 - Trail (June 24-26) overlaps blocker ACTUAL (June 25-27) > hoverDateByISO("2026-06-23"); >- getDateByISO("2026-06-23").should( >- "have.class", >- "flatpickr-disabled" >- ); >+ getDateByISO("2026-06-23").should("have.class", "flatpickr-disabled"); > > // Clear selection for next phase > cy.get("#booking_period").clearFlatpickr(); >@@ -841,17 +872,11 @@ describe("Booking Modal Date Picker Tests", () => { > > // Hover June 21 - Trail (June 22-24) overlaps blocker LEAD (June 23-24) â DISABLED > hoverDateByISO("2026-06-21"); >- getDateByISO("2026-06-21").should( >- "have.class", >- "flatpickr-disabled" >- ); >+ getDateByISO("2026-06-21").should("have.class", "flatpickr-disabled"); > > // June 20 - Trail (June 21-23), June 23 overlaps blocker lead â DISABLED > hoverDateByISO("2026-06-20"); >- getDateByISO("2026-06-20").should( >- "have.class", >- "flatpickr-disabled" >- ); >+ getDateByISO("2026-06-20").should("have.class", "flatpickr-disabled"); > > // June 19 - Trail (June 20-22) doesn't reach blocker lead (starts June 23) â NOT disabled > hoverDateByISO("2026-06-19"); >@@ -873,10 +898,7 @@ describe("Booking Modal Date Picker Tests", () => { > > // June 20: trail (June 21-23) overlaps blocker lead (June 23-24) â DISABLED > hoverDateByISO("2026-06-20"); >- getDateByISO("2026-06-20").should( >- "have.class", >- "flatpickr-disabled" >- ); >+ getDateByISO("2026-06-20").should("have.class", "flatpickr-disabled"); > > // June 19: trail (June 20-22) clear of blocker lead â NOT disabled > hoverDateByISO("2026-06-19"); >@@ -1133,14 +1155,16 @@ describe("Booking Modal Date Picker Tests", () => { > let chain = cy.wrap(null); > testItems.forEach((item, index) => { > const enumchron = String.fromCharCode(65 + index); >- chain = chain.then(() => cy.task("query", { >- sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = ?, dateaccessioned = ? WHERE itemnumber = ?", >- values: [ >- enumchron, >- `2024-12-0${4 - index}`, >- item.item_id, >- ], >- })); >+ chain = chain.then(() => >+ cy.task("query", { >+ sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = ?, dateaccessioned = ? WHERE itemnumber = ?", >+ values: [ >+ enumchron, >+ `2024-12-0${4 - index}`, >+ item.item_id, >+ ], >+ }) >+ ); > }); > return chain; > }) >@@ -1178,24 +1202,26 @@ describe("Booking Modal Date Picker Tests", () => { > { item: testItems[0], start: 10, end: 15 }, // ITEM 0 > { item: testItems[1], start: 13, end: 20 }, // ITEM 1 > { item: testItems[2], start: 18, end: 25 }, // ITEM 2 >- { item: testItems[3], start: 1, end: 7 }, // ITEM 3 >+ { item: testItems[3], start: 1, end: 7 }, // ITEM 3 > { item: testItems[3], start: 23, end: 30 }, // ITEM 3 > ]; > let chain = cy.wrap(null); > bookings.forEach(b => { >- chain = chain.then(() => cy.task("query", { >- sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) >+ chain = chain.then(() => >+ cy.task("query", { >+ sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) > VALUES (?, ?, ?, ?, ?, ?, ?)`, >- values: [ >- testBiblio.biblio_id, >- testPatron.patron_id, >- b.item.item_id, >- "CPL", >- today.add(b.start, "day").format("YYYY-MM-DD"), >- today.add(b.end, "day").format("YYYY-MM-DD"), >- "new", >- ], >- })); >+ values: [ >+ testBiblio.biblio_id, >+ testPatron.patron_id, >+ b.item.item_id, >+ "CPL", >+ today.add(b.start, "day").format("YYYY-MM-DD"), >+ today.add(b.end, "day").format("YYYY-MM-DD"), >+ "new", >+ ], >+ }) >+ ); > }); > return chain; > }) >@@ -1213,7 +1239,9 @@ describe("Booking Modal Date Picker Tests", () => { > ); > > cy.get("booking-modal-island .modal").should("exist"); >- cy.get("[data-booking-modal]").first().then($btn => $btn[0].click()); >+ cy.get("[data-booking-modal]") >+ .first() >+ .then($btn => $btn[0].click()); > cy.get("booking-modal-island .modal", { > timeout: 10000, > }).should("be.visible"); >@@ -1272,20 +1300,20 @@ describe("Booking Modal Date Picker Tests", () => { > // SCENARIO 1: Start day 5 > cy.log("=== Scenario 1: Start day 5 ==="); > cy.get("@flatpickrInput").openFlatpickr(); >- cy.get("@flatpickrInput") >- .selectFlatpickrDate(today.add(5, "day").toDate()); >+ cy.get("@flatpickrInput").selectFlatpickrDate( >+ today.add(5, "day").toDate() >+ ); > > checkDatesAvailable(6, 17); > checkDatesDisabled(18, 20); > > // SCENARIO 2: Start day 8 >- cy.log( >- "=== Scenario 2: Start day 8 (all items available) ===" >- ); >+ cy.log("=== Scenario 2: Start day 8 (all items available) ==="); > cy.get("@flatpickrInput").clearFlatpickr(); > cy.get("@flatpickrInput").openFlatpickr(); >- cy.get("@flatpickrInput") >- .selectFlatpickrDate(today.add(8, "day").toDate()); >+ cy.get("@flatpickrInput").selectFlatpickrDate( >+ today.add(8, "day").toDate() >+ ); > > checkDatesAvailable(9, 22); > checkDatesDisabled(23, 25); >@@ -1294,8 +1322,9 @@ describe("Booking Modal Date Picker Tests", () => { > cy.log("=== Scenario 3: Start day 19 ==="); > cy.get("@flatpickrInput").clearFlatpickr(); > cy.get("@flatpickrInput").openFlatpickr(); >- cy.get("@flatpickrInput") >- .selectFlatpickrDate(today.add(19, "day").toDate()); >+ cy.get("@flatpickrInput").selectFlatpickrDate( >+ today.add(19, "day").toDate() >+ ); > > checkDatesAvailable(20, 25); > }); >@@ -1356,14 +1385,16 @@ describe("Booking Modal Date Picker Tests", () => { > let chain = cy.wrap(null); > testItems.forEach((item, index) => { > const enumchron = String.fromCharCode(65 + index); >- chain = chain.then(() => cy.task("query", { >- sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = ?, dateaccessioned = ? WHERE itemnumber = ?", >- values: [ >- enumchron, >- `2024-12-0${4 - index}`, >- item.item_id, >- ], >- })); >+ chain = chain.then(() => >+ cy.task("query", { >+ sql: "UPDATE items SET bookable = 1, itype = 'BK', homebranch = 'CPL', enumchron = ?, dateaccessioned = ? WHERE itemnumber = ?", >+ values: [ >+ enumchron, >+ `2024-12-0${4 - index}`, >+ item.item_id, >+ ], >+ }) >+ ); > }); > return chain; > }) >@@ -1397,31 +1428,35 @@ describe("Booking Modal Date Picker Tests", () => { > }) > .then(() => { > // Create bookings on ITEM 0 and ITEM 1 for days 10-12 >- return cy.task("query", { >- sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) >+ return cy >+ .task("query", { >+ sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) > VALUES (?, ?, ?, ?, ?, ?, ?)`, >- values: [ >- testBiblio.biblio_id, >- testPatron.patron_id, >- testItems[0].item_id, >- "CPL", >- today.add(10, "day").format("YYYY-MM-DD"), >- today.add(12, "day").format("YYYY-MM-DD"), >- "new", >- ], >- }).then(() => cy.task("query", { >- sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) >+ values: [ >+ testBiblio.biblio_id, >+ testPatron.patron_id, >+ testItems[0].item_id, >+ "CPL", >+ today.add(10, "day").format("YYYY-MM-DD"), >+ today.add(12, "day").format("YYYY-MM-DD"), >+ "new", >+ ], >+ }) >+ .then(() => >+ cy.task("query", { >+ sql: `INSERT INTO bookings (biblio_id, patron_id, item_id, pickup_library_id, start_date, end_date, status) > VALUES (?, ?, ?, ?, ?, ?, ?)`, >- values: [ >- testBiblio.biblio_id, >- testPatron.patron_id, >- testItems[1].item_id, >- "CPL", >- today.add(10, "day").format("YYYY-MM-DD"), >- today.add(12, "day").format("YYYY-MM-DD"), >- "new", >- ], >- })); >+ values: [ >+ testBiblio.biblio_id, >+ testPatron.patron_id, >+ testItems[1].item_id, >+ "CPL", >+ today.add(10, "day").format("YYYY-MM-DD"), >+ today.add(12, "day").format("YYYY-MM-DD"), >+ "new", >+ ], >+ }) >+ ); > }) > .then(() => { > cy.intercept( >@@ -1437,7 +1472,9 @@ describe("Booking Modal Date Picker Tests", () => { > ); > > cy.get("booking-modal-island .modal").should("exist"); >- cy.get("[data-booking-modal]").first().then($btn => $btn[0].click()); >+ cy.get("[data-booking-modal]") >+ .first() >+ .then($btn => $btn[0].click()); > cy.get("booking-modal-island .modal", { > timeout: 10000, > }).should("be.visible"); >@@ -1470,8 +1507,9 @@ describe("Booking Modal Date Picker Tests", () => { > ); > > cy.get("@flatpickrInput").openFlatpickr(); >- cy.get("@flatpickrInput") >- .hoverFlatpickrDate(today.add(15, "day").toDate()); >+ cy.get("@flatpickrInput").hoverFlatpickrDate( >+ today.add(15, "day").toDate() >+ ); > > // Day 15 should NOT be disabled (at least one item is free) > cy.get("@flatpickrInput") >@@ -1479,8 +1517,9 @@ describe("Booking Modal Date Picker Tests", () => { > .should("not.have.class", "flatpickr-disabled"); > > // Actually click day 15 to verify it's selectable >- cy.get("@flatpickrInput") >- .selectFlatpickrDate(today.add(15, "day").toDate()); >+ cy.get("@flatpickrInput").selectFlatpickrDate( >+ today.add(15, "day").toDate() >+ ); > > // Reset for next scenario > cy.get("@flatpickrInput").clearFlatpickr(); >@@ -1512,7 +1551,9 @@ describe("Booking Modal Date Picker Tests", () => { > ); > > cy.get("booking-modal-island .modal").should("exist"); >- cy.get("[data-booking-modal]").first().then($btn => $btn[0].click()); >+ cy.get("[data-booking-modal]") >+ .first() >+ .then($btn => $btn[0].click()); > cy.get("booking-modal-island .modal", { > timeout: 10000, > }).should("be.visible"); >@@ -1538,8 +1579,9 @@ describe("Booking Modal Date Picker Tests", () => { > cy.get("#booking_period").as("flatpickrInput2"); > > cy.get("@flatpickrInput2").openFlatpickr(); >- cy.get("@flatpickrInput2") >- .hoverFlatpickrDate(today.add(15, "day").toDate()); >+ cy.get("@flatpickrInput2").hoverFlatpickrDate( >+ today.add(15, "day").toDate() >+ ); > > // Day 15 should NOW be disabled (all items have conflicts) > cy.get("@flatpickrInput2") >@@ -1555,8 +1597,9 @@ describe("Booking Modal Date Picker Tests", () => { > > // Hover a date whose trail overlaps with bookings > // Day 13 trail should get hover-trail class on hover >- cy.get("@flatpickrInput2") >- .hoverFlatpickrDate(today.add(13, "day").toDate()); >+ cy.get("@flatpickrInput2").hoverFlatpickrDate( >+ today.add(13, "day").toDate() >+ ); > cy.get("@flatpickrInput2") > .getFlatpickrDate(today.add(13, "day").toDate()) > .should("have.class", "booking-day--hover-trail"); >@@ -1569,8 +1612,9 @@ describe("Booking Modal Date Picker Tests", () => { > ); > > // Hover a date whose lead period overlaps with bookings >- cy.get("@flatpickrInput2") >- .hoverFlatpickrDate(today.add(8, "day").toDate()); >+ cy.get("@flatpickrInput2").hoverFlatpickrDate( >+ today.add(8, "day").toDate() >+ ); > cy.get("@flatpickrInput2") > .getFlatpickrDate(today.add(8, "day").toDate()) > .should("have.class", "booking-day--hover-lead"); >diff --git a/t/cypress/support/flatpickr.js b/t/cypress/support/flatpickr.js >index 54654e04c66..45c49a9f252 100644 >--- a/t/cypress/support/flatpickr.js >+++ b/t/cypress/support/flatpickr.js >@@ -196,40 +196,6 @@ Cypress.Commands.add( > } > ); > >-/** >- * Helper to close an open Flatpickr calendar. >- */ >-Cypress.Commands.add("closeFlatpickr", { prevSubject: true }, subject => { >- return cy.wrap(subject).then($input => { >- // Wait for flatpickr to be initialized and then close it >- return cy >- .wrap($input) >- .should($el => { >- expect($el[0]).to.have.property("_flatpickr"); >- }) >- .then(() => { >- $input[0]._flatpickr.close(); >- return cy.wrap(subject); >- }); >- }); >-}); >- >-/** >- * Helper to navigate to a specific month and year in a Flatpickr calendar. >- */ >-Cypress.Commands.add( >- "navigateToFlatpickrMonth", >- { prevSubject: true }, >- (subject, targetDate, timeout = 10000) => { >- return ensureCalendarIsOpen(cy.wrap(subject), timeout).then($input => { >- const dayjsDate = dayjs(targetDate); >- return navigateToMonthAndYear(dayjsDate, $input, timeout).then(() => >- cy.wrap($input) >- ); >- }); >- } >-); >- > /** > * Helper to get the Flatpickr mode ('single', 'range', 'multiple'). > */ >@@ -271,7 +237,11 @@ Cypress.Commands.add( > cy.wrap($input).should($el => { > const fp = $el[0]._flatpickr; > expect(fp.selectedDates.length).to.eq(1); >- expect(dayjs(fp.selectedDates[0]).format("YYYY-MM-DD")).to.eq(dayjsDate.format("YYYY-MM-DD")); >+ expect( >+ dayjs(fp.selectedDates[0]).format( >+ "YYYY-MM-DD" >+ ) >+ ).to.eq(dayjsDate.format("YYYY-MM-DD")); > }); > cy.get(".flatpickr-calendar.open").should( > "not.exist", >@@ -363,13 +333,24 @@ Cypress.Commands.add( > cy.wrap($input).should($el => { > const fp = $el[0]._flatpickr; > expect(fp.selectedDates.length).to.eq(2); >- expect(dayjs(fp.selectedDates[0]).format("YYYY-MM-DD")).to.eq(startDayjsDate.format("YYYY-MM-DD")); >- expect(dayjs(fp.selectedDates[1]).format("YYYY-MM-DD")).to.eq(endDayjsDate.format("YYYY-MM-DD")); >+ expect( >+ dayjs(fp.selectedDates[0]).format( >+ "YYYY-MM-DD" >+ ) >+ ).to.eq(startDayjsDate.format("YYYY-MM-DD")); >+ expect( >+ dayjs(fp.selectedDates[1]).format( >+ "YYYY-MM-DD" >+ ) >+ ).to.eq(endDayjsDate.format("YYYY-MM-DD")); > }); > > // Close calendar if it stayed open > cy.get("body").then($body => { >- if ($body.find(".flatpickr-calendar.open").length > 0) { >+ if ( >+ $body.find(".flatpickr-calendar.open") >+ .length > 0 >+ ) { > $input[0]._flatpickr.close(); > } > }); >@@ -408,8 +389,6 @@ Cypress.Commands.add( > } > ); > >-// --- Enhanced Assertion Commands --- >- > /** > * Helper to get a specific Flatpickr day element by its date. > */ >@@ -445,151 +424,6 @@ Cypress.Commands.add( > } > ); > >-/** >- * Assertion helper to check if a Flatpickr date is disabled. >- */ >-Cypress.Commands.add( >- "flatpickrDateShouldBeDisabled", >- { prevSubject: true }, >- (subject, date) => { >- return cy >- .wrap(subject) >- .getFlatpickrDate(date) >- .should("have.class", "flatpickr-disabled") >- .then(() => cy.wrap(subject)); >- } >-); >- >-/** >- * Assertion helper to check if a Flatpickr date is enabled. >- */ >-Cypress.Commands.add( >- "flatpickrDateShouldBeEnabled", >- { prevSubject: true }, >- (subject, date) => { >- return cy >- .wrap(subject) >- .getFlatpickrDate(date) >- .should("not.have.class", "flatpickr-disabled") >- .then(() => cy.wrap(subject)); >- } >-); >- >-/** >- * Assertion helper to check if a Flatpickr date is selected. >- */ >-Cypress.Commands.add( >- "flatpickrDateShouldBeSelected", >- { prevSubject: true }, >- (subject, date) => { >- return cy >- .wrap(subject) >- .getFlatpickrDate(date) >- .should("have.class", "selected") >- .then(() => cy.wrap(subject)); >- } >-); >- >-/** >- * Assertion helper to check if a Flatpickr date is not selected. >- */ >-Cypress.Commands.add( >- "flatpickrDateShouldNotBeSelected", >- { prevSubject: true }, >- (subject, date) => { >- return cy >- .wrap(subject) >- .getFlatpickrDate(date) >- .should("not.have.class", "selected") >- .then(() => cy.wrap(subject)); >- } >-); >- >-/** >- * Assertion helper to check if a Flatpickr date is today. >- */ >-Cypress.Commands.add( >- "flatpickrDateShouldBeToday", >- { prevSubject: true }, >- (subject, date) => { >- return cy >- .wrap(subject) >- .getFlatpickrDate(date) >- .should("have.class", "today") >- .then(() => cy.wrap(subject)); >- } >-); >- >-/** >- * Assertion helper to check if a Flatpickr date is in a range (has inRange class). >- */ >-Cypress.Commands.add( >- "flatpickrDateShouldBeInRange", >- { prevSubject: true }, >- (subject, date) => { >- return cy >- .wrap(subject) >- .getFlatpickrDate(date) >- .should("have.class", "inRange") >- .then(() => cy.wrap(subject)); >- } >-); >- >-/** >- * Assertion helper to check if a Flatpickr date is the start of a range. >- */ >-Cypress.Commands.add( >- "flatpickrDateShouldBeRangeStart", >- { prevSubject: true }, >- (subject, date) => { >- return cy >- .wrap(subject) >- .getFlatpickrDate(date) >- .should("have.class", "startRange") >- .then(() => cy.wrap(subject)); >- } >-); >- >-/** >- * Assertion helper to check if a Flatpickr date is the end of a range. >- */ >-Cypress.Commands.add( >- "flatpickrDateShouldBeRangeEnd", >- { prevSubject: true }, >- (subject, date) => { >- return cy >- .wrap(subject) >- .getFlatpickrDate(date) >- .should("have.class", "endRange") >- .then(() => cy.wrap(subject)); >- } >-); >- >-/** >- * Helper to get the selected dates from a Flatpickr instance. >- * Returns the selected dates as an array of YYYY-MM-DD formatted strings. >- */ >-Cypress.Commands.add( >- "getFlatpickrSelectedDates", >- { prevSubject: true }, >- subject => { >- return cy.wrap(subject).then($input => { >- const fpInstance = $input[0]._flatpickr; >- if (!fpInstance) { >- throw new Error( >- `Flatpickr: Cannot find flatpickr instance on element. Make sure it's initialized with flatpickr.` >- ); >- } >- >- const selectedDates = fpInstance.selectedDates.map(date => >- dayjs(date).format("YYYY-MM-DD") >- ); >- >- return selectedDates; >- }); >- } >-); >- > /** > * Helper to clear a Flatpickr input by setting its value to empty. > * Works with hidden inputs by using the Flatpickr API directly. >-- >2.53.0
You cannot view the attachment while viewing its details because your browser does not support IFRAMEs.
View the attachment on a separate page
.
View Attachment As Diff
View Attachment As Raw
Actions:
View
|
Diff
|
Splinter Review
Attachments on
bug 41129
:
188546
|
188551
|
189348
|
189350
|
193909
|
193910
|
193911
|
193912
|
193913
|
193925
|
193926
| 193927 |
193928
|
193929