@@ -, +, @@ - 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 - 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 --- 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 --- a/circ/curbside_pickups.pl +++ a/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; --- a/koha-tmpl/intranet-tmpl/prog/en/includes/js-date-format.inc +++ a/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); --- a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/curbside_pickup.tt +++ a/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); --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation-home.tt +++ a/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
  • --- a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/curbside_pickups.tt +++ a/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' %] --