Bugzilla – Attachment 136593 Details for
Bug 30650
Add a curbside pickup module
Home
|
New
|
Browse
|
Search
|
[?]
|
Reports
|
Help
|
New Account
|
Log In
[x]
|
Forgot Password
Login:
[x]
[patch]
Bug 30650: Add circulation page view
Bug-30650-Add-circulation-page-view.patch (text/plain), 51.17 KB, created by
Jonathan Druart
on 2022-06-27 13:25:37 UTC
(
hide
)
Description:
Bug 30650: Add circulation page view
Filename:
MIME Type:
Creator:
Jonathan Druart
Created:
2022-06-27 13:25:37 UTC
Size:
51.17 KB
patch
obsolete
>From 74aa2e4c48ff3b036eba477a8cea537be8a9337b Mon Sep 17 00:00:00 2001 >From: Jonathan Druart <jonathan.druart@bugs.koha-community.org> >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 <http://www.gnu.org/licenses>. >+ >+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 = $('<li><label>'+get_day_lib(day)+'<label></li>'); > slots_per_day[day].forEach(function(slot) { > let span_node = $('<span class="pickup-slot"></span>'); >- span_node.append('<input type="hidden" class="pickup-slot" name="pickup-slot-'+branchcode+'" value="'+slot+'"/>'); >+ span_node.append('<input type="hidden" name="pickup-slot-'+branchcode+'" value="'+slot+'"/>'); > span_node.append('<span>'+format_slot(slot)+'</span>'); > >- let delete_link = $('<a href="#" on><i class="fa fa-trash" aria-hidden="true"></i>').on('click', function(e){ e.preventDefault; delete_slot($(this).closest('li'), branchcode); }); >+ let delete_link = $('<a href="#" on><i class="fa fa-trash" aria-hidden="true"></i>').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 @@ > <li> > <a class="circ-button" href="/cgi-bin/koha/circ/waitingreserves.pl"><i class="fa fa-calendar"></i> Holds awaiting pickup</a> > </li> >+ [% IF ( Koha.Preference('CurbsidePickup') && CAN_user_parameters_manage_curbside_pickups ) %] >+ <li> >+ <a class="circ-button" href="/cgi-bin/koha/circ/curbside_pickups.pl"><i class="fa fa-refresh"></i> Curbside Pickups</a> >+ </li> >+ [% END %] > <li> > <a class="circ-button" href="/cgi-bin/koha/circ/reserveratios.pl"><i class="fa fa-line-chart"></i> Hold ratios</a> > </li> >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' %] >+<title>Curbside pickups › Circulation › Koha</title> >+<style> >+ .pickup_time input[type='radio'] { >+ display: none; >+ } >+ .pickup_time { >+ margin: .2em; >+ } >+ .pickup_time label { >+ background-color: #ffffcc; >+ display: inline-block; >+ cursor: pointer; >+ } >+ .pickup_time input[type='radio']:checked + label { >+ background-color: #bcdb89; >+ } >+ .pickup_time input[type='radio']:disabled+ label { >+ background-color: #ff9090; >+ } >+</style> >+[% INCLUDE 'doc-head-close.inc' %] >+</head> >+ >+[% SET today_iso = date.format(date.now, format = '%Y-%m-%d') %] >+ >+<body id="circ_curbside-pickups" class="circ"> >+ [% INCLUDE 'header.inc' %] >+ [% INCLUDE 'cat-search.inc' %] >+ >+ >+<nav id="breadcrumbs" aria-label="Breadcrumb" class="breadcrumb"> >+ <ol> >+ <li> >+ <a href="/cgi-bin/koha/mainpage.pl">Home</a> >+ </li> >+ <li> >+ <a href="/cgi-bin/koha/circ/circulation-home.pl">Circulation</a> >+ </li> >+ <li> >+ <a href="#" aria-current="page">Curbside pickups</a> >+ </li> >+ </ol> >+</nav> >+ >+<div class="main container-fluid"> >+ <div class="row"> >+ <div class="col-md-10 col-md-offset-1 col-lg-10 col-lg-offset-1"> >+ <main> >+ >+ <h1>Curbside pickups</h1> >+ >+ [% UNLESS policy.enabled %] >+ <div class="dialog alert"> >+ Curbside pickups are not enabled for your location. >+ </div> >+ [% INCLUDE 'intranet-bottom.inc' %] >+ [% STOP %] >+ [% END %] >+ >+ [% FOR m IN messages %] >+ <div class="dialog [% m.type | html %]"> >+ [% SWITCH m.code %] >+ [% CASE 'too_many_pickups' %] >+ <span>This patron already has a scheduled pickup for this library.</span> >+ [% CASE 'cannot_checkout' %] >+ <span>Unable to check the items out to [% INCLUDE 'patron-title.inc' patron=m.patron %]</span> >+ [% CASE 'no_patron_found' %] >+ <span>No patron found with cardnumber [% m.cardnumber | html %].</span> >+ [% CASE %] >+ <span>[% m.code | html %]</span> >+ [% END %] >+ </div> >+ [% 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 %] >+ <div id="pickup-tabs" class="toptabs"> >+ <ul class="nav nav-tabs" role="tablist"> >+ [% IF !tab OR tab == 'to-be-staged' %] >+ <li role="presentation" class="active"> >+ [% ELSE %] >+ <li role="presentation"> >+ [% END %] >+ <a id="to-be-staged-tab" href="#to-be-staged" role="tab" data-toggle="tab">To be staged ([% to_be_staged.count | html %])</a> >+ </li> >+ [% IF tab == 'staged-and-ready' %] >+ <li role="presentation" class="active"> >+ [% ELSE %] >+ <li role="presentation"> >+ [% END %] >+ <a id="staged-and-ready-tab" href="#staged-and-ready" role="tab" data-toggle="tab">Staged & ready ([% staged_and_ready.count | html %])</a> >+ </li> >+ [% IF tab == 'patron-is-outside' %] >+ <li role="presentation" class="active"> >+ [% ELSE %] >+ <li role="presentation"> >+ [% END %] >+ <a id="patron-is-outside-tab" href="#patron-is-outside" role="tab" data-toggle="tab">Patron is outside ([% patron_outside.count | html %])</a> >+ </li> >+ [% IF tab == 'delivered-today' %] >+ <li role="presentation" class="active"> >+ [% ELSE %] >+ <li role="presentation"> >+ [% END %] >+ <a id="delivered-today-tab" href="#delivered-today" role="tab" data-toggle="tab">Delivered today ([% delivered_today.count | html %])</a> >+ </li> >+ [% IF tab == 'schedule-pickup' %] >+ <li role="presentation" class="active"> >+ [% ELSE %] >+ <li role="presentation"> >+ [% END %] >+ <a id="schedule-pickup-tab" href="#schedule-pickup" role="tab" data-toggle="tab">Schedule pickup</a> >+ </li> >+ </ul> >+ >+ <div class="tab-content"> >+ [% IF !tab OR tab == 'to-be-staged' %] >+ <div id="to-be-staged" role="tabpanel" class="tab-pane active"> >+ [% ELSE %] >+ <div id="to-be-staged" role="tabpanel" class="tab-pane"> >+ [% END %] >+ <form method="post" class="form"> >+ <p> >+ <button type="submit" class="btn btn-default"><i class="fa fa-refresh" aria-hidden="true"></i> Refresh</button> >+ </p> >+ </form> >+ >+ [% IF to_be_staged.count %] >+ <table class="table table-striped"> >+ <thead> >+ <tr> >+ <th>Pickup Date/Time</th> >+ <th>Patron</th> >+ <th>Items for pickup</th> >+ <th>Action</th> >+ </tr> >+ </thead> >+ <tbody> >+ [% FOREACH cp IN to_be_staged %] >+ [% UNLESS cp.staged_datetime %] >+ <tr class="[% class | html %]"> >+ <td>[% cp.scheduled_pickup_datetime | $KohaDates with_hours = 1 %]</td> >+ <td> >+ <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% cp.borrowernumber | uri %]">[% cp.patron.firstname | html %] [% cp.patron.surname | html %] ([% cp.patron.cardnumber | html %])</a> >+ [% IF cp.notes %] >+ <br/> >+ Notes: [% cp.notes | html %] >+ [% END %] >+ </td> >+ <td> >+ [% SET waiting_holds = cp.patron.holds.search( found => 'W', branchcode => logged_in_user.branchcode ) %] >+ [% FOREACH h IN waiting_holds %] >+ <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% h.biblionumber | uri %]">[% h.biblio.title | html %]</a> ([% h.biblio.author | html %], <a href="/cgi-bin/koha/catalogue/moredetail.pl?itemnumber=[% h.itemnumber | html %]&biblionumber=[% h.biblionumber | html %]#item[% h.itemnumber | html %]">[% h.item.barcode | html %]</a>)<br/> >+ [% END %] >+ </td> >+ <td> >+ <form method="post" class="form"> >+ <input type="hidden" name="op" value="mark-as-staged"/> >+ <input type="hidden" name="tab" value="to-be-staged"/> >+ <input type="hidden" name="id" value="[% cp.id | html %]"/> >+ <p> >+ <button type="submit" class="btn btn-default mark-as-staged-and-ready-btn"><i class="fa fa-check" aria-hidden="true"></i> Mark as <i>staged & ready</i></button> >+ </p> >+ </form> >+ >+ <form method="post" class="form"> >+ <input type="hidden" name="op" value="cancel"/> >+ <input type="hidden" name="tab" value="to-be-staged"/> >+ <input type="hidden" name="id" value="[% cp.id | html %]"/> >+ <p> >+ <button type="submit" class="btn btn-default cancel-btn"><i class="fa fa-ban" aria-hidden="true"></i> Cancel</button> >+ </p> >+ </form> >+ </td> >+ </tr> >+ [% END %] >+ [% END %] >+ </tbody> >+ </table> >+ [% ELSE %] >+ <span>There are no pickups to be staged.</span> >+ [% END %] >+ </div> >+ >+ [% IF tab == "staged-and-ready" %] >+ <div id="staged-and-ready" role="tabpanel" class="tab-pane active"> >+ [% ELSE %] >+ <div id="staged-and-ready" role="tabpanel" class="tab-pane"> >+ [% END %] >+ <form method="post" class="form"> >+ <input type="hidden" name="tab" value="staged-and-ready"/> >+ <p> >+ <button type="submit" class="btn btn-default"><i class="fa fa-refresh" aria-hidden="true"></i> Refresh</button> >+ </p> >+ </form> >+ >+ [% IF staged_and_ready.count %] >+ <table class="table table-striped"> >+ <thead> >+ <tr> >+ <th>Pickup Date/Time</th> >+ <th>Patron</th> >+ <th>Items for pickup</th> >+ <th>Staged by</th> >+ <th>Action</th> >+ </tr> >+ </thead> >+ <tbody> >+ [% FOREACH cp IN staged_and_ready %] >+ [% IF cp.staged_datetime && !cp.arrival_datetime %] >+ <tr class="[% class | html %]"> >+ <td>[% cp.scheduled_pickup_datetime | $KohaDates with_hours = 1 %]</td> >+ <td> >+ <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% cp.borrowernumber | uri %]">[% cp.patron.firstname | html %] [% cp.patron.surname | html %] ([% cp.patron.cardnumber | html %])</a> >+ [% IF cp.notes %] >+ <br/> >+ Notes: [% cp.notes | html %] >+ [% END %] >+ </td> >+ <td> >+ [% SET waiting_holds = cp.patron.holds.search( found => 'W', branchcode => logged_in_user.branchcode ) %] >+ [% FOREACH h IN waiting_holds %] >+ <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% h.biblionumber | uri %]">[% h.biblio.title | html %]</a> ([% h.biblio.author | html %], <a href="/cgi-bin/koha/catalogue/moredetail.pl?itemnumber=[% h.itemnumber | html %]&biblionumber=[% h.biblionumber | html %]#item[% h.itemnumber | html %]">[% h.item.barcode | html %]</a>)<br/> >+ [% END %] >+ </td> >+ <td> >+ [% cp.staged_by_staff.firstname | html %] [% cp.staged_by_staff.surname | html %] >+ </td> >+ <td> >+ <form method="post" class="form"> >+ <input type="hidden" name="op" value="mark-patron-has-arrived"/> >+ <input type="hidden" name="tab" value="staged-and-ready"/> >+ <input type="hidden" name="id" value="[% cp.id | html %]"/> >+ <p> >+ <button type="submit" class="btn btn-default patron-has-arrived-btn"><i class="fa fa-map-marker" aria-hidden="true"></i> Patron has arrived</button> >+ </p> >+ </form> >+ >+ <form method="post" class="form"> >+ <input type="hidden" name="op" value="mark-as-delivered"/> >+ <input type="hidden" name="tab" value="staged-and-ready"/> >+ <input type="hidden" name="id" value="[% cp.id | html %]"/> >+ <p> >+ <button type="submit" class="btn btn-default mark-as-delivered-btn"><i class="fa fa-envelope" aria-hidden="true"></i> Mark as <i>delivered</i></button> >+ </p> >+ </form> >+ >+ <form method="post" class="form"> >+ <input type="hidden" name="op" value="mark-as-unstaged"/> >+ <input type="hidden" name="tab" value="staged-and-ready"/> >+ <input type="hidden" name="id" value="[% cp.id | html %]"/> >+ <p> >+ <button type="submit" class="btn btn-default mark-as-to-be-staged-btn"><i class="fa fa-undo" aria-hidden="true"></i> Mark as <i>to be staged</i></button> >+ </p> >+ </form> >+ </td> >+ </tr> >+ [% END %] >+ [% END %] >+ </tbody> >+ </table> >+ [% ELSE %] >+ <span>There are no pickups staged and ready.</span> >+ [% END %] >+ </div> >+ >+ [% IF tab == "patron-is-outside" %] >+ <div id="patron-is-outside" role="tabpanel" class="tab-pane active"> >+ [% ELSE %] >+ <div id="patron-is-outside" role="tabpanel" class="tab-pane"> >+ [% END %] >+ <form method="post" class="form"> >+ <input type="hidden" name="tab" value="patron-is-outside"/> >+ <p> >+ <button type="submit" class="btn btn-default"><i class="fa fa-refresh" aria-hidden="true"></i> Refresh</button> >+ </p> >+ </form> >+ >+ [% IF patron_outside.count %] >+ <table class="table table-striped"> >+ <thead> >+ <tr> >+ <th>Pickup Date/Time</th> >+ <th>Patron</th> >+ <th>Items for pickup</th> >+ <th>Staged by</th> >+ <th>Action</th> >+ </tr> >+ </thead> >+ <tbody> >+ [% FOREACH cp IN patron_outside %] >+ [% IF cp.arrival_datetime && !cp.delivered_datetime %] >+ <tr class="[% class | html %]"> >+ <td>[% cp.scheduled_pickup_datetime | $KohaDates with_hours = 1 %]</td> >+ <td> >+ <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% cp.borrowernumber | uri %]">[% cp.patron.firstname | html %] [% cp.patron.surname | html %] ([% cp.patron.cardnumber | html %])</a> >+ [% IF cp.notes %] >+ <br/> >+ Notes: [% cp.notes | html %] >+ [% END %] >+ </td> >+ <td> >+ [% SET waiting_holds = cp.patron.holds.search( found => 'W', branchcode => logged_in_user.branchcode ) %] >+ [% FOREACH h IN waiting_holds %] >+ <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% h.biblionumber | uri %]">[% h.biblio.title | html %]</a> ([% h.biblio.author | html %], <a href="/cgi-bin/koha/catalogue/moredetail.pl?itemnumber=[% h.itemnumber | html %]&biblionumber=[% h.biblionumber | html %]#item[% h.itemnumber | html %]">[% h.item.barcode | html %]</a>)<br/> >+ [% END %] >+ </td> >+ <td> >+ [% cp.staged_by_staff.firstname | html %] [% cp.staged_by_staff.surname | html %] >+ </td> >+ <td> >+ <form method="post" class="form"> >+ <input type="hidden" name="op" value="mark-as-delivered"/> >+ <input type="hidden" name="tab" value="patron-is-outside"/> >+ <input type="hidden" name="id" value="[% cp.id | html %]"/> >+ <p> >+ <button type="submit" class="btn btn-default mark-as-delivered-btn"><i class="fa fa-envelope" aria-hidden="true"></i> Mark as delivered</button> >+ </p> >+ </form> >+ >+ <form method="post" class="form"> >+ <input type="hidden" name="op" value="mark-as-staged"/> >+ <input type="hidden" name="tab" value="patron-is-outside"/> >+ <input type="hidden" name="id" value="[% cp.id | html %]"/> >+ <p> >+ <button type="submit" class="btn btn-default mark-as-staged-and-ready-btn"><i class="fa fa-undo" aria-hidden="true"></i> Mark as <i>staged & ready</i></button> >+ </p> >+ </form> >+ >+ <form method="post" class="form"> >+ <input type="hidden" name="op" value="mark-as-unstaged"/> >+ <input type="hidden" name="tab" value="patron-is-outside"/> >+ <input type="hidden" name="id" value="[% cp.id | html %]"/> >+ <p> >+ <button type="submit" class="btn btn-default mark-as-to-be-staged-btn"><i class="fa fa-undo" aria-hidden="true"></i> Mark as <i>to be staged</i></button> >+ </p> >+ </form> >+ </td> >+ </tr> >+ [% END %] >+ [% END %] >+ </tbody> >+ </table> >+ [% ELSE %] >+ <span>There are no patrons waiting outside.</span> >+ [% END %] >+ </div> >+ >+ [% IF tab == "delivered-today" %] >+ <div id="delivered-today" role="tabpanel" class="tab-pane active"> >+ [% ELSE %] >+ <div id="delivered-today" role="tabpanel" class="tab-pane"> >+ [% END %] >+ <form method="post" class="form"> >+ <input type="hidden" name="tab" value="delivered-today"/> >+ <p> >+ <button type="submit" class="btn btn-default"><i class="fa fa-refresh" aria-hidden="true"></i> Refresh</button> >+ </p> >+ </form> >+ >+ [% IF delivered_today.count %] >+ <table class="table table-striped"> >+ <thead> >+ <tr> >+ <th>Deliver Date/Time</th> >+ <th>Patron</th> >+ <th>Items checked out</th> >+ </tr> >+ </thead> >+ <tbody> >+ [% FOREACH cp IN delivered_today %] >+ [% IF cp.delivered_datetime %] >+ <tr class="[% class | html %]"> >+ <td>[% cp.delivered_datetime | $KohaDates with_hours = 1 %]</td> >+ <td> >+ <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% cp.borrowernumber | uri %]">[% cp.patron.firstname | html %] [% cp.patron.surname | html %] ([% cp.patron.cardnumber | html %])</a> >+ [% IF cp.notes %] >+ <br/> >+ Notes: [% cp.notes | html %] >+ [% END %] >+ </td> >+ <td> >+ [% FOREACH c IN cp.checkouts %] >+ [% IF date.format(c.issuedate, format = '%Y-%m-%d') == today_iso %] >+ <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% c.item.biblionumber | uri %]">[% c.item.biblio.title | html %]</a> ([% c.item.biblio.author | html %], <a href="/cgi-bin/koha/catalogue/moredetail.pl?itemnumber=[% c.itemnumber | html %]&biblionumber=[% c.item.biblionumber | html %]#item[% c.itemnumber | html %]">[% c.item.barcode | html %]</a>)<br/> >+ [% END %] >+ [% END %] >+ </td> >+ </tr> >+ [% END %] >+ [% END %] >+ </tbody> >+ </table> >+ [% ELSE %] >+ <span>No pickups have been delivered today.</span> >+ [% END %] >+ </div> >+ >+ [% IF tab == "schedule-pickup" %] >+ <div id="schedule-pickup" role="tabpanel" class="tab-pane active"> >+ [% ELSE %] >+ <div id="schedule-pickup" role="tabpanel" class="tab-pane"> >+ [% END %] >+ [% IF !patron || ( patron && existing_curbside_pickups.count >= 1 ) %] >+ [% IF existing_curbside_pickups.count >= 1 %] >+ <div class="dialog alert"> >+ [% patron.firstname | html %] [% patron.surname | html %] ([% patron.cardnumber | html %]) already has a scheduled pickup for this library. >+ </div> >+ [% END %] >+ <form method="post" class="form-inline"> >+ <input type="hidden" name="op" value="find-patron"/> >+ <input type="hidden" name="tab" value="schedule-pickup"/> >+ >+ <div class="form-group"> >+ <label class="sr-only" for="input-patron-cardnumber">Cardnumber</label> >+ <div class="input-group"> >+ <div class="input-group-addon">Card number</div> >+ <input type="text" class="form-control" name="cardnumber" id="input-patron-cardnumber" placeholder="Enter patron cardnumber"/> >+ </div> >+ </div> >+ >+ <button type="submit" class="btn btn-default">Submit</button> >+ </form> >+ [% ELSE %] >+ <form id="create-pickup" method="post" class="form-horizontal"> >+ <fieldset class="rows" style="float: none;"> >+ <input type="hidden" name="borrowernumber" value="[% patron.id | html %]"/> >+ <input type="hidden" name="op" value="create-pickup"/> >+ <input type="hidden" name="tab" value="schedule-pickup"/> >+ <ol> >+ <li> >+ <label>Patron: </label> >+ <span>[% INCLUDE 'patron-title.inc' patron=patron %]</span> >+ </li> >+ >+ <li> >+ <label>Items ready for pickup: </label> >+ <span> >+ [% SET waiting_holds = patron.holds.search( found => 'W', branchcode => logged_in_user.branchcode ) %] >+ [% IF waiting_holds.count %] >+ [% FOREACH h IN waiting_holds %] >+ <p> >+ <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% h.biblionumber | uri %]">[% h.biblio.title | html %]</a> ([% h.biblio.author | html %], <a href="/cgi-bin/koha/catalogue/moredetail.pl?itemnumber=[% h.itemnumber | html %]&biblionumber=[% h.biblionumber | html %]#item[% h.itemnumber | html %]">[% h.item.barcode | html %]</a>) >+ </p> >+ [% END %] >+ [% ELSE %] >+ <span>There is no waiting holds for this patron at this location.</span> >+ [% END %] >+ </span> >+ </li> >+ >+ <li> >+ <label for="pickup_date">Pickup date: </label> >+ <input id="pickup_date" name="pickup_date" required="required" class="flatpickr" /> >+ </li> >+ >+ <li id="pickup-times" class="radio"></li> >+ >+ <li> >+ <label for="notes">Notes: </label> >+ <input id="notes" name="notes" type="text" /> >+ </li> >+ </ol> >+ </fieldset> >+ >+ <fieldset class="action"> >+ <input id="schedule-pickup-button" type="submit" value="Submit" /> >+ </fieldset> >+ >+ </form> >+ [% END %] >+ </div> >+ </div> >+ </div> >+ </main> >+ </div> >+ </div> <!-- /.row --> >+ >+[% MACRO jsinclude BLOCK %] >+ [% Asset.js("lib/dayjs/dayjs.min.js") | $raw %] >+ [% Asset.js("lib/dayjs/plugin/isSameOrAfter.js") | $raw %] >+ <script>dayjs.extend(window.dayjs_plugin_isSameOrAfter)</script> >+ [% INCLUDE 'calendar.inc' %] >+ <script> >+ let pickups = [% To.json(curbside_pickups.unblessed) | $raw %]; >+ let policy = [% To.json(policy.unblessed) | $raw %]; >+ >+ let existingPickupMoments = []; >+ pickups.forEach(function(pickup){ >+ let scheduled_pickup_datetime = pickup.scheduled_pickup_datetime; >+ let pickupMoment = dayjs(scheduled_pickup_datetime); >+ >+ existingPickupMoments.push(pickupMoment); >+ }); >+ >+ let opening_slots = [% To.json(policy.opening_slots.unblessed) | $raw %]; >+ let slots_per_day = {}; >+ opening_slots.forEach(function(slot){ >+ let day = slot.day; >+ if(!slots_per_day[day]) slots_per_day[day] = []; >+ slots_per_day[day].push(slot); >+ }); >+ >+ >+ $(document).ready(function() { >+ >+ $('#schedule-pickup-tab').on('click', function() { >+ $('#input-patron-cardnumber').focus(); >+ }); >+ >+ $("#pickup_date").on('change', function() { >+ >+ $('#pickup-times').empty(); >+ $('#schedule-pickup-button').prop( 'disabled', 1 ); >+ >+ var currentDate = $(this).val(); >+ >+ let selectedDate = dayjs(currentDate); >+ >+ let pickupSlots = []; >+ let available_count = 0; >+ let dow = selectedDate.day(); // Sunday is 0 (at least for now) >+ if (!slots_per_day[dow]){ >+ $('#pickup-times').html("<div>"+_("No pickup time define for this day.")+"</div>"); >+ return; >+ } >+ >+ slots_per_day[dow].forEach(function(slot){ >+ let pickup_interval = policy.pickup_interval; >+ >+ let listStartMoment = selectedDate.hour(slot.start_hour).minute(slot.start_minute); >+ let listEndMoment = selectedDate.hour(slot.end_hour).minute(slot.end_minute); >+ >+ let keep_going = true; >+ let now = dayjs(); >+ >+ // Initialize pickup slots starting at opening time >+ let pickupIntervalStartMoment = listStartMoment; >+ let pickupIntervalEndMoment = listStartMoment.add(pickup_interval, 'minutes'); >+ while (keep_going) { >+ let available = true; >+ >+ if (pickupIntervalStartMoment.isBefore(now)) { >+ // Slots in the past are unavailable >+ available = false; >+ } >+ >+ if (pickupIntervalEndMoment.isAfter(listEndMoment)) { >+ // Slots after the end of pickup times for the day are unavailable >+ available = false; >+ } >+ >+ let pickups_scheduled = 0; >+ existingPickupMoments.forEach(function(pickupMoment){ >+ // An existing pickup time >+ if (pickupMoment.isSameOrAfter(pickupIntervalStartMoment) && pickupMoment.isBefore(pickupIntervalEndMoment)) { >+ // This calculated pickup is in use by another scheduled pickup >+ pickups_scheduled++; >+ } >+ }); >+ >+ if (pickups_scheduled >= policy.patrons_per_interval) { >+ available = false; >+ } >+ >+ pickupSlots.push( >+ { >+ "available": available, >+ "moment": pickupIntervalStartMoment, >+ "pickups_scheduled": pickups_scheduled >+ } >+ ); >+ >+ if ( available ) { >+ available_count++; >+ } >+ >+ pickupIntervalStartMoment = pickupIntervalEndMoment; >+ pickupIntervalEndMoment = pickupIntervalStartMoment.add(pickup_interval, 'minutes'); >+ if (pickupIntervalEndMoment.isAfter(listEndMoment)) { >+ // This latest slot is after the end of pickup times for the day, so we can stop >+ keep_going = false; >+ } >+ } >+ }); >+ >+ for (let i = 0; i < pickupSlots.length; i++) { >+ let pickupSlot = pickupSlots[i]; >+ let optText = pickupSlot.moment.format("HH:mm"); >+ let optValue = pickupSlot.moment.format("YYYY-MM-DD HH:mm:ss"); >+ let pickups_scheduled = pickupSlot.pickups_scheduled; >+ let disabled = pickupSlot.available ? "" : "disabled"; >+ $("#pickup-times").append(`<span class="pickup_time"><input type="radio" id="slot_${i}" name="pickup_time" value="${optValue}" ${disabled} /> <label for="slot_${i}">${optText} (${pickups_scheduled})</label></span>`); >+ } >+ >+ $('#pickup-times').show(); >+ $('#schedule-pickup-button').prop( 'disabled', available_count <= 0 ); >+ }); >+ >+ $("#create-pickup").on('submit', function(){ >+ if ( ! $("input[type='radio']:checked").length ) { >+ alert(_("Please select a date and a pickup time")) >+ return false; >+ } >+ return true; >+ }); >+ }); >+ </script> >+[% END %] >+ >+ >+[% INCLUDE 'intranet-bottom.inc' %] >-- >2.25.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 30650
:
136584
|
136585
|
136586
|
136587
|
136588
|
136589
|
136590
|
136591
|
136592
|
136593
|
136594
|
136595
|
136596
|
136597
|
136598
|
136599
|
136600
|
136601
|
136602
|
136603
|
136604
|
136605
|
136606
|
136607
|
136608
|
136609
|
136610
|
137095
|
137100
|
137199
|
137211
|
137212
|
137277
|
137278
|
137279
|
137280
|
137281
|
137282
|
137283
|
137284
|
137285
|
137286
|
137287
|
137288
|
137289
|
137290
|
137291
|
137292
|
137293
|
137294
|
137295
|
137296
|
137297
|
137298
|
137299
|
137300
|
137301
|
137302
|
137303
|
137304
|
137305
|
137306
|
137307
|
138261
|
138262
|
138263
|
138264
|
138265
|
138266
|
138267
|
138268
|
138269
|
138270
|
138271
|
138272
|
138273
|
138274
|
138275
|
138276
|
138277
|
138278
|
138279
|
138280
|
138281
|
138282
|
138283
|
138284
|
138285
|
138286
|
138287
|
138288
|
138289
|
138290
|
138291
|
138292
|
138319
|
138320
|
138321
|
138322
|
138323
|
138324
|
138325
|
138330
|
138331
|
138332
|
138333
|
138334
|
138335
|
138336
|
138337
|
138338
|
138339
|
138340
|
138341
|
138342
|
138343
|
138344
|
138345
|
138346
|
138347
|
138348
|
138349
|
138350
|
138351
|
138352
|
138353
|
138354
|
138355
|
138356
|
138357
|
138358
|
138359
|
138360
|
138361
|
138362
|
138363
|
138364
|
138365
|
138366
|
138367
|
138368
|
138369
|
138375
|
138401
|
138610