Bugzilla – Attachment 151216 Details for
Bug 29002
Add ability to book items ahead of time
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 29002: Add ability to book material from the staff client
Bug-29002-Add-ability-to-book-material-from-the-st.patch (text/plain), 23.09 KB, created by
Martin Renvoize (ashimema)
on 2023-05-15 18:05:42 UTC
(
hide
)
Description:
Bug 29002: Add ability to book material from the staff client
Filename:
MIME Type:
Creator:
Martin Renvoize (ashimema)
Created:
2023-05-15 18:05:42 UTC
Size:
23.09 KB
patch
obsolete
>From 3fdfa1b4c763f072975624eef68cab0eeca01e71 Mon Sep 17 00:00:00 2001 >From: Martin Renvoize <martin.renvoize@ptfs-europe.com> >Date: Wed, 29 Sep 2021 15:17:38 +0100 >Subject: [PATCH] Bug 29002: Add ability to book material from the staff client > >This patch introduces a new modal to the biblio details page to allow >booking of materials. > >Test plan >1) Navigate to the details page of a biblio >2) Note the new 'Place booking' button in the toolbar >3) Click the new button and note the new modal dialogue >4) Enter part of a patron name or cardnumber and then select from the > presented results >5) Select a start date and end date from the calender >6) Submit >--- > Koha/Biblio.pm | 21 +- > Koha/Items.pm | 18 + > Koha/REST/V1/Biblios.pm | 2 + > api/v1/swagger/paths/biblios.yaml | 5 + > .../prog/en/includes/cat-toolbar.inc | 4 + > .../prog/en/includes/modals/place_booking.inc | 50 +++ > .../prog/en/modules/catalogue/detail.tt | 4 + > .../prog/js/place_booking_modal.js | 319 ++++++++++++++++++ > 8 files changed, 420 insertions(+), 3 deletions(-) > create mode 100644 koha-tmpl/intranet-tmpl/prog/en/includes/modals/place_booking.inc > create mode 100644 koha-tmpl/intranet-tmpl/prog/js/place_booking_modal.js > >diff --git a/Koha/Biblio.pm b/Koha/Biblio.pm >index 0a4bf7d2ee..34071f73c8 100644 >--- a/Koha/Biblio.pm >+++ b/Koha/Biblio.pm >@@ -200,6 +200,7 @@ sub can_article_request { > return q{}; > } > >+ > =head3 check_booking > > my $bookable = >@@ -218,7 +219,7 @@ sub check_booking { > my $end_date = dt_from_string( $params->{end_date} ); > my $booking_id = $params->{booking_id}; > >- my $bookable_items = $self->items; >+ my $bookable_items = $self->bookable_items; > my $total_bookable = $bookable_items->count; > > my $dtf = Koha::Database->new->schema->storage->datetime_parser; >@@ -285,8 +286,8 @@ sub place_booking { > { > start_date => $params->{start_date}, > end_date => $params->{end_date}, >- borrowernumber => $patron->borrowernumber, >- biblionumber => $self->biblionumber >+ patron_id => $patron->borrowernumber, >+ biblio_id => $self->biblionumber > } > )->store(); > return $booking; >@@ -570,6 +571,20 @@ sub items { > return Koha::Items->_new_from_dbic( $items_rs ); > } > >+=head3 bookable_items >+ >+ my $bookable_items = $biblio->bookable_items; >+ >+Returns the related Koha::Items resultset filtered to those items that can be booked. >+ >+=cut >+ >+sub bookable_items { >+ my ($self) = @_; >+ return $self->items->filter_by_bookable; >+} >+ >+ > =head3 host_items > > my $host_items = $biblio->host_items(); >diff --git a/Koha/Items.pm b/Koha/Items.pm >index 3f3ba3fb4f..873da63a3e 100644 >--- a/Koha/Items.pm >+++ b/Koha/Items.pm >@@ -158,6 +158,24 @@ sub filter_out_lost { > return $self->search( $params ); > } > >+=head3 filter_by_bookable >+ >+ my $filterd_items = $items->filter_by_bookable; >+ >+Returns a new resultset, containing only those items that are allowed to be booked. >+ >+=cut >+ >+sub filter_by_bookable { >+ my ($self) = @_; >+ >+ return $self->search( >+ { >+ notforloan => [ 0, undef ], >+ withdrawn => [ 0, undef ] >+ } >+ ); >+} > > =head3 move_to_biblio > >diff --git a/Koha/REST/V1/Biblios.pm b/Koha/REST/V1/Biblios.pm >index 9db9a3ae7c..8e49ec7067 100644 >--- a/Koha/REST/V1/Biblios.pm >+++ b/Koha/REST/V1/Biblios.pm >@@ -290,6 +290,7 @@ sub get_items { > my $c = shift->openapi->valid_input or return; > > my $biblio = Koha::Biblios->find( { biblionumber => $c->validation->param('biblio_id') }, { prefetch => ['items'] } ); >+ my $bookable_only = delete $c->validation->output->{bookable}; > > unless ( $biblio ) { > return $c->render( >@@ -303,6 +304,7 @@ sub get_items { > return try { > > my $items_rs = $biblio->items; >+ $items_rs = $items_rs->filter_by_bookable if $bookable_only; > my $items = $c->objects->search( $items_rs ); > return $c->render( > status => 200, >diff --git a/api/v1/swagger/paths/biblios.yaml b/api/v1/swagger/paths/biblios.yaml >index 9ce749c470..aba96c7f8d 100644 >--- a/api/v1/swagger/paths/biblios.yaml >+++ b/api/v1/swagger/paths/biblios.yaml >@@ -424,6 +424,11 @@ > - $ref: "../swagger.yaml#/parameters/q_body" > - $ref: "../swagger.yaml#/parameters/q_header" > - $ref: "../swagger.yaml#/parameters/request_id_header" >+ - name: bookable >+ in: query >+ description: Limit to items that are bookable >+ required: false >+ type: boolean > consumes: > - application/json > produces: >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/cat-toolbar.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/cat-toolbar.inc >index b9a4c3184f..401b7d10db 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/includes/cat-toolbar.inc >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/cat-toolbar.inc >@@ -194,6 +194,8 @@ > [% END %] > [% END %] > >+<div class="btn-group"><button id="placbooking" class="btn btn-default" data-toggle="modal" data-target="#placeBookingModal" data-biblionumber="[% biblionumber | html %]"><i class="fa fa-calendar"></i> Place booking</button></div> >+ > [% IF Koha.Preference('ArticleRequests') %] > <div class="btn-group"><a id="placehold" class="btn btn-default" href="/cgi-bin/koha/circ/request-article.pl?biblionumber=[% biblionumber | html %]"><i class="fa fa-file-text-o"></i> Request article</a></div> > [% END %] >@@ -244,3 +246,5 @@ > </div> > </div> > </div> >+ >+ [% INCLUDE modals/place_booking.inc %] >diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/modals/place_booking.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/place_booking.inc >new file mode 100644 >index 0000000000..752b703086 >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/en/includes/modals/place_booking.inc >@@ -0,0 +1,50 @@ >+<!-- Place booking modal --> >+<div class="modal" id="placeBookingModal" tabindex="-1" role="dialog" aria-labelledby="placeBookingLabel"> >+ <form id="placeBookingForm"> >+ <div class="modal-dialog" role="document"> >+ <div class="modal-content"> >+ <div class="modal-header"> >+ <button type="button" class="closebtn" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button> >+ <h4 class="modal-title" id="placeBookingLabel">Place booking</h4> >+ </div> >+ <div class="modal-body"> >+ <div id="booking_result"></div> >+ <fieldset class="rows"> >+ <input type="hidden" name="biblio_id" id="booking_biblio_id"> >+ <input type="hidden" name="start_date" id="booking_start_date"> >+ <input type="hidden" name="end_date" id="booking_end_date"> >+ <ol> >+ <li> >+ <div class="hint">Enter patron card number or partial name:</div> >+ <label class="required" for="booking_patron_id">Patron: </label> >+ <select name="booking_patron_id" id="booking_patron_id" required="required"> >+ <option></option> >+ [% IF patron %] >+ <option value="[% borrowernumber | uri %]" selected="selected">[% patron.firstname | html %] [% patron.surname %] ([% patron.cardnumber %] )</option> >+ [% END %] >+ </select> >+ </li> >+ <label for="booking_item_id">Item: </label> >+ <select name="booking_item_id" id="booking_item_id" disabled="true"> >+ <option value="0">Any item</option> >+ </select> >+ </li> >+ >+ <li> >+ <div id="period_fields"> >+ <label class="required" for="period">Period: </label> >+ <input type="text" id="period" name="period" class="flatpickr" data-flatpickr-futuredate="true" required="required" disabled="true" autocomplete="off"> >+ <span class="required">Required</span> >+ </div> >+ </li> >+ </ol> >+ </fieldset> >+ </div> <!-- /.modal-body --> >+ <div class="modal-footer"> >+ <button type="submit" class="btn btn-primary">Submit</button> >+ <button type="button" class="btn btn-default" data-dismiss="modal" aria-hidden="true">Cancel</button> >+ </div> <!-- /.modal-footer --> >+ </div> <!-- /.modal-content --> >+ </div> <!-- /.modal-dialog --> >+ </form> >+</div> <!-- /#placeBookingModal --> >diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt >index 60a2944610..b637e46b01 100644 >--- a/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt >+++ b/koha-tmpl/intranet-tmpl/prog/en/modules/catalogue/detail.tt >@@ -1345,6 +1345,9 @@ > > [% MACRO jsinclude BLOCK %] > [% INCLUDE 'catalog-strings.inc' %] >+ [% INCLUDE 'calendar.inc' %] >+ [% INCLUDE 'select2.inc' %] >+ [% INCLUDE 'js-date-format.inc' %] > [% Asset.js("js/catalog.js") | $raw %] > [% Asset.js("js/recalls.js") | $raw %] > [% Asset.js("js/coce.js") | $raw %] >@@ -1814,6 +1817,7 @@ > [% INCLUDE 'js-biblio-format.inc' %] > [% Asset.js("js/browser.js") | $raw %] > [% Asset.js("js/table_filters.js") | $raw %] >+ [% Asset.js("js/place_booking_modal.js") | $raw %] > <script> > var browser; > browser = KOHA.browser('[% searchid | html %]', parseInt(biblionumber, 10)); >diff --git a/koha-tmpl/intranet-tmpl/prog/js/place_booking_modal.js b/koha-tmpl/intranet-tmpl/prog/js/place_booking_modal.js >new file mode 100644 >index 0000000000..f62323d71a >--- /dev/null >+++ b/koha-tmpl/intranet-tmpl/prog/js/place_booking_modal.js >@@ -0,0 +1,319 @@ >+$('#placeBookingModal').on('show.bs.modal', function(e) { >+ var button = $(e.relatedTarget); >+ var biblionumber = button.data('biblionumber'); >+ var itemnumber = button.data('itemnumber') || 0; >+ $('#booking_biblio_id').val(biblionumber); >+ >+ // Patron select2 >+ $("#booking_patron_id").kohaSelect({ >+ dropdownParent: $(".modal-content", "#placeBookingModal"), >+ width: '30%', >+ dropdownAutoWidth: true, >+ allowClear: true, >+ minimumInputLength: 3, >+ ajax: { >+ url: '/api/v1/patrons', >+ delay: 250, >+ dataType: 'json', >+ data: function(params) { >+ var search_term = (params.term === undefined) ? '' : params.term; >+ var query = { >+ 'q': JSON.stringify({ >+ "-or": [{ >+ "firstname": { >+ "-like": search_term + '%' >+ } >+ }, >+ { >+ "surname": { >+ "-like": search_term + '%' >+ } >+ }, >+ { >+ "cardnumber": { >+ "-like": search_term + '%' >+ } >+ } >+ ] >+ }), >+ '_order_by': 'firstname', >+ '_page': params.page, >+ }; >+ return query; >+ }, >+ processResults: function(data, params) { >+ var results = []; >+ data.results.forEach(function(patron) { >+ results.push({ >+ "id": patron.patron_id, >+ "text": escape_str(patron.firstname) + " " + escape_str(patron.surname) + " (" + escape_str(patron.cardnumber) + ")" >+ }); >+ }); >+ return { >+ "results": results, "pagination": { "more": data.pagination.more } >+ }; >+ }, >+ }, >+ placeholder: "Search for a patron" >+ }); >+ >+ // If passed patron, pre-select >+ var patron_id = button.data('patron') || 0; >+ if (patron_id) { >+ var patron = $.ajax({ >+ url: '/api/v1/patrons/' + patron_id, >+ dataType: 'json', >+ type: 'GET' >+ }); >+ >+ $.when(patron).then( >+ function(patron){ >+ var newOption = new Option(escape_str(patron.firstname) + " " + escape_str(patron.surname) + " (" + escape_str(patron.cardnumber) + ")", patron.patron_id, true, true); >+ $('#booking_patron_id').append(newOption).trigger('change'); >+ } >+ ); >+ } >+ >+ // Item select2 >+ $("#booking_item_id").select2({ >+ dropdownParent: $(".modal-content", "#placeBookingModal"), >+ width: '30%', >+ dropdownAutoWidth: true, >+ minimumResultsForSearch: 20, >+ placeholder: "Select item" >+ }); >+ >+ // Adopt flatpickr and update mode >+ var periodPicker = $("#period").get(0)._flatpickr; >+ periodPicker.set('mode', 'range'); >+ >+ // Fetch list of bookable items >+ var items = $.ajax({ >+ url: '/api/v1/biblios/' + biblionumber + '/items?bookable=1' + '&_per_page=-1', >+ dataType: 'json', >+ type: 'GET' >+ }); >+ >+ // Fetch list of existing bookings >+ var bookings = $.ajax({ >+ url: '/api/v1/bookings?biblio_id=' + biblionumber, >+ dataType: 'json', >+ type: 'GET' >+ }); >+ >+ // Update item select2 and period flatpickr >+ $.when(items, bookings).then( >+ function(items,bookings){ >+ >+ // Total bookable items >+ var bookable = 0; >+ >+ for (item of items[0]) { >+ bookable++; >+ // Populate item select >+ if (!($('#booking_item_id').find("option[value='" + item.item_id + "']").length)) { >+ if (itemnumber && itemnumber == item.item_id) { >+ // Create a DOM Option and pre-select by default >+ var newOption = new Option(escape_str(item.external_id), item.item_id, true, true); >+ // Append it to the select >+ $('#booking_item_id').append(newOption); >+ } else { >+ // Create a DOM Option and de-select by default >+ var newOption = new Option(escape_str(item.external_id), item.item_id, false, false); >+ // Append it to the select >+ $('#booking_item_id').append(newOption); >+ } >+ } >+ } >+ >+ // Redraw select with new options and enable >+ $('#booking_item_id').trigger('change'); >+ $("#booking_item_id").prop("disabled", false); >+ >+ // Set disabled dates in datepicker >+ periodPicker.config.disable.push( function(date) { >+ >+ // set local copy of selectedDates >+ let selectedDates = periodPicker.selectedDates; >+ >+ // set booked counter >+ let booked = 0; >+ >+ // reset the unavailable items array >+ let unavailable_items = []; >+ >+ // reset the biblio level bookings array >+ let biblio_bookings = []; >+ >+ // disable dates before selected date >+ if (selectedDates[0] && selectedDates[0] > date) { >+ return true; >+ } >+ >+ // iterate existing bookings >+ for (booking of bookings[0]) { >+ var start_date = flatpickr.parseDate(booking.start_date); >+ var end_date = flatpickr.parseDate(booking.end_date); >+ >+ // patron has selected a start date (end date checks) >+ if (selectedDates[0]) { >+ >+ // new booking start date is between existing booking start and end dates >+ if (selectedDates[0] >= start_date && selectedDates[0] <= end_date) { >+ if (booking.item_id) { >+ if (unavailable_items.indexOf(booking.item_id) === -1) { >+ unavailable_items.push(booking.item_id); >+ } >+ } else { >+ if (biblio_bookings.indexOf(booking.booking_id) === -1) { >+ biblio_bookings.push(booking.booking_id); >+ } >+ } >+ } >+ >+ // new booking end date would be between existing booking start and end dates >+ else if (date >= start_date && date <= end_date) { >+ if (booking.item_id) { >+ if (unavailable_items.indexOf(booking.item_id) === -1) { >+ unavailable_items.push(booking.item_id); >+ } >+ } else { >+ if (biblio_bookings.indexOf(booking.booking_id) === -1) { >+ biblio_bookings.push(booking.booking_id); >+ } >+ } >+ } >+ >+ // new booking would span existing booking >+ else if (selectedDates[0] <= start_date && date >= end_date) { >+ if (booking.item_id) { >+ if (unavailable_items.indexOf(booking.item_id) === -1) { >+ unavailable_items.push(booking.item_id); >+ } >+ } else { >+ if (biblio_bookings.indexOf(booking.booking_id) === -1) { >+ biblio_bookings.push(booking.booking_id); >+ } >+ } >+ } >+ >+ // new booking would not conflict >+ else { >+ continue; >+ } >+ >+ // check that there are available items >+ // available = all bookable items - booked items - booked biblios >+ let total_available = items[0].length - unavailable_items.length - biblio_bookings.length; >+ if (total_available === 0) { >+ return true; >+ } >+ } >+ >+ // patron has not yet selected a start date (start date checks) >+ else if (date <= end_date && date >= start_date) { >+ >+ // same item, disable date >+ if (booking.item_id && booking.item_id === itemnumber) { >+ return true; >+ } >+ >+ // count all clashes, both item and biblio level >+ booked++; >+ if (booked == bookable) { >+ return true; >+ } >+ } >+ } >+ }); >+ >+ // Enable flatpickr now we have date function populated >+ periodPicker.redraw(); >+ $("#period_fields :input").prop('disabled', false); >+ >+ // Setup listener for item select2 >+ $('#booking_item_id').on('select2:select', function(e) { >+ itemnumber = e.params.data.id ? e.params.data.id : null; >+ >+ // redraw pariodPicker taking selected item into account >+ periodPicker.redraw(); >+ }); >+ >+ // Set onClose for flatpickr >+ periodPicker.config.onClose.push(function(selectedDates, dateStr, instance) { >+ >+ let startDate = new Date(selectedDates[0]); >+ >+ // set end datetime hours and minutes to the end of the day >+ let endDate = new Date(selectedDates[1]); >+ endDate.setHours(endDate.getHours()+23); >+ endDate.setMinutes(endDate.getMinutes()+59); >+ >+ $('#booking_start_date').val(startDate.toISOString()); >+ $('#booking_end_date').val(endDate.toISOString()); >+ >+ // set available items in select2 >+ if ( selectedDates[0] && selectedDates[1] ) { >+ var booked_items = bookings[0].filter(function(booking) { >+ let start_date = flatpickr.parseDate(booking.start_date); >+ let end_date = flatpickr.parseDate(booking.end_date); >+ // This booking ends before the start of the new booking >+ if ( end_date <= selectedDates[0] ) { >+ return false; >+ } >+ // This booking starts after then end of the new booking >+ if ( start_date >= selectedDates[1] ) { >+ return false; >+ } >+ // This booking overlaps >+ return true; >+ }); >+ $("#booking_item_id > option").each(function() { >+ let option = $(this); >+ if ( itemnumber && itemnumber == option.val() ) { >+ console.log("itemnumber defined and equal to value"); >+ } else if ( booked_items.some(function(booked_item){ >+ return option.val() == booked_item.item_id; >+ }) ) { >+ option.prop('disabled',true); >+ } else { >+ option.prop('disabled',false); >+ } >+ }); >+ } >+ }); >+ }, >+ function(jqXHR, textStatus, errorThrown){ >+ console.log("Fetch failed"); >+ } >+ ); >+}); >+ >+$("#placeBookingForm").on('submit', function(e) { >+ e.preventDefault(); >+ >+ var url = '/api/v1/bookings'; >+ >+ var start_date = $('#booking_start_date').val(); >+ var end_date = $('#booking_end_date').val(); >+ var item_id = $('#booking_item_id').val(); >+ >+ var posting = $.post( >+ url, >+ JSON.stringify({ >+ "start_date": start_date, >+ "end_date": end_date, >+ "biblio_id": $('#booking_biblio_id').val(), >+ "item_id": item_id != 0 ? item_id : null, >+ "patron_id": $('#booking_patron_id').find(':selected').val() >+ }) >+ ); >+ >+ posting.done(function(data) { >+ $('#placeBookingModal').modal('hide'); >+ }); >+ >+ posting.fail(function(data) { >+ $('#booking_result').replaceWith('<div id="booking_result" class="alert alert-danger">Failure</div>'); >+ }); >+}); >-- >2.40.1
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 29002
:
125456
|
125457
|
125458
|
125459
|
151209
|
151211
|
151213
|
151216
|
151217
|
151218
|
158357
|
158358
|
160831