19ebcd4eb5
To activate Curbside pickups
1- Enable the CurbsidePickup system preference
1.1 Go to Administration > Global system preferences
1.2 Search for CurbsidePickup
1.3 Change the value for 'Enable'
1.4 Click on 'Save all circulation preferences'
2- Configure time slots for at least one library
2.1 Go to Administration > Curbside pickup
2.2 Fill out the form for Centerville (or another library)
Enable: Check
Pickup interval: 10 (or other)
Maximum boss per interval: 3 (or other)
Patron-scheduled pickup: Check
Enable for waiting holds only: DO NOT check
2.3 Add a time slot
In 'New slot', enter
Monday
From: 10:00
To: 12:00
Click on 'Add'
3- Make an appointment from the OPAC
3.1 Go to OPAC
3.2 Open mobile mode
3.3 Connect with a user
3.4 Click on 'Curbside pickups'
3.5 Choose the library in 'Pick a library'
3.6 Choose a date in 'Pickup date'
3.7 Choose a time range in 'Select a time'
3.8 Click on 'Schedule pickup'
=> Notice The table is not responsive
4- Apply the patch
5- Execute 'yarn build --view opac'
6- Clean your cache or open your navigator on private mode (to load updated css files)
7- Perform step 3.1, 3.2, 3.3, 3.4,
8- click on 'Your pickups'
=> Confirm that the CurbsidePickup table are now displayed correctly and is now responsive.
9- Click on 'Charges'
=> Confirm that the Charges table are now displayed correctly and is now responsive.
I correct a little bug. The "suspend_hold" button did not work anymore. This change resets it to it's original state.
Signed-off-by: David Cook <dcook@prosentient.com.au>
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
Signed-off-by: Tomas Cohen Arazi <tomascohen@theke.io>
(cherry picked from commit 16008c24c5
)
Signed-off-by: Martin Renvoize <martin.renvoize@ptfs-europe.com>
494 lines
25 KiB
Text
494 lines
25 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 › [% 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>
|
|
<th></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>
|
|
<td></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" data-flatpickr-futureinclusive="true" />
|
|
<span class="required">Required</span>
|
|
</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' %]
|
|
[% INCLUDE 'datatables.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() {
|
|
$('#pickups-table').dataTable($.extend(true, {}, dataTablesDefaults, {
|
|
"searching": false,
|
|
"paging": false,
|
|
"info": false,
|
|
"autoWidth": false,
|
|
"responsive": {
|
|
"details": { "type": 'column',"target": -1 }
|
|
},
|
|
"columnDefs": [
|
|
{ "className": 'dtr-control', "orderable": false, "targets": -1 }
|
|
],
|
|
}));
|
|
$("#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"));
|
|
}
|
|
});
|
|
|
|
const pickupDate_fp = document.getElementById("pickup-date")._flatpickr;
|
|
|
|
$('#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();
|
|
}
|
|
|
|
pickupDate_fp.set('disable', [function(date) {
|
|
return !policies[branchcode].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].sort((a, b) => a.start_hour - b.start_hour).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 %]
|