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> Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
This commit is contained in:
parent
572df4f172
commit
8a712ede28
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 %]
|
||||
|
|
@ -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 };
|
||||
Loading…
Reference in New Issue