From 74aa2e4c48ff3b036eba477a8cea537be8a9337b Mon Sep 17 00:00:00 2001 From: Jonathan Druart Date: Tue, 10 May 2022 10:14:20 +0200 Subject: [PATCH] Bug 30650: Add circulation page view This is the main commit message. A plugin already exists to manage curbside pickups. This new enhancehemnt is suggesting an implementation that is ready to be integrated into Koha core in order to provide the feature out-of-the-box. What has been done in this patch set: - Deal with installations using the existing plugin (upgrade the DB schema and migrate their data) - Add a new syspref (CurbsidePickup) and two new permissions: * parameters.manage_curbside_pickups * circulate.manage_curbside_pickups - Add an administration page to setup the configuration: admin/curbside_pickup.pl - Add a circulation page to manage the existing pickups, and create new one - Add a new OPAC view "your curbside pickups" to let patron manage their pickups, and create new ones - Add link from the "member" toolbar Improvements compared to the plugin: - Ability to create several pickup windows per day - Better display of the pickup times (not in a dropdown list) - Ability to disable pickups for patrons without waiting holds - Display pickups on the patron circulation page - Display pickups of the library on the homepage - Prevent pickup to be created on a holiday - Better error handling (exceptions) - Unit tests More improvements are already planned, see related bug reports. Test plan: After you setup the feature correctly from the administration view, you will be able to use the schedule curbside pickups from the staff interface, and from the OPAC interface if you selected "patron-scheduled pickup" A. Staff interface 1. Go to Circulation > Curbside pickups => If the logged-in user has the circulate.manage_curbside_pickups permission you will be able to create and manage curbside pickups 2. Go to a patron detail page and click the "Schedule pickup" button in the toolbar 3. If the patron has waiting holds and you selected "Enable for waiting holds only", of if you didn't select the option, you will be able to select a pickup date and slots to create a pickup. 4. Confirm that you cannot create more pickups per slot than what you defined in the curbside pickup configuration for this library 5. Confirm that you cannot create a pickup if the feature is disabled for the library 6. Notice that you can mark the pickup as "stage and ready", then "patron is outside" and finally "delivered today". You can also rollback the change 7. Notice that once the pickup has been marked as delivered, the item has been checked out and that a new notice has been generated (if the patron has "Hold_Filled" in their messaging preferences 8. Confirm that the information about current pickups is displayed on the circulation page of the patron B. OPAC interface 1. Create a new curbside pickup from the OPAC 2. Confirm that the same limitations as from the staff interface are in effect (waiting holds, number of patron per slots, etc.) 3. Confirm that you can cancel a pickup and alert staff of you arrival 4. Confirm that you cannot cancel a pickup that has been delivered already Sponsored-by: Association KohaLa - https://koha-fr.org/ --- circ/curbside_pickups.pl | 153 +++++ .../prog/en/includes/js-date-format.inc | 2 +- .../prog/en/modules/admin/curbside_pickup.tt | 14 +- .../prog/en/modules/circ/circulation-home.tt | 5 + .../prog/en/modules/circ/curbside_pickups.tt | 623 ++++++++++++++++++ 5 files changed, 788 insertions(+), 9 deletions(-) create mode 100755 circ/curbside_pickups.pl create mode 100644 koha-tmpl/intranet-tmpl/prog/en/modules/circ/curbside_pickups.tt diff --git a/circ/curbside_pickups.pl b/circ/curbside_pickups.pl new file mode 100755 index 00000000000..52933da73a2 --- /dev/null +++ b/circ/curbside_pickups.pl @@ -0,0 +1,153 @@ +#! /usr/bin/perl + +# This file is part of Koha. +# +# Koha is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# Koha is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Koha; if not, see . + +use Modern::Perl; +use CGI qw ( -utf8 ); +use Try::Tiny; +use C4::Context; +use C4::Auth qw( get_template_and_user ); +use C4::Output qw( output_html_with_http_headers ); + +use Koha::DateUtils qw( dt_from_string ); +use Koha::CurbsidePickups; +use Koha::CurbsidePickupPolicies; +use Koha::Libraries; +use Koha::Patrons; + +my $input = CGI->new; +my $op = $input->param('op') || 'list'; +my $tab = $input->param('tab'), +my @messages; + +my ( $template, $loggedinuser, $cookie ) = get_template_and_user( + { + template_name => "circ/curbside_pickups.tt", + query => $input, + type => "intranet", + flagsrequired => { parameters => 'manage_curbside_pickups' }, + } +); + +my $branchcode = C4::Context->userenv()->{'branch'}; +my $libraries = Koha::Libraries->search( {}, { order_by => ['branchname'] } ); +if ( $op eq 'find-patron' ) { + my $cardnumber = $input->param('cardnumber'); + my $borrowernumber = $input->param('borrowernumber'); + + my $patron = + $cardnumber + ? Koha::Patrons->find( { cardnumber => $cardnumber } ) + : Koha::Patrons->find($borrowernumber); + + my $existing_curbside_pickups; + + if ( $patron ){ + $existing_curbside_pickups = Koha::CurbsidePickups->search( + { + branchcode => $branchcode, + borrowernumber => $patron->id, + delivered_datetime => undef, + scheduled_pickup_datetime => { '>' => \'DATE(NOW())' }, + } + ); + } else { + push @messages, { + type => 'error', + code => 'no_patron_found', + cardnumber => $cardnumber + }; + } + + $tab = 'schedule-pickup'; + $template->param( + patron => $patron, + existing_curbside_pickups => $existing_curbside_pickups, + ); +} +elsif ( $op eq 'create-pickup' ) { + my $borrowernumber = $input->param('borrowernumber'); + my $scheduled_pickup_datetime = $input->param('pickup_time'); + my $notes = $input->param('notes'); + + try { + Koha::CurbsidePickup->new( + { + branchcode => $branchcode, + borrowernumber => $borrowernumber, + scheduled_pickup_datetime => dt_from_string($scheduled_pickup_datetime), + notes => $notes, + } + )->store(); + } catch { + if ( $_->isa('Koha::Exceptions::CubsidePickup::TooManyPickups') ) { + push @messages, { + type => 'error', + code => 'too_many_pickups', + patron => Koha::Patrons->find($borrowernumber) + }; + } else { + warn $_; + push @messages, { + type => 'error', + code => 'something_wrong_happened', + }; + } + } + # $self->_notify_new_pickup($curbside_pickup); TODO +} +elsif ( $op eq 'cancel' ) { + my $id = $input->param('id'); + my $curbside_pickup = Koha::CurbsidePickups->find($id); + $curbside_pickup->delete() if $curbside_pickup; +} +elsif ( $op eq 'mark-as-staged' ) { + my $id = $input->param('id'); + my $curbside_pickup = Koha::CurbsidePickups->find($id); + $curbside_pickup->mark_as_staged if $curbside_pickup; +} +elsif ( $op eq 'mark-as-unstaged' ) { + my $id = $input->param('id'); + my $curbside_pickup = Koha::CurbsidePickups->find($id); + $curbside_pickup->mark_as_unstaged if $curbside_pickup; +} +elsif ( $op eq 'mark-patron-has-arrived' ) { + my $id = $input->param('id'); + my $curbside_pickup = Koha::CurbsidePickups->find($id); + $curbside_pickup->mark_patron_has_arrived if $curbside_pickup; +} +elsif ( $op eq 'mark-as-delivered' ) { + my $id = $input->param('id'); + my $curbside_pickup = Koha::CurbsidePickups->find($id); + # FIXME Add a try-catch here + $curbside_pickup->mark_as_delivered if $curbside_pickup; +} + +$template->param( + messages => \@messages, + op => $op, + tab => $tab, + policy => Koha::CurbsidePickupPolicies->find({ branchcode => $branchcode }), + curbside_pickups => Koha::CurbsidePickups->search( + { + branchcode => $branchcode, + scheduled_pickup_datetime => { '>' => \'DATE(NOW())' }, + } + ), +); + + +output_html_with_http_headers $input, $cookie, $template->output; diff --git a/koha-tmpl/intranet-tmpl/prog/en/includes/js-date-format.inc b/koha-tmpl/intranet-tmpl/prog/en/includes/js-date-format.inc index 3dae094b8dc..92445eca313 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/includes/js-date-format.inc +++ b/koha-tmpl/intranet-tmpl/prog/en/includes/js-date-format.inc @@ -57,7 +57,7 @@ window.$time = function(value, options) { if(!value) return ''; - var tz = (opitons&&options.tz)||def_tz; + var tz = (options&&options.tz)||def_tz; var m = moment(value); if(tz) m.tz(tz); diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/curbside_pickup.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/curbside_pickup.tt index 1e5e952ea3c..9ce491234d5 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/curbside_pickup.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/admin/curbside_pickup.tt @@ -195,12 +195,10 @@ return format_hhmm(start) + _(" to ") + format_hhmm(end); } function delete_slot(node, branchcode){ - let slot = $(node).find('.pickup-slot').val(); - let splitted = slot.split("-"); - let day = splitted[0]; - let start = splitted[1]; - let end = splitted[2]; - opening_slots[branchcode].splice($.inArray(start+'-'+end, opening_slots), 1); + let slot = $(node).find('input').val(); + opening_slots[branchcode] = $.grep(opening_slots[branchcode], function(elt, index) { + return elt !== slot; + }); refresh_pickup_hours(branchcode); } function refresh_pickup_hours(branchcode) { @@ -218,10 +216,10 @@ let li_node = $('
  • '); slots_per_day[day].forEach(function(slot) { let span_node = $(''); - span_node.append(''); + span_node.append(''); span_node.append(''+format_slot(slot)+''); - let delete_link = $('').on('click', function(e){ e.preventDefault; delete_slot($(this).closest('li'), branchcode); }); + let delete_link = $('').on('click', function(e){ e.preventDefault(); delete_slot($(this).closest('li'), branchcode); }); span_node.append(delete_link); span_node.appendTo(li_node); diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation-home.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation-home.tt index 6821d18f4b3..c8732dbe9ab 100644 --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation-home.tt +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation-home.tt @@ -83,6 +83,11 @@
  • Holds awaiting pickup
  • + [% IF ( Koha.Preference('CurbsidePickup') && CAN_user_parameters_manage_curbside_pickups ) %] +
  • + Curbside Pickups +
  • + [% END %]
  • Hold ratios
  • diff --git a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/curbside_pickups.tt b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/curbside_pickups.tt new file mode 100644 index 00000000000..f5a95163bf5 --- /dev/null +++ b/koha-tmpl/intranet-tmpl/prog/en/modules/circ/curbside_pickups.tt @@ -0,0 +1,623 @@ +[% USE KohaDates %] +[% USE ItemTypes %] +[% USE Branches %] +[% USE AuthorisedValues %] +[% USE Asset %] +[% USE raw %] +[% USE To %] +[% SET footerjs = 1 %] +[% INCLUDE 'doc-head-open.inc' %] +Curbside pickups › Circulation › Koha + +[% INCLUDE 'doc-head-close.inc' %] + + +[% SET today_iso = date.format(date.now, format = '%Y-%m-%d') %] + + + [% INCLUDE 'header.inc' %] + [% INCLUDE 'cat-search.inc' %] + + + + +
    +
    +
    +
    + +

    Curbside pickups

    + + [% UNLESS policy.enabled %] +
    + Curbside pickups are not enabled for your location. +
    + [% INCLUDE 'intranet-bottom.inc' %] + [% STOP %] + [% END %] + + [% FOR m IN messages %] +
    + [% SWITCH m.code %] + [% CASE 'too_many_pickups' %] + This patron already has a scheduled pickup for this library. + [% CASE 'cannot_checkout' %] + Unable to check the items out to [% INCLUDE 'patron-title.inc' patron=m.patron %] + [% CASE 'no_patron_found' %] + No patron found with cardnumber [% m.cardnumber | html %]. + [% CASE %] + [% m.code | html %] + [% END %] +
    + [% END %] + + [% SET to_be_staged = curbside_pickups.filter_by_to_be_staged %] + [% SET staged_and_ready = curbside_pickups.filter_by_staged_and_ready %] + [% SET patron_outside = curbside_pickups.filter_by_patron_outside %] + [% SET delivered_today = curbside_pickups.filter_by_delivered %] +
    + + +
    + [% IF !tab OR tab == 'to-be-staged' %] +
    + [% ELSE %] +
    + [% END %] +
    +

    + +

    +
    + + [% IF to_be_staged.count %] + + + + + + + + + + + [% FOREACH cp IN to_be_staged %] + [% UNLESS cp.staged_datetime %] + + + + + + + [% END %] + [% END %] + +
    Pickup Date/TimePatronItems for pickupAction
    [% cp.scheduled_pickup_datetime | $KohaDates with_hours = 1 %] + [% cp.patron.firstname | html %] [% cp.patron.surname | html %] ([% cp.patron.cardnumber | html %]) + [% IF cp.notes %] +
    + Notes: [% cp.notes | html %] + [% END %] +
    + [% SET waiting_holds = cp.patron.holds.search( found => 'W', branchcode => logged_in_user.branchcode ) %] + [% FOREACH h IN waiting_holds %] + [% h.biblio.title | html %] ([% h.biblio.author | html %], [% h.item.barcode | html %])
    + [% END %] +
    +
    + + + +

    + +

    +
    + +
    + + + +

    + +

    +
    +
    + [% ELSE %] + There are no pickups to be staged. + [% END %] +
    + + [% IF tab == "staged-and-ready" %] +
    + [% ELSE %] +
    + [% END %] +
    + +

    + +

    +
    + + [% IF staged_and_ready.count %] + + + + + + + + + + + + [% FOREACH cp IN staged_and_ready %] + [% IF cp.staged_datetime && !cp.arrival_datetime %] + + + + + + + + [% END %] + [% END %] + +
    Pickup Date/TimePatronItems for pickupStaged byAction
    [% cp.scheduled_pickup_datetime | $KohaDates with_hours = 1 %] + [% cp.patron.firstname | html %] [% cp.patron.surname | html %] ([% cp.patron.cardnumber | html %]) + [% IF cp.notes %] +
    + Notes: [% cp.notes | html %] + [% END %] +
    + [% SET waiting_holds = cp.patron.holds.search( found => 'W', branchcode => logged_in_user.branchcode ) %] + [% FOREACH h IN waiting_holds %] + [% h.biblio.title | html %] ([% h.biblio.author | html %], [% h.item.barcode | html %])
    + [% END %] +
    + [% cp.staged_by_staff.firstname | html %] [% cp.staged_by_staff.surname | html %] + +
    + + + +

    + +

    +
    + +
    + + + +

    + +

    +
    + +
    + + + +

    + +

    +
    +
    + [% ELSE %] + There are no pickups staged and ready. + [% END %] +
    + + [% IF tab == "patron-is-outside" %] +
    + [% ELSE %] +
    + [% END %] +
    + +

    + +

    +
    + + [% IF patron_outside.count %] + + + + + + + + + + + + [% FOREACH cp IN patron_outside %] + [% IF cp.arrival_datetime && !cp.delivered_datetime %] + + + + + + + + [% END %] + [% END %] + +
    Pickup Date/TimePatronItems for pickupStaged byAction
    [% cp.scheduled_pickup_datetime | $KohaDates with_hours = 1 %] + [% cp.patron.firstname | html %] [% cp.patron.surname | html %] ([% cp.patron.cardnumber | html %]) + [% IF cp.notes %] +
    + Notes: [% cp.notes | html %] + [% END %] +
    + [% SET waiting_holds = cp.patron.holds.search( found => 'W', branchcode => logged_in_user.branchcode ) %] + [% FOREACH h IN waiting_holds %] + [% h.biblio.title | html %] ([% h.biblio.author | html %], [% h.item.barcode | html %])
    + [% END %] +
    + [% cp.staged_by_staff.firstname | html %] [% cp.staged_by_staff.surname | html %] + +
    + + + +

    + +

    +
    + +
    + + + +

    + +

    +
    + +
    + + + +

    + +

    +
    +
    + [% ELSE %] + There are no patrons waiting outside. + [% END %] +
    + + [% IF tab == "delivered-today" %] +
    + [% ELSE %] +
    + [% END %] +
    + +

    + +

    +
    + + [% IF delivered_today.count %] + + + + + + + + + + [% FOREACH cp IN delivered_today %] + [% IF cp.delivered_datetime %] + + + + + + [% END %] + [% END %] + +
    Deliver Date/TimePatronItems checked out
    [% cp.delivered_datetime | $KohaDates with_hours = 1 %] + [% cp.patron.firstname | html %] [% cp.patron.surname | html %] ([% cp.patron.cardnumber | html %]) + [% IF cp.notes %] +
    + Notes: [% cp.notes | html %] + [% END %] +
    + [% FOREACH c IN cp.checkouts %] + [% IF date.format(c.issuedate, format = '%Y-%m-%d') == today_iso %] + [% c.item.biblio.title | html %] ([% c.item.biblio.author | html %], [% c.item.barcode | html %])
    + [% END %] + [% END %] +
    + [% ELSE %] + No pickups have been delivered today. + [% END %] +
    + + [% IF tab == "schedule-pickup" %] +
    + [% ELSE %] +
    + [% END %] + [% IF !patron || ( patron && existing_curbside_pickups.count >= 1 ) %] + [% IF existing_curbside_pickups.count >= 1 %] +
    + [% patron.firstname | html %] [% patron.surname | html %] ([% patron.cardnumber | html %]) already has a scheduled pickup for this library. +
    + [% END %] +
    + + + +
    + +
    +
    Card number
    + +
    +
    + + +
    + [% ELSE %] +
    +
    + + + +
      +
    1. + + [% INCLUDE 'patron-title.inc' patron=patron %] +
    2. + +
    3. + + + [% SET waiting_holds = patron.holds.search( found => 'W', branchcode => logged_in_user.branchcode ) %] + [% IF waiting_holds.count %] + [% FOREACH h IN waiting_holds %] +

      + [% h.biblio.title | html %] ([% h.biblio.author | html %], [% h.item.barcode | html %]) +

      + [% END %] + [% ELSE %] + There is no waiting holds for this patron at this location. + [% END %] +
      +
    4. + +
    5. + + +
    6. + +
    7. + +
    8. + + +
    9. +
    +
    + +
    + +
    + +
    + [% END %] +
    +
    +
    +
    +
    +
    + +[% MACRO jsinclude BLOCK %] + [% Asset.js("lib/dayjs/dayjs.min.js") | $raw %] + [% Asset.js("lib/dayjs/plugin/isSameOrAfter.js") | $raw %] + + [% INCLUDE 'calendar.inc' %] + +[% END %] + + +[% INCLUDE 'intranet-bottom.inc' %] -- 2.25.1