Koha/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-curbside-pickups.tt
Jonathan Druart c164194f41
Bug 31262: Curbside pickup - Remove day without slots
We disable dates from the date picker that do not have slots defined in
the configuration.

Note that dates that have slots configured but none are available will
still be displayed. To implement that we would need to calculate the
availability for all the dates displayed on the widget (1 month) and
that will (certainly) slow down considerabily the UI.

Test plan:
Configure curbside pickup for a given library. Define slots for several
days of the week (not all).
Schedule a pickup and confirm that only the days with slots defined are
available in the date picker widget

Sponsored-by: Association KohaLa - https://koha-fr.org/

Signed-off-by: David Nind <david@davidnind.com>

Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
2022-11-10 10:04:16 -03:00

476 lines
24 KiB
Text

[% 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 &rsaquo; [% IF ( LibraryNameTitle ) %][% LibraryNameTitle | html %][% ELSE %]Koha online[% END %] catalog</title>
[% FILTER collapse %]
<style>
.pickup_time input[type='radio'] {
display: none;
}
.pickup_time {
display: inline-block;
}
#pickup-times {
display: flex;
flex-wrap: wrap;
}
#pickup-time-slots {
max-width: 80%;
}
fieldset.rows .pickup_time label {
background: #e6e6e6 linear-gradient(180deg,#f0f0f0,#e6e6e6);
border: 1px solid #b3b3b3;
border-radius: 3px;
cursor: pointer;
float: none;
font-weight: normal;
padding: .2em .1em;
text-align: center;
width: 6rem;
}
fieldset.rows .pickup_time label:hover {
background: #e6e6e6 linear-gradient(180deg,#F7F7F7,#F0F0F0);
}
fieldset.rows .pickup_time label::before {
font-family: FontAwesome;
color: #AAA;
content: "\f1db";
display: inline-block;
padding-right: .5em;
}
fieldset.rows .pickup_time input:checked + label {
background: #5cb85c linear-gradient(180deg,#5cb85c,#4cae4c);
border: 1px solid #548e54;
color: #FFF;
}
fieldset.rows .pickup_time input:checked + label::before {
font-family: FontAwesome;
color: #FFF;
content: "\f05d";
display: inline-block;
padding-right: .5em;
}
fieldset.rows .pickup_time input:disabled + label {
background: #F0F0F0 none;
border: 1px solid #E6E6E6;
color: #6c6c6c;
}
fieldset.rows .pickup_time input:disabled + label:hover {
cursor: not-allowed;
}
fieldset.rows .pickup_time input:disabled + label::before {
content: "";
display: inline-block;
}
#existing-pickup-warning {
color: #c00;
display: inline-block;
}
.pickups_available {
font-size: 90%;
}
.pickups_available::before {
content: "(";
}
.pickups_available::after {
content: ")";
}
</style>
[% END %]
[% 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 %]
[% IF m.type == "error" %]
<div class="alert alert-warning">
[% ELSE %]
<div class="alert alert-info">
[% END %]
[% SWITCH m.code %]
[% CASE 'not_enabled' %]
<span>The curbside pickup feature is not enabled for this library.</span>
[% CASE 'library_is_closed' %]
<span>Cannot create a curbside pickup for this day, it is a holiday.</span>
[% CASE 'no_waiting_holds' %]
<span>This patron does not have waiting holds.</span>
[% CASE 'too_many_pickups' %]
<span>You already have a scheduled pickup for this library.</span>
[% CASE 'no_matching_slots' %]
<span>Wrong slot selected.</span>
[% CASE 'no_more_pickups_available' %]
<span>There are no more pickups available for this slot. Please choose another one.</span>
[% CASE 'cannot_checkout' %]
<span>Unable to check the items out to [% INCLUDE 'patron-title.inc' patron=m.patron %]</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">
[% IF patron_curbside_pickups.count %]
<a href="#user-pickups" class="nav-link active" aria-controls="user-pickups" aria-selected="true" role="tab" data-toggle="tab">Your pickups</a>
[% ELSE %]
<a href="#user-pickups" class="nav-link" aria-controls="user-pickups" role="tab" data-toggle="tab">Your pickups</a>
[% END %]
</li>
[% IF policies.count %]
<li class="nav-item" role="presentation" id="tab-user-schedule-pickup">
[% IF patron_curbside_pickups.count %]
<a href="#user-schedule-pickup" class="nav-link" aria-controls="user-schedule-pickup" role="tab" data-toggle="tab">Schedule a pickup</a>
[% ELSE %]
<a href="#user-schedule-pickup" class="nav-link active" aria-controls="user-schedule-pickup" aria-selected="true" role="tab" data-toggle="tab">Schedule a pickup</a>
[% END %]
</li>
[% END %]
</ul>
<div class="tab-content">
[% IF patron_curbside_pickups.count %]
<div role="tabpanel" class="tab-pane active" id="user-pickups" aria-labelledby="tab-user-pickups">
[% ELSE %]
<div role="tabpanel" class="tab-pane" id="user-pickups" aria-labelledby="tab-user-pickups">
[% END %]
[% 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 %]
[% IF patron_curbside_pickups.count %]
<div role="tabpanel" class="tab-pane" id="user-schedule-pickup" aria-labelledby="tab-user-schedule-pickup">
[% ELSE %]
<div role="tabpanel" class="tab-pane active" id="user-schedule-pickup" aria-labelledby="tab-user-schedule-pickup">
[% END %]
<form id="create-pickup" method="post">
<fieldset class="rows">
<ol>
<li>
<label for="pickup-branch" class="required">Pickup library:</label>
<select name="pickup_branch" id="pickup-branch" required="required">
<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" style="display: none;">You already have a pickup scheduled for this library.</span>
<div class="required_label required">Required</div>
</li>
<li id="pickup_date_item">
<label for="pickup-date">Pickup date:</label>
<input name="pickup_date" type="text" class="flatpickr" id="pickup-date" required="required"/>
<div class="required_label required">Required</div>
</li>
<li id="pickup-times"></li>
<li id="pickup_notes_item">
<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 %]
[% Asset.js("lib/dayjs/plugin/customParseFormat.js") | $raw %]
<script>dayjs.extend(window.dayjs_plugin_isSameOrAfter)</script>
<script>dayjs.extend(window.dayjs_plugin_customParseFormat)</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;
}, {});
let can_schedule_at = {};
[% 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;
[% IF p.enable_waiting_holds_only %]
[% SET waiting_holds = logged_in_user.holds.search( found => 'W', branchcode => p.branchcode ) %]
[% UNLESS waiting_holds.count %]
policies['[% p.branchcode | html %]'].enabled = 0;
[% END %]
[% END %]
[% 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);
});
$(document).ready(function() {
$("#pickup-branch option").each(function(){
if ( $(this).val() != "" && !policies[$(this).val()].enabled ) {
$(this).prop("disabled", "disabled");
$(this).attr("title", _("You don't have waiting holds at this library"));
}
});
$('#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,#pickup_date_item,#pickup_notes_item").hide();
} else {
$('#existing-pickup-warning').hide();
$("#pickup-date,#pickup_date_item").show();
}
});
const pickupDate_fp = document.getElementById("pickup-date")._flatpickr;
pickupDate_fp.set('disable', [function(date) {
return !slots_per_day.hasOwnProperty(date.getDay());
}]);
pickupDate_fp.config.onClose.push(function( selectedDates, dateStr, instance ){
/* Here we add an onClose event to the existing flatpickr instance */
/* It fires after the user has selected a date from the calendar popup */
$('#pickup-times').html("<label>" + _("Select a time") + ":</label><div id=\"pickup-time-slots\"></div>");
$('#schedule-pickup-button').prop( 'disabled', 1 );
var currentDate = dateStr;
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 defined for this day.")+"</div>");
return;
}
policy.slots_per_day[dow].forEach(function(slot){
let pickup_interval = policy.pickup_interval;
if (!pickup_interval) {
$('#pickup-times').html("<div>"+_("No pickup time defined for this day.")+"</div>");
return;
}
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;
let display_slot = true;
if (pickupIntervalStartMoment.isBefore(now)) {
// Slots in the past are unavailable
available = false;
display_slot = 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;
}
if ( display_slot ) {
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 pickups_available = policy.patrons_per_interval - pickups_scheduled;
let disabled = pickupSlot.available ? "" : "disabled";
$("#pickup-time-slots").append(`<span class="pickup_time"><input type="radio" id="slot_${i}" name="pickup_time" value="${optValue}" ${disabled} /> <label class="pickup_select" for="slot_${i}" data-toggle="tooltip" title="` + _("Appointments available: ") + `${pickups_available}">${optText} <span class="pickups_available">${pickups_available}</span></label></span>`);
}
$("#pickup_notes_item,#pickup-times").show();
});
$("#pickup_date_item,#pickup_notes_item,#pickup-times").hide();
$("#create-pickup").on('submit', function(){
if ( ! $("input[type='radio']:checked").length ) {
alert(_("Please select a date and a pickup time"));
return false;
}
return true;
});
$("#pickup-times").tooltip({
selector: ".pickup_select"
});
});
</script>
[% END %]