From 1327c2d9be7872cec5707d003375cb9d63514313 Mon Sep 17 00:00:00 2001 From: Thibaud Guillot Date: Fri, 20 Sep 2024 19:08:26 +0000 Subject: [PATCH] Bug 33738: Add bookings feature on the OPAC side MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inspired by the work of Paul and Martin, I'm submitting this proposal for the bookings functionality on the OPAC side. Bookings are perform as on the staff side with the api, adding, cancelling and modifying the pickup library works (normally :) ). Test plan: 1) Apply this patch and restart_all 2) Having a bookable item 3) Go to the OPAC side and click on “Place booking” in the menu. 4) Choose item and click on '+' to add it in list, after choosing one or many items you can check availability to have correct pickup librairies and calendar (with valid dates) according to your list of items. 5) Add booking(s); if there is an error, it will appear in the modal. 6) Cancel booking or Modify pickup library if you want Notes: - Add new public routes to prevent missing permissions - Add new 'booking' route to send booking availability (according to checkouts, holds, other bookings..) Thanks for testing Co-authored-by: Paul Derscheid Sponsored by: Association de Gestion des Œuvres Sociales d'Inria (AGOS) --- Koha/REST/V1/Biblios.pm | 10 +- Koha/REST/V1/Bookings.pm | 146 ++++++++ .../booking_availability_info.yaml | 23 ++ api/v1/swagger/paths/biblios.yaml | 5 + api/v1/swagger/paths/bookings.yaml | 48 +++ api/v1/swagger/paths/public_patrons.yaml | 45 +++ api/v1/swagger/swagger.yaml | 6 + .../bootstrap/en/includes/bookings-table.inc | 73 ++++ .../bootstrap/en/includes/modals/bookings.inc | 95 +++++ .../en/includes/opac-detail-sidebar.inc | 4 + .../bootstrap/en/includes/select2.inc | 6 + .../bootstrap/en/includes/usermenu.inc | 9 + .../bootstrap/en/modules/opac-bookings.tt | 159 +++++++++ .../bootstrap/en/modules/opac-user.tt | 13 +- .../bootstrap/js/modals/place_booking.js | 337 ++++++++++++++++++ .../lib/select2/css/select2-spinner.gif | Bin 0 -> 1849 bytes .../opac-tmpl/lib/select2/css/select2.min.css | 1 + .../opac-tmpl/lib/select2/css/select2.png | Bin 0 -> 613 bytes .../opac-tmpl/lib/select2/css/select2x2.png | Bin 0 -> 845 bytes .../opac-tmpl/lib/select2/js/select2.min.js | 2 + opac/opac-bookings.pl | 143 ++++++++ opac/opac-detail.pl | 3 + opac/opac-user.pl | 6 + 23 files changed, 1131 insertions(+), 3 deletions(-) create mode 100644 api/v1/swagger/definitions/booking_availability_info.yaml create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/includes/bookings-table.inc create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/includes/modals/bookings.inc create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/includes/select2.inc create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-bookings.tt create mode 100644 koha-tmpl/opac-tmpl/bootstrap/js/modals/place_booking.js create mode 100644 koha-tmpl/opac-tmpl/lib/select2/css/select2-spinner.gif create mode 100644 koha-tmpl/opac-tmpl/lib/select2/css/select2.min.css create mode 100644 koha-tmpl/opac-tmpl/lib/select2/css/select2.png create mode 100644 koha-tmpl/opac-tmpl/lib/select2/css/select2x2.png create mode 100644 koha-tmpl/opac-tmpl/lib/select2/js/select2.min.js create mode 100755 opac/opac-bookings.pl diff --git a/Koha/REST/V1/Biblios.pm b/Koha/REST/V1/Biblios.pm index 0c6f950585..7fa92fa990 100644 --- a/Koha/REST/V1/Biblios.pm +++ b/Koha/REST/V1/Biblios.pm @@ -537,6 +537,11 @@ sub get_items_public { { prefetch => ['items'] } ); + my $bookable_only = $c->param('bookable'); + + # Deletion of parameter to avoid filtering on the items table in the case of bookings on 'itemtype' + $c->req->params->remove('bookable'); + return $c->render_resource_not_found("Bibliographic record") unless $biblio; @@ -544,8 +549,9 @@ sub get_items_public { my $patron = $c->stash('koha.user'); - my $items_rs = $biblio->items->filter_by_visible_in_opac( { patron => $patron } ); - my $items = $c->objects->search($items_rs); + my $items_rs = $biblio->items->filter_by_visible_in_opac({ patron => $patron }); + $items_rs = $items_rs->filter_by_bookable if $bookable_only; + my $items = $c->objects->search( $items_rs ); return $c->render( status => 200, openapi => $items diff --git a/Koha/REST/V1/Bookings.pm b/Koha/REST/V1/Bookings.pm index 096c58961b..54d20116f2 100644 --- a/Koha/REST/V1/Bookings.pm +++ b/Koha/REST/V1/Bookings.pm @@ -20,6 +20,11 @@ use Modern::Perl; use Mojo::Base 'Mojolicious::Controller'; use Koha::Bookings; +use Koha::Items; +use Koha::Holds; + +use Koha::DateUtils qw( dt_from_string ); +use List::Util 'uniq'; use Try::Tiny qw( catch try ); @@ -148,4 +153,145 @@ sub delete { }; } +sub get_availability_info { + my $c = shift->openapi->valid_input or return; + my $item = Koha::Items->find( { itemnumber => $c->param('item_id') }, + { prefetch => ['bookings'] } ); + my $patron = Koha::Patrons->find( $c->param('patron_id') ); + + return $c->render_resource_not_found("Item") unless $item; + + my @unavailability_dates; + my $now = dt_from_string; + my $hold_without_expiration = undef; + + my $bookings_rs = $item->bookings; + if ( defined($bookings_rs) ) { + while ( my $booking = $bookings_rs->next ) { + if ( $booking->status eq 'new' ) { + my $start_dt = dt_from_string( $booking->start_date ); + my $end_dt = dt_from_string( $booking->end_date ); + + # Define manually end of booking at the end of the day + $end_dt->set_hour(23); + $end_dt->set_minute(59); + $end_dt->set_second(59); + + if ( $end_dt > $now ) { + while ( $start_dt <= $end_dt ) { + my $date_only = $start_dt->ymd; + push @unavailability_dates, $date_only; + $start_dt->add( days => 1 ); + } + } + } + } + } + + my $holds = $item->current_holds; + if ( defined($holds) ) { + while ( my $hold = $holds->next ) { + if ( defined $hold->waitingdate ) { + if ( defined $hold->expirationdate ) { + my $expiration_date_dt = + dt_from_string( $hold->expirationdate ); + + # Define manually end of expiration date at the end of the day + $expiration_date_dt->set_hour(23); + $expiration_date_dt->set_minute(59); + $expiration_date_dt->set_second(59); + + while ( $now <= $expiration_date_dt ) { + my $date_only = $now->ymd; + push @unavailability_dates, $date_only; + $now->add( days => 1 ); + } + } + else { + $hold_without_expiration = 1; + } + } + } + } + + my $issue = $item->checkout; + if ($issue) { + my $date_due_dt = dt_from_string( $issue->date_due ); + + # Define manually end of issue at the end of the day + $date_due_dt->set_hour(23); + $date_due_dt->set_minute(59); + $date_due_dt->set_second(59); + + while ( $now <= $date_due_dt ) { + my $date_only = $now->ymd; + push @unavailability_dates, $date_only; + $now->add( days => 1 ); + } + } + + @unavailability_dates = uniq @unavailability_dates; + + my $pickup_locations = $item->pickup_locations( { 'patron' => $patron } ); + + return try { + return $c->render( + status => 200, + openapi => { + pickup_locations => $pickup_locations, + unavailability_dates => \@unavailability_dates, + hold_without_expiration => $hold_without_expiration, + } + ); + } + catch { + $c->unhandled_exception($_); + }; +} + +=head3 public_add + +Controller function that handles adding a new booking + +=cut + +sub public_add { + my $c = shift->openapi->valid_input or return; + + return try { + my $booking = Koha::Booking->new_from_api( $c->req->json ); + $booking->store; + $booking->discard_changes; + $c->res->headers->location( $c->req->url->to_string . '/' . $booking->booking_id ); + return $c->render( + status => 201, + openapi => $booking, + ); + } catch { + if ( blessed $_ and $_->isa('Koha::Exceptions::Booking::Clash') ) { + return $c->render( + status => 400, + openapi => { error => "Booking would conflict" } + ); + } elsif ( blessed $_ and $_->isa('Koha::Exceptions::Object::DuplicateID') ) { + return $c->render( + status => 409, + openapi => { + error => "Duplicate booking_id", + } + ); + } elsif ( blessed $_ and $_->isa('Koha::Exceptions::Booking::Rule') ) { + return $c->render( + status => 403, + openapi => { + error => $_->{'message'}->{'status'}, + limit => $_->{'message'}->{'limit'} + } + ); + } + + return $c->unhandled_exception($_); + }; +} + 1; diff --git a/api/v1/swagger/definitions/booking_availability_info.yaml b/api/v1/swagger/definitions/booking_availability_info.yaml new file mode 100644 index 0000000000..01527d25dc --- /dev/null +++ b/api/v1/swagger/definitions/booking_availability_info.yaml @@ -0,0 +1,23 @@ +type: object +additionalProperties: false +properties: + item_id: + description: Item ID + type: string + pickup_location: + description: Pickup location of the item + type: string + unavailability_dates: + description: List of unavailability dates of the item + type: array + items: + type: string + format: date-time + hold_without_expiration: + description: Indicates if hold without expiration date exists + type: boolean +required: + - item_id + - pickup_location + - unavailability_dates + - hold_without_expiration \ No newline at end of file diff --git a/api/v1/swagger/paths/biblios.yaml b/api/v1/swagger/paths/biblios.yaml index ab9f8ebd45..99a7de6545 100644 --- a/api/v1/swagger/paths/biblios.yaml +++ b/api/v1/swagger/paths/biblios.yaml @@ -802,6 +802,11 @@ enum: - +strings collectionFormat: csv + - name: bookable + in: query + description: Limit to items that are bookable + required: false + type: boolean consumes: - application/json produces: diff --git a/api/v1/swagger/paths/bookings.yaml b/api/v1/swagger/paths/bookings.yaml index 5a5e2d2d4d..8c1e38c2e2 100644 --- a/api/v1/swagger/paths/bookings.yaml +++ b/api/v1/swagger/paths/bookings.yaml @@ -306,3 +306,51 @@ permissions: circulate: manage_bookings x-mojo-to: Bookings#update +"/public/bookings/availability/info": + get: + x-mojo-to: Bookings#get_availability_info + operationId: getAvailabilityInfo + parameters: + - description: Case insensative search on item_id + in: query + name: item_id + required: true + type: string + - description: Case insensative search on patron_id + in: query + name: patron_id + required: false + type: string + produces: + - application/json + responses: + 200: + description: Availability info of a booking + schema: + items: + $ref: ../swagger.yaml#/definitions/booking_availability_info + type: + - object + - 'null' + 400: + description: | + Bad request. Possible `error_code` attribute values: + + * `invalid_query` + schema: + $ref: "../swagger.yaml#/definitions/error" + 403: + description: Access forbidden + schema: + $ref: ../swagger.yaml#/definitions/error + 500: + description: Internal error + schema: + $ref: ../swagger.yaml#/definitions/error + 503: + description: Under maintenance + schema: + $ref: ../swagger.yaml#/definitions/error + summary: Unavailability of an item + tags: + - bookings \ No newline at end of file diff --git a/api/v1/swagger/paths/public_patrons.yaml b/api/v1/swagger/paths/public_patrons.yaml index dfa00ae825..31182cff83 100644 --- a/api/v1/swagger/paths/public_patrons.yaml +++ b/api/v1/swagger/paths/public_patrons.yaml @@ -344,3 +344,48 @@ description: Under maintenance schema: $ref: "../swagger.yaml#/definitions/error" +"/public/patrons/{patron_id}/bookings": + post: + operationId: addBookingPublic + parameters: + - description: A JSON object containing informations about the new booking + in: body + name: body + required: true + schema: + $ref: ../swagger.yaml#/definitions/booking + produces: + - application/json + responses: + 201: + description: Booking added + schema: + $ref: ../swagger.yaml#/definitions/booking + 400: + description: Bad request + schema: + $ref: ../swagger.yaml#/definitions/error + 401: + description: Authentication required + schema: + $ref: ../swagger.yaml#/definitions/error + 403: + description: Access forbidden + schema: + $ref: ../swagger.yaml#/definitions/error + 409: + description: Conflict + schema: + $ref: ../swagger.yaml#/definitions/error + 500: + description: Internal error + schema: + $ref: ../swagger.yaml#/definitions/error + 503: + description: Under maintenance + schema: + $ref: ../swagger.yaml#/definitions/error + summary: Add a new booking (public) + tags: + - bookings + x-mojo-to: Bookings#public_add diff --git a/api/v1/swagger/swagger.yaml b/api/v1/swagger/swagger.yaml index 7f6f9969f7..f8c32e577a 100644 --- a/api/v1/swagger/swagger.yaml +++ b/api/v1/swagger/swagger.yaml @@ -22,6 +22,8 @@ definitions: $ref: ./definitions/basket.yaml booking: $ref: ./definitions/booking.yaml + booking_availability_info: + $ref: ./definitions/booking_availability_info.yaml booking_patch: $ref: ./definitions/booking_patch.yaml bundle_link: @@ -523,6 +525,8 @@ paths: $ref: "./paths/biblios.yaml#/~1public~1biblios~1{biblio_id}~1items" "/public/biblios/{biblio_id}/ratings": $ref: "./paths/biblios.yaml#/~1public~1biblios~1{biblio_id}~1ratings" + "/public/bookings/availability/info": + $ref: ./paths/bookings.yaml#/~1public~1bookings~1availability~1info /public/libraries: $ref: ./paths/libraries.yaml#/~1public~1libraries "/public/libraries/{library_id}": @@ -533,6 +537,8 @@ paths: $ref: ./paths/public_oauth.yaml#/~1public~1oauth~1login~1{provider_code}~1{interface} "/public/patrons/{patron_id}/article_requests/{article_request_id}": $ref: "./paths/article_requests.yaml#/~1public~1patrons~1{patron_id}~1article_requests~1{article_request_id}" + "/public/patrons/{patron_id}/bookings": + $ref: "./paths/public_patrons.yaml#/~1public~1patrons~1{patron_id}~1bookings" "/public/patrons/{patron_id}/checkouts": $ref: "./paths/public_patrons.yaml#/~1public~1patrons~1{patron_id}~1checkouts" "/public/patrons/{patron_id}/guarantors/can_see_charges": diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/bookings-table.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/bookings-table.inc new file mode 100644 index 0000000000..32e8b49f3d --- /dev/null +++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/bookings-table.inc @@ -0,0 +1,73 @@ +[% USE Branches %] +[% USE ItemTypes %] +[% USE KohaDates %] +[% PROCESS 'i18n.inc' %] + + + + + + + + + + + + + + + + + + [% FOREACH BOOKING IN BOOKINGS %] + + + + + + + [% IF BOOKING.item %] + + + [% ELSE %] + + + [% END %] + + + + [% END # /FOREACH HOLDS %] + +
Bookings ([% BOOKINGS.count | html %] total)
TitlePlaced onPickup locationStart dateEnd dateItem typeBarcodeProvided byModify
+ [% INCLUDE 'biblio-title.inc' biblio=BOOKING.biblio link => 1 %] + [% BOOKING.item.enumchron | html %] + [% BOOKING.biblio.author | html %] + + [% BOOKING.creation_date | $KohaDates %] + + [% BOOKING.pickup_library.branchname | html %] + + [% PROCESS 'modals/bookings.inc' action = 'change-pickup-location' %] + + [% BOOKING.start_date | $KohaDates %] + + [% BOOKING.end_date | $KohaDates %] + + [% BOOKING.item.itemtype.description %] + + [% BOOKING.item.barcode %] + + [% Branches.GetName(BOOKING.item.homebranch) | html %] + + + [% PROCESS 'modals/bookings.inc' action = 'cancel' %] +
diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/modals/bookings.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/modals/bookings.inc new file mode 100644 index 0000000000..af6384eeb3 --- /dev/null +++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/modals/bookings.inc @@ -0,0 +1,95 @@ +[% USE Branches %] +[% SET booking_id = BOOKING.booking_id | html %] + + diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-detail-sidebar.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-detail-sidebar.inc index 03e74f148c..f46de76739 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-detail-sidebar.inc +++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/opac-detail-sidebar.inc @@ -11,6 +11,10 @@ > [% END %] [% END %] + + [% IF ( BookableItems ) %] +
  • Place booking
  • + [% END %] [% END %] [% IF Koha.Preference('UseRecalls') %] diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/select2.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/select2.inc new file mode 100644 index 0000000000..db18d56081 --- /dev/null +++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/select2.inc @@ -0,0 +1,6 @@ +[% USE raw %] +[% USE Asset %] +[% Asset.js("lib/select2/js/select2.min.js") | $raw %] +[% Asset.css("lib/select2/css/select2.min.css") | $raw %] +[% Asset.css("css/select2.css") | $raw %] +[% Asset.js("js/select2.js") | $raw %] \ No newline at end of file diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc index 894b317dcc..d043ca58a4 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc +++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc @@ -62,6 +62,15 @@
  • Lists
  • [% END %] + [% IF logged_in_user.bookings.count %] + [% IF ( bookingsview ) %] +
  • + [% ELSE %] +
  • + [% END %] + Bookings ([% logged_in_user.bookings.count | html %])
  • + [% END %] + [% IF Koha.Preference( 'RoutingSerials' ) && logged_in_user && logged_in_user.get_routing_lists.count %]
  • Routing lists
  • [% END %] diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-bookings.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-bookings.tt new file mode 100644 index 0000000000..5920249ecf --- /dev/null +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-bookings.tt @@ -0,0 +1,159 @@ +[% USE raw %] +[% USE Asset %] +[% USE Koha %] +[% USE KohaDates %] +[% USE Branches %] +[% USE ItemTypes %] +[% USE Price %] +[% USE AuthorisedValues %] +[% USE AdditionalContents %] +[% SET OpacNav = AdditionalContents.get( location => "OpacNav", lang => lang, library => logged_in_user.branchcode || default_branch, blocktitle => 0 ) %] +[% SET OpacNavBottom = AdditionalContents.get( location => "OpacNavBottom", lang => lang, library => logged_in_user.branchcode || default_branch, blocktitle => 0 ) %] +[% SET OpacMySummaryNote = AdditionalContents.get( location => "OpacMySummaryNote", lang => lang, library => branchcode ) %] + +[% SET borrower_club_enrollments = logged_in_user.get_club_enrollments %] +[% SET borrower_enrollable_clubs = logged_in_user.get_enrollable_clubs(1) %] + +[% INCLUDE 'doc-head-open.inc' %] +Your library home › [% IF ( LibraryNameTitle ) %][% LibraryNameTitle | html %][% ELSE %]Koha online[% END %] catalog +[% INCLUDE 'doc-head-close.inc' %] +[% Asset.css("css/overdrive.css") | $raw %] +[% BLOCK cssinclude %][% END %] + +[% INCLUDE 'bodytag.inc' bodyid='opac-user' bodyclass='scrollto' %] +[% INCLUDE 'masthead.inc' %] + +
    + [% WRAPPER breadcrumbs %] + [% WRAPPER breadcrumb_item %] + [% INCLUDE 'patron-title.inc' patron = logged_in_user %] + [% END %] + [% WRAPPER breadcrumb_item bc_active= 1 %] + Your bookings + [% END %] + [% END #/ WRAPPER breadcrumbs %] + +
    +
    +
    + +
    +
    +
    +

    Your bookings

    + + [% IF ( messages ) %] +
    + Booking failed: + [% FOREACH message IN messages %] +
    + [% message.description %] +
    + [% END # /FOREACH messages %] + Go back to notice +
    + [% END %] + + [% IF op == 'list' %] +
    + +
    +
    +
    +
    + [% SET action_method = biblio == '' ? 'add-multiple' : 'add' %] + + [% PROCESS 'modals/bookings.inc' action = action_method %] +
    +
    + [% PROCESS 'bookings-table.inc' %] +
    + [% END %] +
    +
    +
    +
    +
    + +[% INCLUDE 'opac-bottom.inc' %] +[% BLOCK jsinclude %] + [% Asset.js("js/form-submit.js") | $raw %] + [% Asset.js("lib/vis-timeline/vis-timeline-graph2d.min.js") | $raw %] + [% Asset.js("js/modals/place_booking.js") | $raw %] + [% Asset.js("js/modals/place_multiple_bookings.js") | $raw %] + [% Asset.js("lib/bootstrap/js/bootstrap.min.js") | $raw %] + [% INCLUDE 'select2.inc' %] + [% INCLUDE 'calendar.inc' %] + [% INCLUDE 'datatables.inc' %] + + + +[% END %] diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt index 1c63a78d03..88a61ac397 100644 --- a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt +++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-user.tt @@ -275,6 +275,11 @@ Holds ([% RESERVES.count | html %]) [% END %] [% END %] + [% IF ( BOOKINGS.count ) %] + [% WRAPPER tab_item tabname= "opac-user-bookings" %] + Bookings ([% BOOKINGS.count | html %]) + [% END %] + [% END %] [% IF Koha.Preference('UseRecalls') && RECALLS.count %] [% WRAPPER tab_item tabname= "opac-user-recalls" %] Recalls ([% RECALLS.count | html %]) @@ -825,6 +830,12 @@ [% END # /tab_panel#opac-user-holds %] [% END # / #RESERVES.count %] + [% IF ( BOOKINGS.count ) %] + [% WRAPPER tab_panel tabname="opac-user-bookings" %] + [% PROCESS 'bookings-table.inc' %] + [% END # /tab_panel#opac-user-bookings %] + [% END # / #BOOKINGS.count %] + [% IF Koha.Preference('UseRecalls') && RECALLS.count %] [% WRAPPER tab_panel tabname="opac-user-recalls" %] @@ -1174,7 +1185,7 @@ ); }); - var dTables = $("#checkoutst,#holdst,#overduest,#opac-user-relative-issues-table"); + var dTables = $("#checkoutst,#holdst,#overduest,#opac-user-relative-issues-table,#opac-user-bookings-table"); dTables.each(function(){ var thIndex = $(this).find("th.psort").index(); $(this).on("init.dt", function() { diff --git a/koha-tmpl/opac-tmpl/bootstrap/js/modals/place_booking.js b/koha-tmpl/opac-tmpl/bootstrap/js/modals/place_booking.js new file mode 100644 index 0000000000..71f7a26f39 --- /dev/null +++ b/koha-tmpl/opac-tmpl/bootstrap/js/modals/place_booking.js @@ -0,0 +1,337 @@ +let dataFetched = false; +let bookable_items, booking_id, booking_item_id; + +function containsAny(integers1, integers2) { + // Create a hash set to store integers from the second array + let integerSet = {}; + for (let i = 0; i < integers2.length; i++) { + integerSet[integers2[i]] = true; + } + + // Check if any integer from the first array exists in the hash set + for (let i = 0; i < integers1.length; i++) { + if (integerSet[integers1[i]]) { + return true; // Found a match, return true + } + } + + return false; // No match found +} + +$('#opac-add-booking-modal').on('show.bs.modal', function(e) { + // Get context + let biblionumber = $('#biblio-id').val(); + + let start_date = $('#booking-start_date').val(); + let end_date = $('#booking-end_date').val(); + + // Adopt periodPicker + let periodPicker = $("#period").get(0)._flatpickr; + + if ( !dataFetched ) { + // Fetch list of bookable items + let itemsFetch = $.ajax({ + url: '/api/v1/public/biblios/' + biblionumber + '/items?bookable=1' + '&_per_page=-1', + dataType: 'json', + type: 'GET' + }); + + // Update item select2 and period flatpickr + $.when(itemsFetch).then( + function(itemsFetch){ + + // Set variables + bookable_items = itemsFetch; + + // Update flatpickr mode + periodPicker.set('mode', 'range'); + + // Total bookable items + let bookable = 0; + + $.each(bookable_items, function(index, item) { + bookable++; + // Populate item select (NOTE: Do we still need this check for pre-existing select option here?) + if (!($('#booking-item-id').find("option[value='" + item.item_id + "']").length)) { + // Create a DOM Option and de-select by default + let newOption = new Option(escape_str(item.external_id), item.item_id, false, false); + if(!booking_item_id) { + booking_item_id = item.external_id; + } + // Append it to the select + $('#booking-item-id').append(newOption); + } + }); + + // Item select2 + $("#booking-item-id").select2({ + dropdownParent: $(".modal-content", "#opac-add-booking-modal"), + width: '50%', + dropdownAutoWidth: true, + minimumResultsForSearch: 20, + placeholder: __("Select item") + }); + + + // Set onChange for flatpickr + let changeExists = periodPicker.config.onChange.filter(f => f.name ==='periodChange'); + if(changeExists.length === 0) { + periodPicker.config.onChange.push(function periodChange(selectedDates, dateStr, instance) { + // Range set, update hidden fields and set available items + if ( selectedDates[0] && selectedDates[1] ) { + // set form fields from picker + let picker_start = dayjs(selectedDates[0]); + let picker_end = dayjs(selectedDates[1]).endOf('day'); + $('#booking-start_date').val(picker_start.toISOString()); + $('#booking-end_date').val(picker_end.toISOString()); + } + // Range not set, reset field options + else { + $('#booking-item-id > option').each(function() { + $(this).prop('disabled', false); + }); + $('#booking-item-id').trigger('change.select2'); + } + }); + }; + + // Enable flatpickr now we have date function populated + periodPicker.redraw(); + $("#period_fields :input").prop('disabled', false); + + // Redraw select with new options and enable + $('#booking-item-id').trigger('change'); + $("#booking-item-id").prop("disabled", false); + + // Set the flag to indicate that data has been fetched + dataFetched = true; + + // Set form values + setFormValues(booking_item_id,start_date,end_date,periodPicker); + }, + function(jqXHR, textStatus, errorThrown){ + console.log("Fetch failed"); + } + ); + } else { + setFormValues(booking_item_id,start_date,end_date,periodPicker); + }; +}); + +function fetchItemsAvailability() { + let periodPicker = $("#period").get(0)._flatpickr; + let bookingItems = $('#items-list li').map(function() { + return $(this).attr('id').replace('booking_item-', ''); + }).get(); + + let allUnavailabilityDates = []; + let holdWithoutExpirationItems = []; + let pickupLocations = []; + + let requests = bookingItems.map(itemId => { + return $.ajax({ + url: '/api/v1/public/bookings/availability/info', + dataType: 'json', + data: { + patron_id: logged_in_user_id, + item_id: itemId + } + }).then(response => { + if (response.hold_without_expiration) { + holdWithoutExpirationItems.push(itemId); + } + + allUnavailabilityDates = allUnavailabilityDates.concat(response.unavailability_dates); + + pickupLocations = pickupLocations.concat(response.pickup_locations); + }).catch(error => { + console.log("Availability info fetch failed for item " + itemId + ": ", error); + }); + }); + + Promise.all(requests).then(() => { + if (holdWithoutExpirationItems.length > 0) { + $("#booking_result").replaceWith( + `
    Current hold is waiting without expiration date for items: ${holdWithoutExpirationItems.join(', ')}, please select another item if possible.
    ` + ); + } else { + $("#booking_result").empty().removeClass(); + setLocationsPicker([...new Set(pickupLocations)]); + periodPicker.set('disable', [...new Set(allUnavailabilityDates)]); + } + }); +} + +function setLocationsPicker(locations) { + let $pickupSelect = $("#pickup_library_id"); + + $pickupSelect.empty(); + $.each(locations, function (index, pickup_location) { + let option = $( + '" + ); + $pickupSelect.append(option); + }); +} + +function setFormValues(booking_item_id,start_date,end_date,periodPicker){ + + // Set booking start & end if this is an edit + if ( start_date ) { + // Allow invalid pre-load so setDate can set date range + // periodPicker.set('allowInvalidPreload', true); + // FIXME: Why is this the case.. we're passing two valid Date objects + + let dates = [ new Date(start_date), new Date(end_date) ]; + periodPicker.setDate(dates, true); + } + // Reset periodPicker, biblio_id may have been nulled + else { + periodPicker.redraw(); + }; + + // If passed an itemnumber, pre-select + if (booking_item_id) { + $('#booking-item-id').val(booking_item_id).trigger('change'); + } +} + +$("#opac-add-form-booking").on('submit', function(e) { + e.preventDefault(); + + let url = '/api/v1/public/patrons/'+logged_in_user_id+'/bookings'; + + var items_list = $('#items-list li'); + let start_date = $('#booking-start_date').val(); + let end_date = $('#booking-end_date').val(); + let pickup_library_id = $("#pickup_library_id").val(); + + var errors = []; + var requests = []; + var sendBookingRequest = function (item_id) { + return $.post( + url, + JSON.stringify({ + start_date: start_date, + end_date: end_date, + biblio_id: $('#biblio-id').val(), + item_id: item_id, + patron_id: logged_in_user_id, + pickup_library_id: pickup_library_id + }) + ); + }; + + items_list.each(function (i) { + item_id = $(this).data('item_id'); + var request = sendBookingRequest(item_id); + + requests.push(request); + + request.fail(function (data) { + var error = data.responseJSON.error; + var errorMessage = error ? error : "Failure"; + errors.push(item_id + " - " + errorMessage); + }); + }); + + $.when.apply($, requests).done(function () { + if (errors.length > 0) { + $("#booking_result").replaceWith( + '
    ' + + errors.join("
    ") + + "
    " + ); + } else { + $("#opac-add-booking-modal").modal("hide"); + window.location.reload(); + } + }).fail(function () { + $("#booking_result").replaceWith( + '
    ' + + errors.join("
    ") + + "
    " + ); + }); +}); + +$('#opac-add-booking-modal').on('hidden.bs.modal', function (e) { + $('#booking-item-id').val(0).trigger('change'); + $('#items-list').empty(); + $('#period').get(0)._flatpickr.clear(); + $('#booking-start_date').val(''); + $('#booking-end_date').val(''); + $('#booking_id').val(''); + // Reset pickup library select + $("#pickup_library_id").val(null).trigger("change"); + $("#pickup_library_id").empty(); + $("#pickup_library_id").prop("disabled", true); + $("#booking_result").empty().removeClass(); +}); + +function addItem() { + const itemsList = document.getElementById('items-list'); + const bookingItemId = $('#booking-item-id').val(); + const errorDiv = document.getElementById('booking_result'); + const errorElement = document.createElement('div'); + errorElement.id = 'add_booking_list_error'; + errorElement.classList.add('alert', 'alert-danger'); + + const observer = new MutationObserver(function() { + if (itemsList.children.length === 0) { + $("#pickup_library_id").prop("disabled", true); + $("#fetch_items_availability").hide(); + } else { + $("#pickup_library_id").prop("disabled", false); + $("#fetch_items_availability").show(); + } + }); + observer.observe(itemsList, { + childList: true, + subtree: true + }); + + if (bookingItemId) { + let targetItemInList = document.getElementById('booking_item-' + bookingItemId); + if (targetItemInList) { + errorElement.textContent = 'Item already in the list.'; + errorDiv.appendChild(errorElement); + return; + } + + const listItem = document.createElement('li'); + listItem.setAttribute('id', 'booking_item-' + bookingItemId); + listItem.dataset.item_id = bookingItemId; + listItem.textContent = bookingItemId; + + const deleteLink = document.createElement('a'); + deleteLink.classList.add('fa', 'fa-fw', 'fa-times'); + deleteLink.setAttribute('aria-hidden', 'true'); + deleteLink.onclick = function () { + deleteItem(listItem); + }; + listItem.appendChild(deleteLink); + + itemsList.appendChild(listItem); + + $('#booking-item-id').val(''); + } +} +function updateItemList() { + const itemsList = document.getElementById('items-list'); + const itemnumbers = []; + const itemIds = itemsList.getElementsByTagName('li'); + for (let itemId of itemIds) { + itemnumbers.push(itemId.textContent); + } +} +function deleteItem(listItem) { + const itemsList = document.getElementById('items-list'); + itemsList.removeChild(listItem); + $('#pickup_library_id').empty(); + updateItemList(); +} \ No newline at end of file diff --git a/koha-tmpl/opac-tmpl/lib/select2/css/select2-spinner.gif b/koha-tmpl/opac-tmpl/lib/select2/css/select2-spinner.gif new file mode 100644 index 0000000000000000000000000000000000000000..5b33f7e54f4e55b6b8774d86d96895db9af044b4 GIT binary patch literal 1849 zcma*odr(tX9tZI2z31lM+(&YVk%mZ}5P~KlG2s=WSbGzm0!x7^P##Mnh7t-jP!X0Q zk_SQ}Po-L1tlDK;6l?(>v)e5ZBQx4|Y-Q?nr@Px3?9h(3ZWr3^tj=`TP57gKr87N$ zp2wWee1GRRCwo_xahnw)5cxNPJbCg2L6DV|6`#+yw6v6!mDS$f9-JvFD^n;GQ&UrZ zzh5jCkByB101O60U0q#p_1BM>Cv-vP?&s4@g_((4_1L=L$(a91)0=J91Gas#R{McE znYG^9*0A5YZ>#;~+Wkn(W5B0^yELIYLP!K}mB~<)AM@1&nqekynuaEGqPrzoH|KodRXJy)%+w_fu3nE5>@Bd_b zqC$EQ;{c`T&?EsNO|igL9gC7Ygxv?aQUEXMq?~>wg{EyW;VcJ37CUF#HjrT=KQO_* zS>M9yydXk18D(+QDJ1>r);Lav_uYKp$T?4vr{Q$lTo&pKv^?(>L-)G2*lwH!Ah7k? z7oH<8h-(KTKt5V6$8gF)C7Io&P5=SjTh)=zV=E2EUhQZP##L8S{d%UK>>+y82>+FV+#^BzW7u3F)Bb>=lYQ%%j`F>ASe zo*cw@V#u6T`A2He;70mR(V&iV&-7{qP~=SRf&jm9-T{*ZeZ}$rd0#6c&fLG^xJcf5 z+p<`wJYgW+_s*V{uI$nMB;%8`S_3>PfGOj3Rq}@Cx^+j?rk92fANSFDBYnOqQ>Vdj z)(|$AhP4t&Lb=Gvo2#3Gl%9<=Gv`Mz?Po@P4iLF!x}GUWJICDlFk-hS^Whyh7x~VH z@0vD1>HYD4&e+~yzS*-sFR{9`{QEEZO1zg7>R&7cHts-6j!xHVdA8eI+ZlVzd%`es zJT@$#GX(gvCJ1oJN%yLBK}{V=V;seo;!w|Yte!W1%5qLNFWqvZW>h&IiH+oPT=b@E zPhGzv5=(Un*X>v`>%8h_nj^NdYcE6NHS_ifkCV$*D)Tqrbu`s;<=t<4 zAHNqNV?6(g<1PY-w@#I-WYFViz?9TrkMr)u0g`O`u|>T;k|2sV*YF^punvT;$SuTy{j3Gv)yqD!R_CF>yR)MzmmYS5v+~R zXAdD%ng9?df;wd8GxR#%3O+gz};Vo;)sK%Bj-q>Oq%R7JU-KD?vYu>#2UjaDo z&8$>5xW~?KPD_#XFToU1hIb*VOMidUr6iYiO0N|i-7s`T8!cFT`rN!^1Pt78J93i6 z5HI1wIM$94m{3SLDvISDe6$ZG1;eq_D9RTaaC>=cO{@Bs>$IlPCPJJ$h$)-3vzNUQ6OsN#_zWxey!_9%hxwH2_dEJi=yY|1c7nDm2_Lm!Cof8-R_+9UkS zcBE(o47yE)oMR(Q=dp1a2wTX5KvvGyLqlWTa7V&!A*|w|)ax~1_~aJ0=_Lilg*0iQk7#ZD EAHN$8j{pDw literal 0 HcmV?d00001 diff --git a/koha-tmpl/opac-tmpl/lib/select2/css/select2.min.css b/koha-tmpl/opac-tmpl/lib/select2/css/select2.min.css new file mode 100644 index 0000000000..7c18ad59df --- /dev/null +++ b/koha-tmpl/opac-tmpl/lib/select2/css/select2.min.css @@ -0,0 +1 @@ +.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px;padding:1px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right;margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} diff --git a/koha-tmpl/opac-tmpl/lib/select2/css/select2.png b/koha-tmpl/opac-tmpl/lib/select2/css/select2.png new file mode 100644 index 0000000000000000000000000000000000000000..1d804ffb99699b9e030f1010314de0970b5a000d GIT binary patch literal 613 zcmV-r0-F7aP)#WY!I$JQV$)A5aAS1BM||2XVJl=+L1^1S1H% zM-&lx?NZpUrHhn>fk<>POqf2sh40}xxGZfc+t+#Eb(qHy9_3*1(U%t9t)QDnI#YAL(|ACV(>)>6WD-t!8tutHkdb^#3`HzoJG3A2@T`% zA|K@o*b!`R#(7)PWrMFn2))Ca3MR4(zaT`Zr61*kZK5NPnZwQszxh$fyv3?&4c>$q z2m=+yc0dRXRAsPDxF6sD;@rK4JGdR_``1S~o6Xi@2&aR6hcSrEp9HVRzEqVDqBn<1%hR=D4e1f^ra^A|34Cjc=Gny{F(o#MrvPYgZuTJOz(n)-F<| zj()qR;C={)N<0RRvDZ^@6ND+W*}gh-Lip(MDt!(zMSO)!j2j+*hxgzC-e3$@(O2p* zu;+gddm(cZwXTCLx*Ky4THOa*^b^F`woveIeCK^0aR|TJ00000NkvXXu0mjfA#WC6 literal 0 HcmV?d00001 diff --git a/koha-tmpl/opac-tmpl/lib/select2/css/select2x2.png b/koha-tmpl/opac-tmpl/lib/select2/css/select2x2.png new file mode 100644 index 0000000000000000000000000000000000000000..4bdd5c961d452c49dfa0789c2c7ffb82c238fc24 GIT binary patch literal 845 zcmV-T1G4;yP)upQ6WKflyv?C|ADVW!U!t`EpA+x zB)5#EjWk-_X77YJZtQo`E0SF)^1bZr%)B7Cd`*OK*r z5WG-7e-R9G9^69ksDt29&oyHqxPSt|-S>xi3%PTd+GjY+BGF|nWC(7D-sd(kxqd9~ zS@2YF5vB+>dP8+$l^{oO3-lEWiGA*QIU)Wds#9M6RZ9N zcQ4y4)xqQOxD=vwu%7cz1nY#$lT&y8HCmkWgpwQP#3dhnYj9|2aS_R}IUF_^6s#$= zTm%~>A#oM?KIg$kh=<`gJkeoHa2LrulVy$Yx+N_0R3$4I!R*0677f(FKqm`2_o4~W z0h}fQZ`lC^1A+m;fM7uI(R1`S0KtG@KrkQ}5DW+&@cTnDVIow56KciMk7a899t0bC zC1KI{TsMe5NAR%GD_5`B-@ad4k~K3SO%H z_M31|`HV?E6)u$E3c&*<*n20+V@mRCop>R5;DWuZCmjSo7p@R&OYl^@G":">",'"':""","'":"'","/":"/"};return"string"!=typeof e?e:String(e).replace(/[&<>"'\/\\]/g,function(e){return t[e]})},i.appendMany=function(e,t){if("1.7"===o.fn.jquery.substr(0,3)){var n=o();o.map(t,function(e){n=n.add(e)}),t=n}e.append(t)},i.__cache={};var n=0;return i.GetUniqueElementId=function(e){var t=e.getAttribute("data-select2-id");return null==t&&(e.id?(t=e.id,e.setAttribute("data-select2-id",t)):(e.setAttribute("data-select2-id",++n),t=n.toString())),t},i.StoreData=function(e,t,n){var r=i.GetUniqueElementId(e);i.__cache[r]||(i.__cache[r]={}),i.__cache[r][t]=n},i.GetData=function(e,t){var n=i.GetUniqueElementId(e);return t?i.__cache[n]&&null!=i.__cache[n][t]?i.__cache[n][t]:o(e).data(t):i.__cache[n]},i.RemoveData=function(e){var t=i.GetUniqueElementId(e);null!=i.__cache[t]&&delete i.__cache[t],e.removeAttribute("data-select2-id")},i}),e.define("select2/results",["jquery","./utils"],function(h,f){function r(e,t,n){this.$element=e,this.data=n,this.options=t,r.__super__.constructor.call(this)}return f.Extend(r,f.Observable),r.prototype.render=function(){var e=h('
      ');return this.options.get("multiple")&&e.attr("aria-multiselectable","true"),this.$results=e},r.prototype.clear=function(){this.$results.empty()},r.prototype.displayMessage=function(e){var t=this.options.get("escapeMarkup");this.clear(),this.hideLoading();var n=h(''),r=this.options.get("translations").get(e.message);n.append(t(r(e.args))),n[0].className+=" select2-results__message",this.$results.append(n)},r.prototype.hideMessages=function(){this.$results.find(".select2-results__message").remove()},r.prototype.append=function(e){this.hideLoading();var t=[];if(null!=e.results&&0!==e.results.length){e.results=this.sort(e.results);for(var n=0;n",{class:"select2-results__options select2-results__options--nested"});p.append(l),s.append(a),s.append(p)}else this.template(e,t);return f.StoreData(t,"data",e),t},r.prototype.bind=function(t,e){var l=this,n=t.id+"-results";this.$results.attr("id",n),t.on("results:all",function(e){l.clear(),l.append(e.data),t.isOpen()&&(l.setClasses(),l.highlightFirstItem())}),t.on("results:append",function(e){l.append(e.data),t.isOpen()&&l.setClasses()}),t.on("query",function(e){l.hideMessages(),l.showLoading(e)}),t.on("select",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("unselect",function(){t.isOpen()&&(l.setClasses(),l.options.get("scrollAfterSelect")&&l.highlightFirstItem())}),t.on("open",function(){l.$results.attr("aria-expanded","true"),l.$results.attr("aria-hidden","false"),l.setClasses(),l.ensureHighlightVisible()}),t.on("close",function(){l.$results.attr("aria-expanded","false"),l.$results.attr("aria-hidden","true"),l.$results.removeAttr("aria-activedescendant")}),t.on("results:toggle",function(){var e=l.getHighlightedResults();0!==e.length&&e.trigger("mouseup")}),t.on("results:select",function(){var e=l.getHighlightedResults();if(0!==e.length){var t=f.GetData(e[0],"data");"true"==e.attr("aria-selected")?l.trigger("close",{}):l.trigger("select",{data:t})}}),t.on("results:previous",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e);if(!(n<=0)){var r=n-1;0===e.length&&(r=0);var i=t.eq(r);i.trigger("mouseenter");var o=l.$results.offset().top,s=i.offset().top,a=l.$results.scrollTop()+(s-o);0===r?l.$results.scrollTop(0):s-o<0&&l.$results.scrollTop(a)}}),t.on("results:next",function(){var e=l.getHighlightedResults(),t=l.$results.find("[aria-selected]"),n=t.index(e)+1;if(!(n>=t.length)){var r=t.eq(n);r.trigger("mouseenter");var i=l.$results.offset().top+l.$results.outerHeight(!1),o=r.offset().top+r.outerHeight(!1),s=l.$results.scrollTop()+o-i;0===n?l.$results.scrollTop(0):ithis.$results.outerHeight()||o<0)&&this.$results.scrollTop(i)}},r.prototype.template=function(e,t){var n=this.options.get("templateResult"),r=this.options.get("escapeMarkup"),i=n(e,t);null==i?t.style.display="none":"string"==typeof i?t.innerHTML=r(i):h(t).append(i)},r}),e.define("select2/keys",[],function(){return{BACKSPACE:8,TAB:9,ENTER:13,SHIFT:16,CTRL:17,ALT:18,ESC:27,SPACE:32,PAGE_UP:33,PAGE_DOWN:34,END:35,HOME:36,LEFT:37,UP:38,RIGHT:39,DOWN:40,DELETE:46}}),e.define("select2/selection/base",["jquery","../utils","../keys"],function(n,r,i){function o(e,t){this.$element=e,this.options=t,o.__super__.constructor.call(this)}return r.Extend(o,r.Observable),o.prototype.render=function(){var e=n('');return this._tabindex=0,null!=r.GetData(this.$element[0],"old-tabindex")?this._tabindex=r.GetData(this.$element[0],"old-tabindex"):null!=this.$element.attr("tabindex")&&(this._tabindex=this.$element.attr("tabindex")),e.attr("title",this.$element.attr("title")),e.attr("tabindex",this._tabindex),e.attr("aria-disabled","false"),this.$selection=e},o.prototype.bind=function(e,t){var n=this,r=e.id+"-results";this.container=e,this.$selection.on("focus",function(e){n.trigger("focus",e)}),this.$selection.on("blur",function(e){n._handleBlur(e)}),this.$selection.on("keydown",function(e){n.trigger("keypress",e),e.which===i.SPACE&&e.preventDefault()}),e.on("results:focus",function(e){n.$selection.attr("aria-activedescendant",e.data._resultId)}),e.on("selection:update",function(e){n.update(e.data)}),e.on("open",function(){n.$selection.attr("aria-expanded","true"),n.$selection.attr("aria-owns",r),n._attachCloseHandler(e)}),e.on("close",function(){n.$selection.attr("aria-expanded","false"),n.$selection.removeAttr("aria-activedescendant"),n.$selection.removeAttr("aria-owns"),n.$selection.trigger("focus"),n._detachCloseHandler(e)}),e.on("enable",function(){n.$selection.attr("tabindex",n._tabindex),n.$selection.attr("aria-disabled","false")}),e.on("disable",function(){n.$selection.attr("tabindex","-1"),n.$selection.attr("aria-disabled","true")})},o.prototype._handleBlur=function(e){var t=this;window.setTimeout(function(){document.activeElement==t.$selection[0]||n.contains(t.$selection[0],document.activeElement)||t.trigger("blur",e)},1)},o.prototype._attachCloseHandler=function(e){n(document.body).on("mousedown.select2."+e.id,function(e){var t=n(e.target).closest(".select2");n(".select2.select2-container--open").each(function(){this!=t[0]&&r.GetData(this,"element").select2("close")})})},o.prototype._detachCloseHandler=function(e){n(document.body).off("mousedown.select2."+e.id)},o.prototype.position=function(e,t){t.find(".selection").append(e)},o.prototype.destroy=function(){this._detachCloseHandler(this.container)},o.prototype.update=function(e){throw new Error("The `update` method must be defined in child classes.")},o.prototype.isEnabled=function(){return!this.isDisabled()},o.prototype.isDisabled=function(){return this.options.get("disabled")},o}),e.define("select2/selection/single",["jquery","./base","../utils","../keys"],function(e,t,n,r){function i(){i.__super__.constructor.apply(this,arguments)}return n.Extend(i,t),i.prototype.render=function(){var e=i.__super__.render.call(this);return e.addClass("select2-selection--single"),e.html(''),e},i.prototype.bind=function(t,e){var n=this;i.__super__.bind.apply(this,arguments);var r=t.id+"-container";this.$selection.find(".select2-selection__rendered").attr("id",r).attr("role","textbox").attr("aria-readonly","true"),this.$selection.attr("aria-labelledby",r),this.$selection.on("mousedown",function(e){1===e.which&&n.trigger("toggle",{originalEvent:e})}),this.$selection.on("focus",function(e){}),this.$selection.on("blur",function(e){}),t.on("focus",function(e){t.isOpen()||n.$selection.trigger("focus")})},i.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},i.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},i.prototype.selectionContainer=function(){return e("")},i.prototype.update=function(e){if(0!==e.length){var t=e[0],n=this.$selection.find(".select2-selection__rendered"),r=this.display(t,n);n.empty().append(r);var i=t.title||t.text;i?n.attr("title",i):n.removeAttr("title")}else this.clear()},i}),e.define("select2/selection/multiple",["jquery","./base","../utils"],function(i,e,l){function n(e,t){n.__super__.constructor.apply(this,arguments)}return l.Extend(n,e),n.prototype.render=function(){var e=n.__super__.render.call(this);return e.addClass("select2-selection--multiple"),e.html('
        '),e},n.prototype.bind=function(e,t){var r=this;n.__super__.bind.apply(this,arguments),this.$selection.on("click",function(e){r.trigger("toggle",{originalEvent:e})}),this.$selection.on("click",".select2-selection__choice__remove",function(e){if(!r.isDisabled()){var t=i(this).parent(),n=l.GetData(t[0],"data");r.trigger("unselect",{originalEvent:e,data:n})}})},n.prototype.clear=function(){var e=this.$selection.find(".select2-selection__rendered");e.empty(),e.removeAttr("title")},n.prototype.display=function(e,t){var n=this.options.get("templateSelection");return this.options.get("escapeMarkup")(n(e,t))},n.prototype.selectionContainer=function(){return i('
      • ×
      • ')},n.prototype.update=function(e){if(this.clear(),0!==e.length){for(var t=[],n=0;n×');a.StoreData(r[0],"data",t),this.$selection.find(".select2-selection__rendered").prepend(r)}},e}),e.define("select2/selection/search",["jquery","../utils","../keys"],function(r,a,l){function e(e,t,n){e.call(this,t,n)}return e.prototype.render=function(e){var t=r('');this.$searchContainer=t,this.$search=t.find("input");var n=e.call(this);return this._transferTabIndex(),n},e.prototype.bind=function(e,t,n){var r=this,i=t.id+"-results";e.call(this,t,n),t.on("open",function(){r.$search.attr("aria-controls",i),r.$search.trigger("focus")}),t.on("close",function(){r.$search.val(""),r.$search.removeAttr("aria-controls"),r.$search.removeAttr("aria-activedescendant"),r.$search.trigger("focus")}),t.on("enable",function(){r.$search.prop("disabled",!1),r._transferTabIndex()}),t.on("disable",function(){r.$search.prop("disabled",!0)}),t.on("focus",function(e){r.$search.trigger("focus")}),t.on("results:focus",function(e){e.data._resultId?r.$search.attr("aria-activedescendant",e.data._resultId):r.$search.removeAttr("aria-activedescendant")}),this.$selection.on("focusin",".select2-search--inline",function(e){r.trigger("focus",e)}),this.$selection.on("focusout",".select2-search--inline",function(e){r._handleBlur(e)}),this.$selection.on("keydown",".select2-search--inline",function(e){if(e.stopPropagation(),r.trigger("keypress",e),r._keyUpPrevented=e.isDefaultPrevented(),e.which===l.BACKSPACE&&""===r.$search.val()){var t=r.$searchContainer.prev(".select2-selection__choice");if(0this.maximumInputLength?this.trigger("results:message",{message:"inputTooLong",args:{maximum:this.maximumInputLength,input:t.term,params:t}}):e.call(this,t,n)},e}),e.define("select2/data/maximumSelectionLength",[],function(){function e(e,t,n){this.maximumSelectionLength=n.get("maximumSelectionLength"),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("select",function(){r._checkIfMaximumSelected()})},e.prototype.query=function(e,t,n){var r=this;this._checkIfMaximumSelected(function(){e.call(r,t,n)})},e.prototype._checkIfMaximumSelected=function(e,n){var r=this;this.current(function(e){var t=null!=e?e.length:0;0=r.maximumSelectionLength?r.trigger("results:message",{message:"maximumSelected",args:{maximum:r.maximumSelectionLength}}):n&&n()})},e}),e.define("select2/dropdown",["jquery","./utils"],function(t,e){function n(e,t){this.$element=e,this.options=t,n.__super__.constructor.call(this)}return e.Extend(n,e.Observable),n.prototype.render=function(){var e=t('');return e.attr("dir",this.options.get("dir")),this.$dropdown=e},n.prototype.bind=function(){},n.prototype.position=function(e,t){},n.prototype.destroy=function(){this.$dropdown.remove()},n}),e.define("select2/dropdown/search",["jquery","../utils"],function(o,e){function t(){}return t.prototype.render=function(e){var t=e.call(this),n=o('');return this.$searchContainer=n,this.$search=n.find("input"),t.prepend(n),t},t.prototype.bind=function(e,t,n){var r=this,i=t.id+"-results";e.call(this,t,n),this.$search.on("keydown",function(e){r.trigger("keypress",e),r._keyUpPrevented=e.isDefaultPrevented()}),this.$search.on("input",function(e){o(this).off("keyup")}),this.$search.on("keyup input",function(e){r.handleSearch(e)}),t.on("open",function(){r.$search.attr("tabindex",0),r.$search.attr("aria-controls",i),r.$search.trigger("focus"),window.setTimeout(function(){r.$search.trigger("focus")},0)}),t.on("close",function(){r.$search.attr("tabindex",-1),r.$search.removeAttr("aria-controls"),r.$search.removeAttr("aria-activedescendant"),r.$search.val(""),r.$search.trigger("blur")}),t.on("focus",function(){t.isOpen()||r.$search.trigger("focus")}),t.on("results:all",function(e){null!=e.query.term&&""!==e.query.term||(r.showSearch(e)?r.$searchContainer.removeClass("select2-search--hide"):r.$searchContainer.addClass("select2-search--hide"))}),t.on("results:focus",function(e){e.data._resultId?r.$search.attr("aria-activedescendant",e.data._resultId):r.$search.removeAttr("aria-activedescendant")})},t.prototype.handleSearch=function(e){if(!this._keyUpPrevented){var t=this.$search.val();this.trigger("query",{term:t})}this._keyUpPrevented=!1},t.prototype.showSearch=function(e,t){return!0},t}),e.define("select2/dropdown/hidePlaceholder",[],function(){function e(e,t,n,r){this.placeholder=this.normalizePlaceholder(n.get("placeholder")),e.call(this,t,n,r)}return e.prototype.append=function(e,t){t.results=this.removePlaceholder(t.results),e.call(this,t)},e.prototype.normalizePlaceholder=function(e,t){return"string"==typeof t&&(t={id:"",text:t}),t},e.prototype.removePlaceholder=function(e,t){for(var n=t.slice(0),r=t.length-1;0<=r;r--){var i=t[r];this.placeholder.id===i.id&&n.splice(r,1)}return n},e}),e.define("select2/dropdown/infiniteScroll",["jquery"],function(n){function e(e,t,n,r){this.lastParams={},e.call(this,t,n,r),this.$loadingMore=this.createLoadingMore(),this.loading=!1}return e.prototype.append=function(e,t){this.$loadingMore.remove(),this.loading=!1,e.call(this,t),this.showLoadingMore(t)&&(this.$results.append(this.$loadingMore),this.loadMoreIfNeeded())},e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("query",function(e){r.lastParams=e,r.loading=!0}),t.on("query:append",function(e){r.lastParams=e,r.loading=!0}),this.$results.on("scroll",this.loadMoreIfNeeded.bind(this))},e.prototype.loadMoreIfNeeded=function(){var e=n.contains(document.documentElement,this.$loadingMore[0]);if(!this.loading&&e){var t=this.$results.offset().top+this.$results.outerHeight(!1);this.$loadingMore.offset().top+this.$loadingMore.outerHeight(!1)<=t+50&&this.loadMore()}},e.prototype.loadMore=function(){this.loading=!0;var e=n.extend({},{page:1},this.lastParams);e.page++,this.trigger("query:append",e)},e.prototype.showLoadingMore=function(e,t){return t.pagination&&t.pagination.more},e.prototype.createLoadingMore=function(){var e=n('
      • '),t=this.options.get("translations").get("loadingMore");return e.html(t(this.lastParams)),e},e}),e.define("select2/dropdown/attachBody",["jquery","../utils"],function(f,a){function e(e,t,n){this.$dropdownParent=f(n.get("dropdownParent")||document.body),e.call(this,t,n)}return e.prototype.bind=function(e,t,n){var r=this;e.call(this,t,n),t.on("open",function(){r._showDropdown(),r._attachPositioningHandler(t),r._bindContainerResultHandlers(t)}),t.on("close",function(){r._hideDropdown(),r._detachPositioningHandler(t)}),this.$dropdownContainer.on("mousedown",function(e){e.stopPropagation()})},e.prototype.destroy=function(e){e.call(this),this.$dropdownContainer.remove()},e.prototype.position=function(e,t,n){t.attr("class",n.attr("class")),t.removeClass("select2"),t.addClass("select2-container--open"),t.css({position:"absolute",top:-999999}),this.$container=n},e.prototype.render=function(e){var t=f(""),n=e.call(this);return t.append(n),this.$dropdownContainer=t},e.prototype._hideDropdown=function(e){this.$dropdownContainer.detach()},e.prototype._bindContainerResultHandlers=function(e,t){if(!this._containerResultsHandlersBound){var n=this;t.on("results:all",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:append",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("results:message",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("select",function(){n._positionDropdown(),n._resizeDropdown()}),t.on("unselect",function(){n._positionDropdown(),n._resizeDropdown()}),this._containerResultsHandlersBound=!0}},e.prototype._attachPositioningHandler=function(e,t){var n=this,r="scroll.select2."+t.id,i="resize.select2."+t.id,o="orientationchange.select2."+t.id,s=this.$container.parents().filter(a.hasScroll);s.each(function(){a.StoreData(this,"select2-scroll-position",{x:f(this).scrollLeft(),y:f(this).scrollTop()})}),s.on(r,function(e){var t=a.GetData(this,"select2-scroll-position");f(this).scrollTop(t.y)}),f(window).on(r+" "+i+" "+o,function(e){n._positionDropdown(),n._resizeDropdown()})},e.prototype._detachPositioningHandler=function(e,t){var n="scroll.select2."+t.id,r="resize.select2."+t.id,i="orientationchange.select2."+t.id;this.$container.parents().filter(a.hasScroll).off(n),f(window).off(n+" "+r+" "+i)},e.prototype._positionDropdown=function(){var e=f(window),t=this.$dropdown.hasClass("select2-dropdown--above"),n=this.$dropdown.hasClass("select2-dropdown--below"),r=null,i=this.$container.offset();i.bottom=i.top+this.$container.outerHeight(!1);var o={height:this.$container.outerHeight(!1)};o.top=i.top,o.bottom=i.top+o.height;var s=this.$dropdown.outerHeight(!1),a=e.scrollTop(),l=e.scrollTop()+e.height(),c=ai.bottom+s,d={left:i.left,top:o.bottom},p=this.$dropdownParent;"static"===p.css("position")&&(p=p.offsetParent());var h={top:0,left:0};(f.contains(document.body,p[0])||p[0].isConnected)&&(h=p.offset()),d.top-=h.top,d.left-=h.left,t||n||(r="below"),u||!c||t?!c&&u&&t&&(r="below"):r="above",("above"==r||t&&"below"!==r)&&(d.top=o.top-h.top-s),null!=r&&(this.$dropdown.removeClass("select2-dropdown--below select2-dropdown--above").addClass("select2-dropdown--"+r),this.$container.removeClass("select2-container--below select2-container--above").addClass("select2-container--"+r)),this.$dropdownContainer.css(d)},e.prototype._resizeDropdown=function(){var e={width:this.$container.outerWidth(!1)+"px"};this.options.get("dropdownAutoWidth")&&(e.minWidth=e.width,e.position="relative",e.width="auto"),this.$dropdown.css(e)},e.prototype._showDropdown=function(e){this.$dropdownContainer.appendTo(this.$dropdownParent),this._positionDropdown(),this._resizeDropdown()},e}),e.define("select2/dropdown/minimumResultsForSearch",[],function(){function e(e,t,n,r){this.minimumResultsForSearch=n.get("minimumResultsForSearch"),this.minimumResultsForSearch<0&&(this.minimumResultsForSearch=1/0),e.call(this,t,n,r)}return e.prototype.showSearch=function(e,t){return!(function e(t){for(var n=0,r=0;r');return e.attr("dir",this.options.get("dir")),this.$container=e,this.$container.addClass("select2-container--"+this.options.get("theme")),u.StoreData(e[0],"element",this.$element),e},d}),e.define("jquery-mousewheel",["jquery"],function(e){return e}),e.define("jquery.select2",["jquery","jquery-mousewheel","./select2/core","./select2/defaults","./select2/utils"],function(i,e,o,t,s){if(null==i.fn.select2){var a=["open","close","destroy"];i.fn.select2=function(t){if("object"==typeof(t=t||{}))return this.each(function(){var e=i.extend(!0,{},t);new o(i(this),e)}),this;if("string"!=typeof t)throw new Error("Invalid arguments for Select2: "+t);var n,r=Array.prototype.slice.call(arguments,1);return this.each(function(){var e=s.GetData(this,"select2");null==e&&window.console&&console.error&&console.error("The select2('"+t+"') method was called on an element that is not using Select2."),n=e[t].apply(e,r)}),-1. + +use Modern::Perl; + +use C4::Members (); +use C4::Auth qw( get_template_and_user ); +use C4::Output qw( output_html_with_http_headers ); + +use Koha::Biblios (); +use Koha::Booking (); +use Koha::Bookings (); +use Koha::DateUtils qw(dt_from_string); +use Koha::Patrons (); + +use CGI qw( -utf8 ); +use Carp qw( croak ); +use Digest::SHA qw( sha1_base64 ); +use Time::HiRes qw( time ); + +my $query = CGI->new; +my @messages; + +my ( $template, $borrowernumber, $cookie ) = get_template_and_user( + { + template_name => 'opac-bookings.tt', + query => $query, + type => 'opac', + } +); + +my $patron = Koha::Patrons->find($borrowernumber); + +my $op = $query->param('op') // 'list'; +if ( $op eq 'list' ) { + my $bookings = Koha::Bookings->search( { patron_id => $patron->borrowernumber } )->filter_by_active; + my $hash = sha1_base64( join q{}, time, rand ); + + my $biblio; + my $biblio_id = $query->param('biblio_id'); + if ($biblio_id) { + $biblio = Koha::Biblios->find($biblio_id); + } + + $template->param( + op => 'list', BOOKINGS => $bookings, + BOOKING => { booking_id => $hash }, + biblio => $biblio, + ); +} + +if ( $op eq 'cud-add' ) { + eval { + Koha::Booking->new( + { + patron_id => $patron->borrowernumber, + biblio_id => $query->param('biblio_id'), + item_id => $query->param('item_id'), + start_date => dt_from_string( $query->param('start_date') ), + end_date => dt_from_string( $query->param('end_date') ), + } + )->store; + }; + + if ($@) { + if ( ref($@) eq 'Koha::Exceptions::Booking::Clash') { + push @messages, + { description => "Item already booked for this period", biblio_id => $query->param('biblio_id') }; + } + if ( ref($@) eq 'Koha::Exceptions::Booking::Rule') { + push @messages, { description => $@->{message}, biblio_id => $query->param('biblio_id') }; + } + } else { + print $query->redirect('/cgi-bin/koha/opac-user.pl?tab=opac-user-bookings') or croak; + } +} + +if ( $op eq 'cud-cancel' ) { + my $booking_id = $query->param('booking_id'); + my $booking = Koha::Bookings->find($booking_id); + if ( !$booking ) { + print $query->redirect('/cgi-bin/koha/errors/404.pl') or croak; + exit; + } + + if ( $booking->patron_id ne $patron->borrowernumber ) { + print $query->redirect('/cgi-bin/koha/errors/403.pl') or croak; + exit; + } + + my $is_deleted = $booking->delete; + if ( !$is_deleted ) { + print $query->redirect('/cgi-bin/koha/errors/500.pl') or croak; + exit; + } + + print $query->redirect('/cgi-bin/koha/opac-user.pl') or croak; +} + +if ( $op eq 'cud-change-pickup-location' ) { + my $booking_id = $query->param('booking_id'); + my $new_pickup_location = $query->param('new_pickup_location'); + my $booking = Koha::Bookings->find($booking_id); + + if ( !$booking ) { + print $query->redirect('/cgi-bin/koha/errors/404.pl') or croak; + exit; + } + + if ( $booking->patron_id ne $patron->borrowernumber ) { + print $query->redirect('/cgi-bin/koha/errors/403.pl') or croak; + exit; + } + + my $is_updated = $booking->update( { pickup_library_id => $new_pickup_location } ); + if ( !$is_updated ) { + print $query->redirect('/cgi-bin/koha/errors/500.pl') or croak; + exit; + } + + print $query->redirect('/cgi-bin/koha/opac-user.pl?tab=opac-user-bookings') or croak; +} + +$template->param( + bookingsview => 1, + messages => \@messages +); + +output_html_with_http_headers $query, $cookie, $template->output, undef, { force_no_caching => 1 }; diff --git a/opac/opac-detail.pl b/opac/opac-detail.pl index 7222edc6d6..dd75e5e9c3 100755 --- a/opac/opac-detail.pl +++ b/opac/opac-detail.pl @@ -718,6 +718,8 @@ my $holdable_items = $biblio->items->filter_by_for_hold->count; # If we don't have a patron, then holdable items determines holdability my $can_holds_be_placed = $patron ? 0 : $holdable_items; +my $can_bookings_be_placed = $patron ? $biblio->bookable_items->count : 0; + my ( $itemloop_has_images, $otheritemloop_has_images ); my $item_level_holds; my $item_checkouts; @@ -825,6 +827,7 @@ $template->param( item_checkouts => $item_checkouts, item_level_holds => $item_level_holds, ReservableItems => $can_holds_be_placed, + BookableItems => $can_bookings_be_placed, itemloop_has_images => $itemloop_has_images, otheritemloop_has_images => $otheritemloop_has_images, ); diff --git a/opac/opac-user.pl b/opac/opac-user.pl index be5f63896b..42bf3ba598 100755 --- a/opac/opac-user.pl +++ b/opac/opac-user.pl @@ -351,6 +351,12 @@ $template->param( showpriority => $show_priority, ); +my $bookings = $patron->bookings->filter_by_active; + +$template->param( + BOOKINGS => $bookings, +); + if ( C4::Context->preference('UseRecalls') ) { my $recalls = Koha::Recalls->search( { patron_id => $borrowernumber, completed => 0 } ); $template->param( RECALLS => $recalls ); -- 2.39.5