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'] {
20 background-color: #ffffcc;
21 display: inline-block;
24 .pickup_time input[type='radio']:checked + label {
25 background-color: #bcdb89;
29 [% INCLUDE 'doc-head-close.inc' %]
30 [% BLOCK cssinclude %][% END %]
32 [% INCLUDE 'bodytag.inc' bodyid='opac-curside-pickups' bodyclass='scrollto' %]
33 [% INCLUDE 'masthead.inc' %]
35 <nav id="breadcrumbs" aria-label="Breadcrumb" class="breadcrumbs">
36 <ol class="breadcrumb">
37 <li class="breadcrumb-item">
38 <a href="/cgi-bin/koha/opac-main.pl">Home</a>
40 <li class="breadcrumb-item">
41 <a href="/cgi-bin/koha/opac-user.pl">[% INCLUDE 'patron-title.inc' patron = logged_in_user %]</a>
44 <li class="breadcrumb-item active">
45 <a href="#" aria-current="page">Curbside pickups</a>
47 </ol> <!-- / .breadcrumb -->
48 </nav> <!-- /#breadcrumbs -->
50 <div class="container-fluid">
52 <div class="col col-lg-2 order-2 order-lg-1">
54 [% INCLUDE 'navigation.inc' IsPatronPage=1 %]
57 <div class="col-md-12 col-lg-10 order-1">
58 <div id='pickupdetails' class="maincontent">
59 <h2>Curbside pickups</h2>
61 [% FOR m IN messages %]
62 [% IF m.type == "error" %]
63 <div class="alert alert-warning">
65 <div class="alert alert-info">
68 [% CASE 'not_enabled' %]
69 <span>The curbside pickup feature is not enabled for this library.</span>
70 [% CASE 'library_is_closed' %]
71 <span>Cannot create a curbside pickup for this day, it is an holiday.</span>
72 [% CASE 'no_waiting_holds' %]
73 <span>This patron does not have waiting holds.</span>
74 [% CASE 'too_many_pickups' %]
75 <span>You already have a scheduled pickup for this library.</span>
76 [% CASE 'no_matching_slots' %]
77 <span>Wrong slot selected.</span>
78 [% CASE 'no_more_pickups_available' %]
79 <span>There are no more pickups available for this slot. Please choose another one.</span>
80 [% CASE 'cannot_checkout' %]
81 <span>Unable to check the items out to [% INCLUDE 'patron-title.inc' patron=m.patron %]</span>
82 [% CASE 'library_notified' %]
83 <span>The library has been notified of your arrival.</span>
85 <span>[% m.code | html %]</span>
90 <div id="opac-pickups-views" class="toptabs">
91 <ul class="nav nav-tabs" role="tablist">
92 <li class="nav-item" role="presentation" id="tab-user-pickups">
93 [% IF patron_curbside_pickups.count %]
94 <a href="#user-pickups" class="nav-link active" aria-controls="user-pickups" aria-selected="true" role="tab" data-toggle="tab">Your pickups</a>
96 <a href="#user-pickups" class="nav-link" aria-controls="user-pickups" role="tab" data-toggle="tab">Your pickups</a>
100 [% IF policies.count %]
101 <li class="nav-item" role="presentation" id="tab-user-schedule-pickup">
102 [% IF patron_curbside_pickups.count %]
103 <a href="#user-schedule-pickup" class="nav-link" aria-controls="user-schedule-pickup" role="tab" data-toggle="tab">Schedule a pickup</a>
105 <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>
111 <div class="tab-content">
112 [% IF patron_curbside_pickups.count %]
113 <div role="tabpanel" class="tab-pane active" id="user-pickups" aria-labelledby="tab-user-pickups">
115 <div role="tabpanel" class="tab-pane" id="user-pickups" aria-labelledby="tab-user-pickups">
117 [% IF patron_curbside_pickups.count %]
118 <table id="pickups-table" class="table table-striped">
121 <th>Pickup library</td>
128 [% FOR p IN patron_curbside_pickups %]
130 <td>[% Branches.GetName(p.branchcode) | html %]</td>
131 <td>[% p.scheduled_pickup_datetime | $KohaDates with_hours => 1 %]</td>
132 <td>[% p.notes | html %]</td>
135 <input type="hidden" name="op" value="arrival-alert" />
136 <input type="hidden" name="pickup_id" value="[% p.id | html %]" />
137 [% IF ! p.staged_datetime || p.arrival_datetime %]
138 <button class="btn disabled" disabled href="#" >
140 <button type="submit" class="btn" href="#" >
142 <i class="fa fa-bell" aria-hidden="true"></i> Alert staff of your arrival
147 <input type="hidden" name="op" value="cancel-pickup" />
148 <input type="hidden" name="pickup_id" value="[% p.id | html %]" />
149 [% IF p.delivered_datetime %]
150 <button class="btn disabled" disabled href="#" >
152 <button type="submit" class="btn" href="#" >
154 <i class="fa fa-ban" aria-hidden="true"></i> Cancel this pickup</button>
162 <div>No curbside pickups.</div>
166 [% IF policies.count %]
167 [% IF patron_curbside_pickups.count %]
168 <div role="tabpanel" class="tab-pane" id="user-schedule-pickup" aria-labelledby="tab-user-schedule-pickup">
170 <div role="tabpanel" class="tab-pane active" id="user-schedule-pickup" aria-labelledby="tab-user-schedule-pickup">
172 <form id="create-pickup" method="post">
173 <fieldset class="rows">
176 <label for="pickup_branch">Pickup library:</label>
177 <select name="pickup_branch" id="pickup-branch">
178 <option value="">Select a library</option>
179 [% FOR p IN policies %]
180 <option value="[% p.branchcode | html %]">[% Branches.GetName(p.branchcode) | html %]</option>
184 <span id="existing-pickup-warning" class="required" style="display: none;">You already have a pickup scheduled for this library.</span>
187 <label for="pickup_date">Pickup date:</label>
188 <input name="pickup_date" type="text" class="flatpickr" id="pickup-date" disabled required="required"/>
189 <span class="required">Required</span>
192 <li id="pickup-times" class="radio"></li>
195 <label for="notes">Notes:</label>
196 <input name="notes" id="notes" />
201 <fieldset class="action">
202 <input type="hidden" name="op" value="create-pickup" />
203 <input type="submit" id="schedule-pickup-button" class="btn btn-default" disabled value="Schedule pickup" />
211 </div> <!-- / .col-lg-10 -->
212 </div> <!-- / .row -->
213 </div> <!-- / .container-fluid -->
214 </div> <!-- / .main -->
216 [% INCLUDE 'opac-bottom.inc' %]
218 [% BLOCK jsinclude %]
219 [% Asset.js("lib/dayjs/dayjs.min.js") | $raw %]
220 [% Asset.js("lib/dayjs/plugin/isSameOrAfter.js") | $raw %]
221 <script>dayjs.extend(window.dayjs_plugin_isSameOrAfter)</script>
222 [% INCLUDE 'calendar.inc' %]
224 [% SET pickup_exists_in = [] %]
225 [% FOR p IN patron_curbside_pickups %]
226 [% UNLESS p.delivered_by %]
227 [% pickup_exists_in.push(p.branchcode) %]
230 let pickup_exists_in = [% To.json(pickup_exists_in.unique()) | $raw %];
232 let pickups = [% To.json(curbside_pickups.unblessed) | $raw %];
233 let policies = [% To.json(policies.unblessed) | $raw %];
234 policies = policies.reduce((map, e) => {
235 map[e.branchcode] = e;
238 let can_schedule_at = {};
239 [% FOR p IN policies %]
240 var opening_slots = [% To.json(p.opening_slots.unblessed) | $raw %];
241 var slots_per_day = {};
242 opening_slots.forEach(function(slot){
244 if(!slots_per_day[day]) slots_per_day[day] = [];
245 slots_per_day[day].push(slot);
247 policies['[% p.branchcode | html %]'].slots_per_day = slots_per_day;
249 [% IF p.enable_waiting_holds_only %]
250 [% SET waiting_holds = logged_in_user.holds.search( found => 'W', branchcode => p.branchcode ) %]
251 [% UNLESS waiting_holds.count %]
252 policies['[% p.branchcode | html %]'].enabled = 0;
257 let existingPickupMoments = [];
258 pickups.forEach(function(pickup){
259 let scheduled_pickup_datetime = pickup.scheduled_pickup_datetime;
260 let pickupMoment = dayjs(scheduled_pickup_datetime);
262 if(!existingPickupMoments[pickup.branchcode]) existingPickupMoments[pickup.branchcode] = [];
263 existingPickupMoments[pickup.branchcode].push(pickupMoment);
266 $("#pickup-branch option").each(function(){
267 if ( $(this).val() != "" && !policies[$(this).val()].enabled ) {
268 $(this).prop("disabled", "disabled");
269 $(this).attr("title", _("You don't have waiting holds at this location"));
273 $('#pickup-branch').on('change', function() {
274 let branchcode = $(this).val();
276 let existing_pickup = pickup_exists_in.indexOf(branchcode) != -1;
278 $('#pickup-date').val("");
279 $('#pickup-time').val("");
280 $('#pickup-times').hide();
281 $('#schedule-pickup-button').prop('disabled', true);
283 if (existing_pickup) {
284 $('#existing-pickup-warning').show();
285 $('#pickup-date').prop("disabled", true);
287 $('#existing-pickup-warning').hide();
288 $('#pickup-date').prop("disabled", branchcode == "");
292 $("#pickup-date").on('change', function() {
294 $('#pickup-times').empty();
295 $('#schedule-pickup-button').prop( 'disabled', 1 );
297 var currentDate = $(this).val();
298 let branchcode = $("#pickup-branch").val();
299 let policy = policies[branchcode];
301 let selectedDate = dayjs(currentDate);
303 let pickupSlots = [];
304 let available_count = 0;
305 let dow = selectedDate.day(); // Sunday is 0 (at least for now)
306 if (!policy.slots_per_day[dow]){
307 $('#pickup-times').html("<div>"+_("No pickup time define for this day.")+"</div>");
311 policy.slots_per_day[dow].forEach(function(slot){
312 let pickup_interval = policy.pickup_interval;
313 if (!pickup_interval) {
314 $('#pickup-times').html("<div>"+_("No pickup time define for this day.")+"</div>");
318 let listStartMoment = selectedDate.hour(slot.start_hour).minute(slot.start_minute);
319 let listEndMoment = selectedDate.hour(slot.end_hour).minute(slot.end_minute);
321 let keep_going = true;
324 // Initialize pickup slots starting at opening time
325 let pickupIntervalStartMoment = listStartMoment;
326 let pickupIntervalEndMoment = listStartMoment.add(pickup_interval, 'minutes');
328 let available = true;
329 let display_slot = true;
331 if (pickupIntervalStartMoment.isBefore(now)) {
332 // Slots in the past are unavailable
334 display_slot = false;
337 if (pickupIntervalEndMoment.isAfter(listEndMoment)) {
338 // Slots after the end of pickup times for the day are unavailable
342 let pickups_scheduled = 0;
344 if (existingPickupMoments[branchcode]){
345 existingPickupMoments[branchcode].forEach(function(pickupMoment){
346 // An existing pickup time
347 if (pickupMoment.isSameOrAfter(pickupIntervalStartMoment) && pickupMoment.isBefore(pickupIntervalEndMoment)) {
348 // This calculated pickup is in use by another scheduled pickup
354 if (pickups_scheduled >= policy.patrons_per_interval) {
358 if ( display_slot ) {
361 "available": available,
362 "moment": pickupIntervalStartMoment,
363 "pickups_scheduled": pickups_scheduled
372 pickupIntervalStartMoment = pickupIntervalEndMoment;
373 pickupIntervalEndMoment = pickupIntervalStartMoment.add(pickup_interval, 'minutes');
374 if (pickupIntervalEndMoment.isAfter(listEndMoment)) {
375 // This latest slot is after the end of pickup times for the day, so we can stop
380 $('#schedule-pickup-button').prop( 'disabled', available_count <= 0 );
383 for (let i = 0; i < pickupSlots.length; i++) {
384 let pickupSlot = pickupSlots[i];
385 let optText = pickupSlot.moment.format("HH:mm");
386 let optValue = pickupSlot.moment.format("YYYY-MM-DD HH:mm:ss");
387 let pickups_scheduled = pickupSlot.pickups_scheduled;
388 let disabled = pickupSlot.available ? "" : "disabled";
389 $("#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>`);
392 $('#pickup-times').show();
396 $(document).ready(function() {
397 $("#pickup-times").hide();
399 $("#create-pickup").on('submit', function(){
400 if ( ! $("input[type='radio']:checked").length ) {
401 alert(_("Please select a date and a pickup time"))