6 [% USE AdditionalContents %]
7 [% SET OpacNav = AdditionalContents.get( location => "OpacNav", lang => lang, library => logged_in_user.branchcode || default_branch, blocktitle => 0 ) %]
8 [% SET OpacNavBottom = AdditionalContents.get( location => "OpacNavBottom", lang => lang, library => logged_in_user.branchcode || default_branch, blocktitle => 0 ) %]
9 [% INCLUDE 'doc-head-open.inc' %]
10 <title>Your curbside pickups › [% IF ( LibraryNameTitle ) %][% LibraryNameTitle | html %][% ELSE %]Koha online[% END %] catalog</title>
13 .pickup_time input[type='radio'] {
17 display: inline-block;
26 fieldset.rows .pickup_time label {
27 background: #e6e6e6 linear-gradient(180deg,#f0f0f0,#e6e6e6);
28 border: 1px solid #b3b3b3;
37 fieldset.rows .pickup_time label:hover {
38 background: #e6e6e6 linear-gradient(180deg,#F7F7F7,#F0F0F0);
40 fieldset.rows .pickup_time label::before {
41 font-family: FontAwesome;
44 display: inline-block;
47 fieldset.rows .pickup_time input:checked + label {
48 background: #5cb85c linear-gradient(180deg,#5cb85c,#4cae4c);
49 border: 1px solid #548e54;
52 fieldset.rows .pickup_time input:checked + label::before {
53 font-family: FontAwesome;
56 display: inline-block;
59 fieldset.rows .pickup_time input:disabled + label {
60 background: #F0F0F0 none;
61 border: 1px solid #E6E6E6;
64 fieldset.rows .pickup_time input:disabled + label:hover {
67 fieldset.rows .pickup_time input:disabled + label::before {
69 display: inline-block;
71 #existing-pickup-warning {
73 display: inline-block;
78 .pickups_available::before {
81 .pickups_available::after {
86 [% INCLUDE 'doc-head-close.inc' %]
87 [% BLOCK cssinclude %][% END %]
89 [% INCLUDE 'bodytag.inc' bodyid='opac-curside-pickups' bodyclass='scrollto' %]
90 [% INCLUDE 'masthead.inc' %]
92 <nav id="breadcrumbs" aria-label="Breadcrumb" class="breadcrumbs">
93 <ol class="breadcrumb">
94 <li class="breadcrumb-item">
95 <a href="/cgi-bin/koha/opac-main.pl">Home</a>
97 <li class="breadcrumb-item">
98 <a href="/cgi-bin/koha/opac-user.pl">[% INCLUDE 'patron-title.inc' patron = logged_in_user %]</a>
101 <li class="breadcrumb-item active">
102 <a href="#" aria-current="page">Curbside pickups</a>
104 </ol> <!-- / .breadcrumb -->
105 </nav> <!-- /#breadcrumbs -->
107 <div class="container-fluid">
109 <div class="col col-lg-2 order-2 order-lg-1">
110 <div id="navigation">
111 [% INCLUDE 'navigation.inc' IsPatronPage=1 %]
114 <div class="col-md-12 col-lg-10 order-1">
115 <div id='pickupdetails' class="maincontent">
116 <h2>Curbside pickups</h2>
118 [% FOR m IN messages %]
119 [% IF m.type == "error" %]
120 <div class="alert alert-warning">
122 <div class="alert alert-info">
125 [% CASE 'not_enabled' %]
126 <span>The curbside pickup feature is not enabled for this library.</span>
127 [% CASE 'library_is_closed' %]
128 <span>Cannot create a curbside pickup for this day, it is a holiday.</span>
129 [% CASE 'no_waiting_holds' %]
130 <span>This patron does not have waiting holds.</span>
131 [% CASE 'too_many_pickups' %]
132 <span>You already have a scheduled pickup for this library.</span>
133 [% CASE 'no_matching_slots' %]
134 <span>Wrong slot selected.</span>
135 [% CASE 'no_more_pickups_available' %]
136 <span>There are no more pickups available for this slot. Please choose another one.</span>
137 [% CASE 'cannot_checkout' %]
138 <span>Unable to check the items out to [% INCLUDE 'patron-title.inc' patron=m.patron %]</span>
139 [% CASE 'library_notified' %]
140 <span>The library has been notified of your arrival.</span>
142 <span>[% m.code | html %]</span>
147 <div id="opac-pickups-views" class="toptabs">
148 <ul class="nav nav-tabs" role="tablist">
149 <li class="nav-item" role="presentation" id="tab-user-pickups">
150 [% IF patron_curbside_pickups.count %]
151 <a href="#user-pickups" class="nav-link active" aria-controls="user-pickups" aria-selected="true" role="tab" data-toggle="tab">Your pickups</a>
153 <a href="#user-pickups" class="nav-link" aria-controls="user-pickups" role="tab" data-toggle="tab">Your pickups</a>
157 [% IF policies.count %]
158 <li class="nav-item" role="presentation" id="tab-user-schedule-pickup">
159 [% IF patron_curbside_pickups.count %]
160 <a href="#user-schedule-pickup" class="nav-link" aria-controls="user-schedule-pickup" role="tab" data-toggle="tab">Schedule a pickup</a>
162 <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>
168 <div class="tab-content">
169 [% IF patron_curbside_pickups.count %]
170 <div role="tabpanel" class="tab-pane active" id="user-pickups" aria-labelledby="tab-user-pickups">
172 <div role="tabpanel" class="tab-pane" id="user-pickups" aria-labelledby="tab-user-pickups">
174 [% IF patron_curbside_pickups.count %]
175 <table id="pickups-table" class="table table-striped">
178 <th>Pickup library</td>
185 [% FOR p IN patron_curbside_pickups %]
187 <td>[% Branches.GetName(p.branchcode) | html %]</td>
188 <td>[% p.scheduled_pickup_datetime | $KohaDates with_hours => 1 %]</td>
189 <td>[% p.notes | html %]</td>
192 <input type="hidden" name="op" value="arrival-alert" />
193 <input type="hidden" name="pickup_id" value="[% p.id | html %]" />
194 [% IF ! p.staged_datetime || p.arrival_datetime %]
195 <button class="btn disabled" disabled href="#" >
197 <button type="submit" class="btn" href="#" >
199 <i class="fa fa-bell" aria-hidden="true"></i> Alert staff of your arrival
204 <input type="hidden" name="op" value="cancel-pickup" />
205 <input type="hidden" name="pickup_id" value="[% p.id | html %]" />
206 [% IF p.delivered_datetime %]
207 <button class="btn disabled" disabled href="#" >
209 <button type="submit" class="btn" href="#" >
211 <i class="fa fa-ban" aria-hidden="true"></i> Cancel this pickup</button>
219 <div>No curbside pickups.</div>
223 [% IF policies.count %]
224 [% IF patron_curbside_pickups.count %]
225 <div role="tabpanel" class="tab-pane" id="user-schedule-pickup" aria-labelledby="tab-user-schedule-pickup">
227 <div role="tabpanel" class="tab-pane active" id="user-schedule-pickup" aria-labelledby="tab-user-schedule-pickup">
229 <form id="create-pickup" method="post">
230 <fieldset class="rows">
233 <label for="pickup-branch" class="required">Pickup library:</label>
234 <select name="pickup_branch" id="pickup-branch" required="required">
235 <option value="">Select a library</option>
236 [% FOR p IN policies %]
237 <option value="[% p.branchcode | html %]">[% Branches.GetName(p.branchcode) | html %]</option>
241 <span id="existing-pickup-warning" class="required" style="display: none;">You already have a pickup scheduled for this library.</span>
242 <div class="required_label required">Required</div>
244 <li id="pickup_date_item">
245 <label for="pickup-date">Pickup date:</label>
246 <input name="pickup_date" type="text" class="flatpickr" id="pickup-date" required="required" data-flatpickr-futuredate="true" />
247 <div class="required_label required">Required</div>
250 <li id="pickup-times"></li>
252 <li id="pickup_notes_item">
253 <label for="notes">Notes:</label>
254 <input name="notes" id="notes" />
259 <fieldset class="action">
260 <input type="hidden" name="op" value="create-pickup" />
261 <input type="submit" id="schedule-pickup-button" class="btn btn-default" disabled value="Schedule pickup" />
269 </div> <!-- / .col-lg-10 -->
270 </div> <!-- / .row -->
271 </div> <!-- / .container-fluid -->
272 </div> <!-- / .main -->
274 [% INCLUDE 'opac-bottom.inc' %]
276 [% BLOCK jsinclude %]
277 [% Asset.js("lib/dayjs/dayjs.min.js") | $raw %]
278 [% Asset.js("lib/dayjs/plugin/isSameOrAfter.js") | $raw %]
279 [% Asset.js("lib/dayjs/plugin/customParseFormat.js") | $raw %]
280 <script>dayjs.extend(window.dayjs_plugin_isSameOrAfter)</script>
281 <script>dayjs.extend(window.dayjs_plugin_customParseFormat)</script>
282 [% INCLUDE 'calendar.inc' %]
284 [% SET pickup_exists_in = [] %]
285 [% FOR p IN patron_curbside_pickups %]
286 [% UNLESS p.delivered_by %]
287 [% pickup_exists_in.push(p.branchcode) %]
290 let pickup_exists_in = [% To.json(pickup_exists_in.unique()) | $raw %];
292 let pickups = [% To.json(curbside_pickups.unblessed) | $raw %];
293 let policies = [% To.json(policies.unblessed) | $raw %];
294 policies = policies.reduce((map, e) => {
295 map[e.branchcode] = e;
298 let can_schedule_at = {};
299 [% FOR p IN policies %]
300 var opening_slots = [% To.json(p.opening_slots.unblessed) | $raw %];
301 var slots_per_day = {};
302 opening_slots.forEach(function(slot){
304 if(!slots_per_day[day]) slots_per_day[day] = [];
305 slots_per_day[day].push(slot);
307 policies['[% p.branchcode | html %]'].slots_per_day = slots_per_day;
309 [% IF p.enable_waiting_holds_only %]
310 [% SET waiting_holds = logged_in_user.holds.search( found => 'W', branchcode => p.branchcode ) %]
311 [% UNLESS waiting_holds.count %]
312 policies['[% p.branchcode | html %]'].enabled = 0;
317 let existingPickupMoments = [];
318 pickups.forEach(function(pickup){
319 let scheduled_pickup_datetime = pickup.scheduled_pickup_datetime;
320 let pickupMoment = dayjs(scheduled_pickup_datetime);
322 if(!existingPickupMoments[pickup.branchcode]) existingPickupMoments[pickup.branchcode] = [];
323 existingPickupMoments[pickup.branchcode].push(pickupMoment);
326 $(document).ready(function() {
327 $("#pickup-branch option").each(function(){
328 if ( $(this).val() != "" && !policies[$(this).val()].enabled ) {
329 $(this).prop("disabled", "disabled");
330 $(this).attr("title", _("You don't have waiting holds at this library"));
334 $('#pickup-branch').on('change', function() {
335 let branchcode = $(this).val();
337 let existing_pickup = pickup_exists_in.indexOf(branchcode) != -1;
339 $('#pickup-date').val("");
340 $('#pickup-time').val("");
341 $('#pickup-times').hide();
342 $('#schedule-pickup-button').prop('disabled', true);
344 if (existing_pickup) {
345 $('#existing-pickup-warning').show();
346 $("#pickup-date,#pickup_date_item,#pickup_notes_item").hide();
348 $('#existing-pickup-warning').hide();
349 $("#pickup-date,#pickup_date_item").show();
353 const pickupDate_fp = document.getElementById("pickup-date")._flatpickr;
354 pickupDate_fp.set('disable', [function(date) {
355 return !slots_per_day.hasOwnProperty(date.getDay());
357 pickupDate_fp.config.onClose.push(function( selectedDates, dateStr, instance ){
358 /* Here we add an onClose event to the existing flatpickr instance */
359 /* It fires after the user has selected a date from the calendar popup */
360 $('#pickup-times').html("<label>" + _("Select a time") + ":</label><div id=\"pickup-time-slots\"></div>");
361 $('#schedule-pickup-button').prop( 'disabled', 1 );
363 var currentDate = dateStr;
364 let branchcode = $("#pickup-branch").val();
365 let policy = policies[branchcode];
367 let selectedDate = dayjs(currentDate);
369 let pickupSlots = [];
370 let available_count = 0;
371 let dow = selectedDate.day(); // Sunday is 0 (at least for now)
372 if (!policy.slots_per_day[dow]){
373 $('#pickup-times').html("<div>"+_("No pickup time defined for this day.")+"</div>");
377 policy.slots_per_day[dow].forEach(function(slot){
378 let pickup_interval = policy.pickup_interval;
379 if (!pickup_interval) {
380 $('#pickup-times').html("<div>"+_("No pickup time defined for this day.")+"</div>");
384 let listStartMoment = selectedDate.hour(slot.start_hour).minute(slot.start_minute);
385 let listEndMoment = selectedDate.hour(slot.end_hour).minute(slot.end_minute);
387 let keep_going = true;
390 // Initialize pickup slots starting at opening time
391 let pickupIntervalStartMoment = listStartMoment;
392 let pickupIntervalEndMoment = listStartMoment.add(pickup_interval, 'minutes');
394 let available = true;
395 let display_slot = true;
397 if (pickupIntervalStartMoment.isBefore(now)) {
398 // Slots in the past are unavailable
400 display_slot = false;
403 if (pickupIntervalEndMoment.isAfter(listEndMoment)) {
404 // Slots after the end of pickup times for the day are unavailable
408 let pickups_scheduled = 0;
410 if (existingPickupMoments[branchcode]){
411 existingPickupMoments[branchcode].forEach(function(pickupMoment){
412 // An existing pickup time
413 if (pickupMoment.isSameOrAfter(pickupIntervalStartMoment) && pickupMoment.isBefore(pickupIntervalEndMoment)) {
414 // This calculated pickup is in use by another scheduled pickup
420 if (pickups_scheduled >= policy.patrons_per_interval) {
424 if ( display_slot ) {
427 "available": available,
428 "moment": pickupIntervalStartMoment,
429 "pickups_scheduled": pickups_scheduled
438 pickupIntervalStartMoment = pickupIntervalEndMoment;
439 pickupIntervalEndMoment = pickupIntervalStartMoment.add(pickup_interval, 'minutes');
440 if (pickupIntervalEndMoment.isAfter(listEndMoment)) {
441 // This latest slot is after the end of pickup times for the day, so we can stop
446 $('#schedule-pickup-button').prop( 'disabled', available_count <= 0 );
449 for (let i = 0; i < pickupSlots.length; i++) {
450 let pickupSlot = pickupSlots[i];
451 let optText = pickupSlot.moment.format("HH:mm");
452 let optValue = pickupSlot.moment.format("YYYY-MM-DD HH:mm:ss");
453 let pickups_scheduled = pickupSlot.pickups_scheduled;
454 let pickups_available = policy.patrons_per_interval - pickups_scheduled;
455 let disabled = pickupSlot.available ? "" : "disabled";
456 $("#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>`);
459 $("#pickup_notes_item,#pickup-times").show();
462 $("#pickup_date_item,#pickup_notes_item,#pickup-times").hide();
464 $("#create-pickup").on('submit', function(){
465 if ( ! $("input[type='radio']:checked").length ) {
466 alert(_("Please select a date and a pickup time"));
471 $("#pickup-times").tooltip({
472 selector: ".pickup_select"