Bugzilla – Attachment 138342 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: OPAC view
Bug-30650-OPAC-view.patch (text/plain), 23.99 KB, created by
Katrin Fischer
on 2022-07-29 16:18:57 UTC
(
hide
)
Description:
Bug 30650: OPAC view
Filename:
MIME Type:
Creator:
Katrin Fischer
Created:
2022-07-29 16:18:57 UTC
Size:
23.99 KB
patch
obsolete
>From a53d757d8847e15a23e8c5c4553caff981918878 Mon Sep 17 00:00:00 2001 >From: Jonathan Druart <jonathan.druart@bugs.koha-community.org> >Date: Tue, 3 May 2022 22:05:58 +0200 >Subject: [PATCH] Bug 30650: OPAC view > >Sponsored-by: Association KohaLa - https://koha-fr.org/ > >Signed-off-by: Koha Team University Lyon 3 <koha@univ-lyon3.fr> > >Signed-off-by: Katrin Fischer <katrin.fischer@bsz-bw.de> >--- > .../bootstrap/en/includes/usermenu.inc | 9 + > .../en/modules/opac-curbside-pickups.tt | 354 ++++++++++++++++++ > opac/opac-curbside-pickups.pl | 128 +++++++ > 3 files changed, 491 insertions(+) > create mode 100644 koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-curbside-pickups.tt > create mode 100755 opac/opac-curbside-pickups.pl > >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc b/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc >index 433b80db82..d172132cb7 100644 >--- a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc >@@ -139,6 +139,15 @@ > <a href="/cgi-bin/koha/opac-illrequests.pl">your interlibrary loan requests</a></li> > [% END %] > >+ [% IF Koha.Preference( 'CurbsidePickup' ) %] >+ [% IF ( ermview ) %] >+ <li class="active"> >+ [% ELSE %] >+ <li> >+ [% END %] >+ <a href="/cgi-bin/koha/opac-curbside-pickups.pl">your curbside pickups</a></li> >+ [% END %] >+ > [% IF Koha.Preference('UseRecalls') %] > [% IF ( recallsview ) %] > <li class="active"> >diff --git a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-curbside-pickups.tt b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-curbside-pickups.tt >new file mode 100644 >index 0000000000..68a29987f6 >--- /dev/null >+++ b/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-curbside-pickups.tt >@@ -0,0 +1,354 @@ >+[% USE raw %] >+[% USE To %] >+[% USE Koha %] >+[% USE KohaDates %] >+[% USE Branches %] >+[% 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 ) %] >+[% INCLUDE 'doc-head-open.inc' %] >+<title>Your curbside pickups › [% IF ( LibraryNameTitle ) %][% LibraryNameTitle | html %][% ELSE %]Koha online[% END %] catalog</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; >+ } >+</style> >+ >+[% INCLUDE 'doc-head-close.inc' %] >+[% BLOCK cssinclude %][% END %] >+</head> >+[% INCLUDE 'bodytag.inc' bodyid='opac-curside-pickups' bodyclass='scrollto' %] >+[% INCLUDE 'masthead.inc' %] >+<div class="main"> >+ <nav id="breadcrumbs" aria-label="Breadcrumb" class="breadcrumbs"> >+ <ol class="breadcrumb"> >+ <li class="breadcrumb-item"> >+ <a href="/cgi-bin/koha/opac-main.pl">Home</a> >+ </li> >+ <li class="breadcrumb-item"> >+ <a href="/cgi-bin/koha/opac-user.pl">[% INCLUDE 'patron-title.inc' patron = logged_in_user %]</a> >+ </li> >+ >+ <li class="breadcrumb-item active"> >+ <a href="#" aria-current="page">Curbside pickups</a> >+ </li> >+ </ol> <!-- / .breadcrumb --> >+ </nav> <!-- /#breadcrumbs --> >+ >+ <div class="container-fluid"> >+ <div class="row"> >+ <div class="col col-lg-2 order-2 order-lg-1"> >+ <div id="navigation"> >+ [% INCLUDE 'navigation.inc' IsPatronPage=1 %] >+ </div> >+ </div> >+ <div class="col-md-12 col-lg-10 order-1"> >+ <div id='pickupdetails' class="maincontent"> >+ <h2>Curbside pickups</h2> >+ >+ [% FOR m IN messages %] >+ <div class="dialog [% m.type | html %]"> >+ [% SWITCH m.code %] >+ [% CASE 'too_many_pickups' %] >+ <span>You already have a scheduled pickup for this library.</span> >+ [% CASE 'cannot_checkout' %] >+ <span>Unable to check the items out. Please contact the library.</span> >+ [% CASE 'library_notified' %] >+ <span>The library has been notified of your arrival.</span> >+ [% CASE %] >+ <span>[% m.code | html %]</span> >+ [% END %] >+ </div> >+ [% END %] >+ >+ <div id="opac-pickups-views" class="toptabs"> >+ <ul class="nav nav-tabs" role="tablist"> >+ <li class="nav-item" role="presentation" id="tab-user-pickups"><a href="#user-pickups" class="nav-link" aria-controls="user-pickups" aria-selected="true" role="tab" data-toggle="tab">Your pickups</a></li> >+ [% IF policies.count %] >+ <li class="nav-item" role="presentation" id="tab-user-schedule-pickup"><a href="#user-schedule-pickup" class="nav-link" aria-controls="user-schedule-pickup" aria-selected="false" role="tab" data-toggle="tab">Schedule a pickup</a></li> >+ [% END %] >+ </ul> >+ >+ <div class="tab-content"> >+ <div role="tabpanel" class="tab-pane" id="user-pickups" aria-labelledby="tab-user-pickups"> >+ [% IF patron_curbside_pickups.count %] >+ <table id="pickups-table" class="table table-striped"> >+ <thead> >+ <tr> >+ <th>Pickup library</td> >+ <th>Schedule</th> >+ <th>Notes</th> >+ <th>Actions</th> >+ </tr> >+ </thead> >+ <tbody> >+ [% FOR p IN patron_curbside_pickups %] >+ <tr> >+ <td>[% Branches.GetName(p.branchcode) | html %]</td> >+ <td>[% p.scheduled_pickup_datetime | $KohaDates with_hours => 1 %]</td> >+ <td>[% p.notes | html %]</td> >+ <td> >+ <form method="post"> >+ <input type="hidden" name="op" value="arrival-alert" /> >+ <input type="hidden" name="pickup_id" value="[% p.id | html %]" /> >+ [% IF ! p.staged_datetime || p.arrival_datetime %] >+ <button class="btn disabled" disabled href="#" > >+ [% ELSE %] >+ <button type="submit" class="btn" href="#" > >+ [% END %] >+ <i class="fa fa-bell" aria-hidden="true"></i> Alert staff of your arrival >+ </button> >+ </form> >+ <p> >+ <form method="post"> >+ <input type="hidden" name="op" value="cancel-pickup" /> >+ <input type="hidden" name="pickup_id" value="[% p.id | html %]" /> >+ [% IF p.delivered_datetime %] >+ <button class="btn disabled" disabled href="#" > >+ [% ELSE %] >+ <button type="submit" class="btn" href="#" > >+ [% END %] >+ <i class="fa fa-ban" aria-hidden="true"></i> Cancel this pickup</button> >+ </form> >+ </td> >+ </tr> >+ [% END %] >+ </tbody> >+ </table> >+ [% ELSE %] >+ <div>No curbside pickups.</div> >+ [% END %] >+ </div> >+ >+ [% IF policies.count %] >+ <div role="tabpanel" class="tab-pane" id="user-schedule-pickup" aria-labelledby="tab-user-schedule-pickup"> >+ <form id="create-pickup" method="post"> >+ <fieldset class="rows"> >+ <ol> >+ <li> >+ <label for="pickup_branch">Pickup library:</label> >+ <select name="pickup_branch" id="pickup-branch"> >+ <option value="">Select a library</option> >+ [% FOR p IN policies %] >+ <option value="[% p.branchcode | html %]">[% Branches.GetName(p.branchcode) | html %]</option> >+ >+ [% END %] >+ </select> >+ <span id="existing-pickup-warning" class="required">You already have a pickup scheduled for this library.</span> >+ </li> >+ <li> >+ <label for="pickup_date">Pickup date:</label> >+ <input name="pickup_date" type="text" class="flatpickr" id="pickup-date" disabled required="required"/> >+ <span class="required">Required</span> >+ </li> >+ >+ <li id="pickup-times" class="radio"></li> >+ >+ <li> >+ <label for="notes">Notes:</label> >+ <input name="notes" id="notes" /> >+ </li> >+ </ol> >+ </fieldset> >+ >+ <fieldset class="action"> >+ <input type="hidden" name="op" value="create-pickup" /> >+ <input type="submit" id="schedule-pickup-button" class="btn btn-default" disabled value="Schedule pickup" /> >+ </fieldset> >+ </form> >+ </div> >+ [% END %] >+ </div> >+ </div> >+ </div> >+ </div> <!-- / .col-lg-10 --> >+ </div> <!-- / .row --> >+ </div> <!-- / .container-fluid --> >+</div> <!-- / .main --> >+ >+[% INCLUDE 'opac-bottom.inc' %] >+ >+[% BLOCK jsinclude %] >+ [% 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> >+ [% SET pickup_exists_in = [] %] >+ [% FOR p IN patron_curbside_pickups %] >+ [% UNLESS p.delivered_by %] >+ [% pickup_exists_in.push(p.branchcode) %] >+ [% END %] >+ [% END %] >+ let pickup_exists_in = [% To.json(pickup_exists_in.unique()) | $raw %]; >+ >+ let pickups = [% To.json(curbside_pickups.unblessed) | $raw %]; >+ let policies = [% To.json(policies.unblessed) | $raw %]; >+ policies = policies.reduce((map, e) => { >+ map[e.branchcode] = e; >+ return map; >+ }, {}); >+ [% FOR p IN policies %] >+ var opening_slots = [% To.json(p.opening_slots.unblessed) | $raw %]; >+ var 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); >+ }); >+ policies['[% p.branchcode | html %]'].slots_per_day = slots_per_day; >+ [% END %] >+ >+ let existingPickupMoments = []; >+ pickups.forEach(function(pickup){ >+ let scheduled_pickup_datetime = pickup.scheduled_pickup_datetime; >+ let pickupMoment = dayjs(scheduled_pickup_datetime); >+ >+ if(!existingPickupMoments[pickup.branchcode]) existingPickupMoments[pickup.branchcode] = []; >+ existingPickupMoments[pickup.branchcode].push(pickupMoment); >+ }); >+ >+ $('#pickup-branch').on('change', function() { >+ let branchcode = $(this).val(); >+ >+ let existing_pickup = pickup_exists_in.indexOf(branchcode) != -1; >+ >+ $('#pickup-date').val(""); >+ $('#pickup-time').val(""); >+ $('#pickup-times').hide(); >+ $('#schedule-pickup-button').prop('disabled', true); >+ >+ if (existing_pickup) { >+ $('#existing-pickup-warning').show(); >+ $('#pickup-date').prop("disabled", true); >+ } else { >+ $('#existing-pickup-warning').hide(); >+ $('#pickup-date').prop("disabled", branchcode == ""); >+ } >+ }); >+ >+ $("#pickup-date").on('change', function() { >+ >+ $('#pickup-times').empty(); >+ $('#schedule-pickup-button').prop( 'disabled', 1 ); >+ >+ var currentDate = $(this).val(); >+ let branchcode = $("#pickup-branch").val(); >+ let policy = policies[branchcode]; >+ >+ let selectedDate = dayjs(currentDate); >+ >+ let pickupSlots = []; >+ let available_count = 0; >+ let dow = selectedDate.day(); // Sunday is 0 (at least for now) >+ if (!policy.slots_per_day[dow]){ >+ $('#pickup-times').html("<div>"+_("No pickup time define for this day.")+"</div>"); >+ return; >+ } >+ >+ policy.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; >+ >+ if (existingPickupMoments[branchcode]){ >+ existingPickupMoments[branchcode].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; >+ } >+ } >+ >+ $('#schedule-pickup-button').prop( 'disabled', available_count <= 0 ); >+ }); >+ >+ 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(); >+ }); >+ >+ >+ $(document).ready(function() { >+ $("#existing-pickup-warning").hide(); >+ $("#pickup-times").hide(); >+ if( $("#opac-pickups-views .tab-pane.active").length < 1 ){ >+ $("#opac-pickups-views a:first").tab("show"); >+ } >+ >+ $("#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 %] >diff --git a/opac/opac-curbside-pickups.pl b/opac/opac-curbside-pickups.pl >new file mode 100755 >index 0000000000..e2801aaf9a >--- /dev/null >+++ b/opac/opac-curbside-pickups.pl >@@ -0,0 +1,128 @@ >+#!/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::Members; >+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 ( $template, $borrowernumber, $cookie ) = get_template_and_user( >+ { >+ template_name => "opac-curbside-pickups.tt", >+ query => $input, >+ type => "opac", >+ } >+); >+ >+unless( C4::Context->preference('CurbsidePickup') ) { >+ print $input->redirect("/cgi-bin/koha/errors/404.pl"); >+ exit; >+} >+ >+my $op = $input->param('op') || 'list'; >+ >+my $logged_in_patron = Koha::Patrons->find($borrowernumber); >+my $branchcode = $input->param('pickup_branch'); >+my @messages; >+ >+if ( $op eq 'create-pickup' ) { >+ 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::CurbsidePickup::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-pickup' ) { >+ my $id = $input->param('pickup_id'); >+ my $curbside_pickup = Koha::CurbsidePickups->search( >+ { borrowernumber => $borrowernumber } )->find($id); >+ $curbside_pickup->delete >+ if $curbside_pickup >+ && !$curbside_pickup->delivered_datetime; >+} >+elsif ( $op eq 'arrival-alert' ) { >+ my $id = $input->param('pickup_id'); >+ my $curbside_pickup = Koha::CurbsidePickups->search( >+ { borrowernumber => $borrowernumber } )->find($id); >+ $curbside_pickup->mark_patron_has_arrived if $curbside_pickup; >+ push @messages, { >+ type => 'message', >+ code => 'library_notified', >+ } >+} >+ >+$template->param( >+ policies => Koha::CurbsidePickupPolicies->search( >+ { >+ enabled => 1, >+ patron_scheduled_pickup => 1, >+ } >+ ), >+ patron_curbside_pickups => Koha::CurbsidePickups->search( >+ { >+ borrowernumber => $logged_in_patron->borrowernumber, >+ scheduled_pickup_datetime => { '>' => \'DATE(NOW())' }, >+ }, >+ { >+ order_by => { -asc => 'scheduled_pickup_datetime' } >+ } >+ ), >+ curbside_pickups => Koha::CurbsidePickups->search( >+ { >+ scheduled_pickup_datetime => { '>' => \'DATE(NOW())' }, >+ }, >+ { >+ order_by => { -asc => 'scheduled_pickup_datetime' } >+ } >+ ), >+); >+ >+output_html_with_http_headers $input, $cookie, $template->output, undef, >+ { force_no_caching => 1 }; >-- >2.30.2
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