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;
27 fieldset.rows .pickup_time label {
28 background: #e6e6e6 linear-gradient(180deg,#f0f0f0,#e6e6e6);
29 border: 1px solid #b3b3b3;
38 fieldset.rows .pickup_time label:hover {
39 background: #e6e6e6 linear-gradient(180deg,#F7F7F7,#F0F0F0);
41 fieldset.rows .pickup_time label::before {
42 font-family: FontAwesome;
45 display: inline-block;
48 fieldset.rows .pickup_time input:checked + label {
49 background: #5cb85c linear-gradient(180deg,#5cb85c,#4cae4c);
50 border: 1px solid #548e54;
53 fieldset.rows .pickup_time input:checked + label::before {
54 font-family: FontAwesome;
57 display: inline-block;
60 fieldset.rows .pickup_time input:disabled + label {
61 background: #F0F0F0 none;
62 border: 1px solid #E6E6E6;
65 fieldset.rows .pickup_time input:disabled + label:hover {
68 fieldset.rows .pickup_time input:disabled + label::before {
70 display: inline-block;
72 #existing-pickup-warning {
74 display: inline-block;
79 .pickups_available::before {
82 .pickups_available::after {
87 [% INCLUDE 'doc-head-close.inc' %]
88 [% BLOCK cssinclude %][% END %]
90 [% INCLUDE 'bodytag.inc' bodyid='opac-curside-pickups' bodyclass='scrollto' %]
91 [% INCLUDE 'masthead.inc' %]
93 <nav id="breadcrumbs" aria-label="Breadcrumb" class="breadcrumbs">
94 <ol class="breadcrumb">
95 <li class="breadcrumb-item">
96 <a href="/cgi-bin/koha/opac-main.pl">Home</a>
98 <li class="breadcrumb-item">
99 <a href="/cgi-bin/koha/opac-user.pl">[% INCLUDE 'patron-title.inc' patron = logged_in_user %]</a>
102 <li class="breadcrumb-item active">
103 <a href="#" aria-current="page">Curbside pickups</a>
105 </ol> <!-- / .breadcrumb -->
106 </nav> <!-- /#breadcrumbs -->
108 <div class="container-fluid">
110 <div class="col col-lg-2 order-2 order-lg-1">
111 <div id="navigation">
112 [% INCLUDE 'navigation.inc' IsPatronPage=1 %]
115 <div class="col-md-12 col-lg-10 order-1">
116 <div id='pickupdetails' class="maincontent">
117 <h2>Curbside pickups</h2>
119 [% FOR m IN messages %]
120 [% IF m.type == "error" %]
121 <div class="alert alert-warning">
123 <div class="alert alert-info">
126 [% CASE 'not_enabled' %]
127 <span>The curbside pickup feature is not enabled for this library.</span>
128 [% CASE 'library_is_closed' %]
129 <span>Cannot create a curbside pickup for this day, it is a holiday.</span>
130 [% CASE 'no_waiting_holds' %]
131 <span>This patron does not have waiting holds.</span>
132 [% CASE 'too_many_pickups' %]
133 <span>You already have a scheduled pickup for this library.</span>
134 [% CASE 'no_matching_slots' %]
135 <span>Wrong slot selected.</span>
136 [% CASE 'no_more_pickups_available' %]
137 <span>There are no more pickups available for this slot. Please choose another one.</span>
138 [% CASE 'cannot_checkout' %]
139 <span>Unable to check the items out to [% INCLUDE 'patron-title.inc' patron=m.patron %]</span>
140 [% CASE 'library_notified' %]
141 <span>The library has been notified of your arrival.</span>
143 <span>[% m.code | html %]</span>
148 <div id="opac-pickups-views" class="toptabs">
149 <ul class="nav nav-tabs" role="tablist">
150 <li class="nav-item" role="presentation" id="tab-user-pickups">
151 [% IF patron_curbside_pickups.count %]
152 <a href="#user-pickups" class="nav-link active" aria-controls="user-pickups" aria-selected="true" role="tab" data-toggle="tab">Your pickups</a>
154 <a href="#user-pickups" class="nav-link" aria-controls="user-pickups" role="tab" data-toggle="tab">Your pickups</a>
158 [% IF policies.count %]
159 <li class="nav-item" role="presentation" id="tab-user-schedule-pickup">
160 [% IF patron_curbside_pickups.count %]
161 <a href="#user-schedule-pickup" class="nav-link" aria-controls="user-schedule-pickup" role="tab" data-toggle="tab">Schedule a pickup</a>
163 <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>
169 <div class="tab-content">
170 [% IF patron_curbside_pickups.count %]
171 <div role="tabpanel" class="tab-pane active" id="user-pickups" aria-labelledby="tab-user-pickups">
173 <div role="tabpanel" class="tab-pane" id="user-pickups" aria-labelledby="tab-user-pickups">
175 [% IF patron_curbside_pickups.count %]
176 <table id="pickups-table" class="table table-striped">
179 <th>Pickup library</td>
186 [% FOR p IN patron_curbside_pickups %]
188 <td>[% Branches.GetName(p.branchcode) | html %]</td>
189 <td>[% p.scheduled_pickup_datetime | $KohaDates with_hours => 1 %]</td>
190 <td>[% p.notes | html %]</td>
193 <input type="hidden" name="op" value="arrival-alert" />
194 <input type="hidden" name="pickup_id" value="[% p.id | html %]" />
195 [% IF ! p.staged_datetime || p.arrival_datetime %]
196 <button class="btn disabled" disabled href="#" >
198 <button type="submit" class="btn" href="#" >
200 <i class="fa fa-bell" aria-hidden="true"></i> Alert staff of your arrival
205 <input type="hidden" name="op" value="cancel-pickup" />
206 <input type="hidden" name="pickup_id" value="[% p.id | html %]" />
207 [% IF p.delivered_datetime %]
208 <button class="btn disabled" disabled href="#" >
210 <button type="submit" class="btn" href="#" >
212 <i class="fa fa-ban" aria-hidden="true"></i> Cancel this pickup</button>
220 <div>No curbside pickups.</div>
224 [% IF policies.count %]
225 [% IF patron_curbside_pickups.count %]
226 <div role="tabpanel" class="tab-pane" id="user-schedule-pickup" aria-labelledby="tab-user-schedule-pickup">
228 <div role="tabpanel" class="tab-pane active" id="user-schedule-pickup" aria-labelledby="tab-user-schedule-pickup">
230 <form id="create-pickup" method="post">
231 <fieldset class="rows">
234 <label for="pickup-branch" class="required">Pickup library:</label>
235 <select name="pickup_branch" id="pickup-branch" required="required">
236 <option value="">Select a library</option>
237 [% FOR p IN policies %]
238 <option value="[% p.branchcode | html %]">[% Branches.GetName(p.branchcode) | html %]</option>
242 <span id="existing-pickup-warning" class="required" style="display: none;">You already have a pickup scheduled for this library.</span>
243 <div class="required_label required">Required</div>
245 <li id="pickup_date_item">
246 <label for="pickup-date">Pickup date:</label>
247 <input name="pickup_date" type="text" class="flatpickr" id="pickup-date" disabled required="required"/>
248 <div class="required_label required">Required</div>
251 <li id="pickup-times"></li>
253 <li id="pickup_notes_item">
254 <label for="notes">Notes:</label>
255 <input name="notes" id="notes" />
260 <fieldset class="action">
261 <input type="hidden" name="op" value="create-pickup" />
262 <input type="submit" id="schedule-pickup-button" class="btn btn-default" disabled value="Schedule pickup" />
270 </div> <!-- / .col-lg-10 -->
271 </div> <!-- / .row -->
272 </div> <!-- / .container-fluid -->
273 </div> <!-- / .main -->
275 [% INCLUDE 'opac-bottom.inc' %]
277 [% BLOCK jsinclude %]
278 [% Asset.js("lib/dayjs/dayjs.min.js") | $raw %]
279 [% Asset.js("lib/dayjs/plugin/isSameOrAfter.js") | $raw %]
280 [% Asset.js("lib/dayjs/plugin/customParseFormat.js") | $raw %]
281 <script>dayjs.extend(window.dayjs_plugin_isSameOrAfter)</script>
282 <script>dayjs.extend(window.dayjs_plugin_customParseFormat)</script>
283 [% INCLUDE 'calendar.inc' %]
285 [% SET pickup_exists_in = [] %]
286 [% FOR p IN patron_curbside_pickups %]
287 [% UNLESS p.delivered_by %]
288 [% pickup_exists_in.push(p.branchcode) %]
291 let pickup_exists_in = [% To.json(pickup_exists_in.unique()) | $raw %];
293 let pickups = [% To.json(curbside_pickups.unblessed) | $raw %];
294 let policies = [% To.json(policies.unblessed) | $raw %];
295 policies = policies.reduce((map, e) => {
296 map[e.branchcode] = e;
299 let can_schedule_at = {};
300 [% FOR p IN policies %]
301 var opening_slots = [% To.json(p.opening_slots.unblessed) | $raw %];
302 var slots_per_day = {};
303 opening_slots.forEach(function(slot){
305 if(!slots_per_day[day]) slots_per_day[day] = [];
306 slots_per_day[day].push(slot);
308 policies['[% p.branchcode | html %]'].slots_per_day = slots_per_day;
310 [% IF p.enable_waiting_holds_only %]
311 [% SET waiting_holds = logged_in_user.holds.search( found => 'W', branchcode => p.branchcode ) %]
312 [% UNLESS waiting_holds.count %]
313 policies['[% p.branchcode | html %]'].enabled = 0;
318 let existingPickupMoments = [];
319 pickups.forEach(function(pickup){
320 let scheduled_pickup_datetime = pickup.scheduled_pickup_datetime;
321 let pickupMoment = dayjs(scheduled_pickup_datetime);
323 if(!existingPickupMoments[pickup.branchcode]) existingPickupMoments[pickup.branchcode] = [];
324 existingPickupMoments[pickup.branchcode].push(pickupMoment);
327 $(document).ready(function() {
328 $("#pickup-branch option").each(function(){
329 if ( $(this).val() != "" && !policies[$(this).val()].enabled ) {
330 $(this).prop("disabled", "disabled");
331 $(this).attr("title", _("You don't have waiting holds at this library"));
335 $('#pickup-branch').on('change', function() {
336 let branchcode = $(this).val();
338 let existing_pickup = pickup_exists_in.indexOf(branchcode) != -1;
340 $('#pickup-date').val("");
341 $('#pickup-time').val("");
342 $('#pickup-times').hide();
343 $('#schedule-pickup-button').prop('disabled', true);
345 if (existing_pickup) {
346 $('#existing-pickup-warning').show();
347 $("#pickup_date_item,#pickup_notes_item").hide();
348 $('#pickup-date').prop("disabled", true);
350 $('#existing-pickup-warning').hide();
351 $("#pickup_date_item").show();
352 $('#pickup-date').prop("disabled", branchcode == "");
356 const pickupDate = document.getElementById("pickup-date");
357 pickupDate._flatpickr.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>");
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, get_dateformat_str(dateformat_pref).toUpperCase());
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-times").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"