5 [% USE AuthorisedValues %]
10 [% INCLUDE 'doc-head-open.inc' %]
11 <title>Curbside pickups › Circulation › Koha</title>
16 .pickup_time input[type='radio'] {
23 background-color: #ffffcc;
24 display: inline-block;
29 .pickup_time input[type='radio']:checked + label {
30 background-color: #bcdb89;
32 .pickup_time input[type='radio']:disabled+ label {
33 background-color: #ff9090;
36 [% INCLUDE 'doc-head-close.inc' %]
39 [% SET today_iso = date.format(date.now, format = '%Y-%m-%d') %]
41 <body id="circ_curbside-pickups" class="circ">
42 [% INCLUDE 'header.inc' %]
43 [% INCLUDE 'cat-search.inc' %]
46 <nav id="breadcrumbs" aria-label="Breadcrumb" class="breadcrumb">
49 <a href="/cgi-bin/koha/mainpage.pl">Home</a>
52 <a href="/cgi-bin/koha/circ/circulation-home.pl">Circulation</a>
55 <a href="#" aria-current="page">Curbside pickups</a>
61 [% BLOCK waiting_holds %]
62 [% SET waiting_holds = cp.patron.holds.search( found => 'W', branchcode => Branches.GetLoggedInBranchcode ) %]
63 [% FOREACH h IN waiting_holds %]
64 <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% h.biblionumber | uri %]">[% h.biblio.title | html %]</a> ([% h.biblio.author | html %], <a href="/cgi-bin/koha/catalogue/moredetail.pl?itemnumber=[% h.itemnumber | html %]&biblionumber=[% h.biblionumber | html %]#item[% h.itemnumber | html %]">[% h.item.barcode | html %]</a>)<br/>
68 [% BLOCK patron_info %]
69 <a href="/cgi-bin/koha/members/moremember.pl?borrowernumber=[% cp.borrowernumber | uri %]">[% cp.patron.firstname | html %] [% cp.patron.surname | html %] ([% cp.patron.cardnumber | html %])</a>
72 <span>Notes: </span>[% cp.notes | html %]
74 [% IF cp.patron.debarred %]
76 <span class="patron_restricted">Patron's account is restricted</span>
78 [% IF cp.patron.has_overdues %]
80 <span class="patron_overdues">Patron has items overdue</span>
84 <div class="main container-fluid">
86 <div class="col-sm-12">
89 [% IF Koha.Preference('CircSidebar') %]
90 <div class="col-sm-10 col-sm-push-2">
92 <div class="col-sm-12">
95 <h1>Curbside pickups</h1>
97 [% UNLESS policy.enabled %]
98 <div class="dialog alert">
99 Curbside pickups are not enabled for your library.
101 [% INCLUDE 'intranet-bottom.inc' %]
105 [% FOR m IN messages %]
106 <div class="dialog [% m.type | html %]">
108 [% CASE 'not_enabled' %]
109 <span>The curbside pickup feature is not enabled for this library.</span>
110 [% CASE 'library_is_closed' %]
111 <span>Cannot create a curbside pickup for this day, it is a holiday.</span>
112 [% CASE 'no_waiting_holds' %]
113 <span>This patron does not have waiting holds.</span>
114 [% CASE 'too_many_pickups' %]
115 <span>This patron already has a scheduled pickup for this library.</span>
116 [% CASE 'no_matching_slots' %]
117 <span>Wrong slot selected.</span>
118 [% CASE 'no_more_pickups_available' %]
119 <span>There are no more pickups available for this slot. Please choose another one.</span>
120 [% CASE 'cannot_checkout' %]
121 <span>Unable to check the items out to [% INCLUDE 'patron-title.inc' patron=m.patron %]</span>
123 <span>[% m.code | html %]</span>
128 <form method="post" class="refresh-form">
130 <input type="hidden" name="tab" id="current-tab" value="[% tab | html %]" />
131 <button type="submit" class="btn btn-default"><i class="fa fa-refresh" aria-hidden="true"></i> Refresh</button>
134 [% IF auto_refresh %]
135 <input type="checkbox" id="auto_refresh" name="auto_refresh" checked="checked" />
137 <input type="checkbox" id="auto_refresh" name="auto_refresh" />
139 Refresh automatically every <input type="text" inputmode="numeric" pattern="[0-9]*" id="refresh_delay" name="refresh_delay" value="[% refresh_delay || 60 | html %]" size="3" /> seconds.
142 <span id="refresh_info"></span>
146 [% SET to_be_staged = curbside_pickups.filter_by_to_be_staged %]
147 [% SET staged_and_ready = curbside_pickups.filter_by_staged_and_ready %]
148 [% SET patron_outside = curbside_pickups.filter_by_patron_outside %]
149 [% SET delivered_today = curbside_pickups.filter_by_delivered %]
150 <div id="pickup-tabs" class="toptabs">
151 <ul class="nav nav-tabs" role="tablist">
152 [% IF !tab OR tab == 'to-be-staged' %]
153 <li role="presentation" class="active">
155 <li role="presentation">
157 <a id="to-be-staged-tab" href="#to-be-staged" role="tab" data-toggle="tab">To be staged ([% to_be_staged.count | html %])</a>
159 [% IF tab == 'staged-and-ready' %]
160 <li role="presentation" class="active">
162 <li role="presentation">
164 <a id="staged-and-ready-tab" href="#staged-and-ready" role="tab" data-toggle="tab">Staged & ready ([% staged_and_ready.count | html %])</a>
166 [% IF tab == 'patron-is-outside' %]
167 <li role="presentation" class="active">
169 <li role="presentation">
171 <a id="patron-is-outside-tab" href="#patron-is-outside" role="tab" data-toggle="tab">Patron is outside ([% patron_outside.count | html %])</a>
173 [% IF tab == 'delivered-today' %]
174 <li role="presentation" class="active">
176 <li role="presentation">
178 <a id="delivered-today-tab" href="#delivered-today" role="tab" data-toggle="tab">Delivered today ([% delivered_today.count | html %])</a>
180 [% IF tab == 'schedule-pickup' %]
181 <li role="presentation" class="active">
183 <li role="presentation">
185 <a id="schedule-pickup-tab" href="#schedule-pickup" role="tab" data-toggle="tab">Schedule pickup</a>
189 <div class="tab-content">
190 [% IF !tab OR tab == 'to-be-staged' %]
191 <div id="to-be-staged" role="tabpanel" class="tab-pane active">
193 <div id="to-be-staged" role="tabpanel" class="tab-pane">
195 [% IF to_be_staged.count %]
196 <table class="table table-striped">
199 <th>Pickup date/time</th>
201 <th>Items for pickup</th>
206 [% FOREACH cp IN to_be_staged %]
207 [% UNLESS cp.staged_datetime %]
208 <tr class="[% class | html %]">
209 <td>[% cp.scheduled_pickup_datetime | $KohaDates with_hours = 1 %]</td>
211 [% PROCESS patron_info %]
214 [% PROCESS waiting_holds %]
217 <form method="post" class="form">
218 <input type="hidden" name="op" value="mark-as-staged"/>
219 <input type="hidden" name="tab" value="to-be-staged"/>
220 <input type="hidden" name="id" value="[% cp.id | html %]"/>
222 <button type="submit" class="btn btn-default mark-as-staged-and-ready-btn"><i class="fa fa-check" aria-hidden="true"></i> Mark as <i>staged & ready</i></button>
226 <form method="post" class="form">
227 <input type="hidden" name="op" value="cancel"/>
228 <input type="hidden" name="tab" value="to-be-staged"/>
229 <input type="hidden" name="id" value="[% cp.id | html %]"/>
231 <button type="submit" class="btn btn-default cancel-btn"><i class="fa fa-ban" aria-hidden="true"></i> Cancel</button>
241 <span>There are no pickups to be staged.</span>
245 [% IF tab == "staged-and-ready" %]
246 <div id="staged-and-ready" role="tabpanel" class="tab-pane active">
248 <div id="staged-and-ready" role="tabpanel" class="tab-pane">
250 [% IF staged_and_ready.count %]
251 <table class="table table-striped">
254 <th>Pickup date/time</th>
256 <th>Items for pickup</th>
262 [% FOREACH cp IN staged_and_ready %]
263 [% IF cp.staged_datetime && !cp.arrival_datetime %]
264 <tr class="[% class | html %]">
265 <td>[% cp.scheduled_pickup_datetime | $KohaDates with_hours = 1 %]</td>
267 [% PROCESS patron_info %]
270 [% PROCESS waiting_holds %]
273 [% cp.staged_by_staff.firstname | html %] [% cp.staged_by_staff.surname | html %]
276 <form method="post" class="form">
277 <input type="hidden" name="op" value="mark-patron-has-arrived"/>
278 <input type="hidden" name="tab" value="staged-and-ready"/>
279 <input type="hidden" name="id" value="[% cp.id | html %]"/>
281 <button type="submit" class="btn btn-default patron-has-arrived-btn"><i class="fa fa-map-marker" aria-hidden="true"></i> Patron has arrived</button>
285 <form method="post" class="form">
286 <input type="hidden" name="op" value="mark-as-delivered"/>
287 <input type="hidden" name="tab" value="staged-and-ready"/>
288 <input type="hidden" name="id" value="[% cp.id | html %]"/>
290 <button type="submit" class="btn btn-default mark-as-delivered-btn"><i class="fa fa-envelope" aria-hidden="true"></i> Mark as <i>delivered</i></button>
294 <form method="post" class="form">
295 <input type="hidden" name="op" value="mark-as-unstaged"/>
296 <input type="hidden" name="tab" value="staged-and-ready"/>
297 <input type="hidden" name="id" value="[% cp.id | html %]"/>
299 <button type="submit" class="btn btn-default mark-as-to-be-staged-btn"><i class="fa fa-undo" aria-hidden="true"></i> Mark as <i>to be staged</i></button>
309 <span>There are no pickups staged and ready.</span>
313 [% IF tab == "patron-is-outside" %]
314 <div id="patron-is-outside" role="tabpanel" class="tab-pane active">
316 <div id="patron-is-outside" role="tabpanel" class="tab-pane">
318 [% IF patron_outside.count %]
319 <table class="table table-striped">
322 <th>Pickup date/time</th>
324 <th>Items for pickup</th>
330 [% FOREACH cp IN patron_outside %]
331 [% IF cp.arrival_datetime && !cp.delivered_datetime %]
332 <tr class="[% class | html %]">
333 <td>[% cp.scheduled_pickup_datetime | $KohaDates with_hours = 1 %]</td>
335 [% PROCESS patron_info %]
338 [% PROCESS waiting_holds %]
341 [% cp.staged_by_staff.firstname | html %] [% cp.staged_by_staff.surname | html %]
344 <form method="post" class="form">
345 <input type="hidden" name="op" value="mark-as-delivered"/>
346 <input type="hidden" name="tab" value="patron-is-outside"/>
347 <input type="hidden" name="id" value="[% cp.id | html %]"/>
349 <button type="submit" class="btn btn-default mark-as-delivered-btn"><i class="fa fa-envelope" aria-hidden="true"></i> Mark as delivered</button>
353 <form method="post" class="form">
354 <input type="hidden" name="op" value="mark-as-staged"/>
355 <input type="hidden" name="tab" value="patron-is-outside"/>
356 <input type="hidden" name="id" value="[% cp.id | html %]"/>
358 <button type="submit" class="btn btn-default mark-as-staged-and-ready-btn"><i class="fa fa-undo" aria-hidden="true"></i> Mark as <i>staged & ready</i></button>
362 <form method="post" class="form">
363 <input type="hidden" name="op" value="mark-as-unstaged"/>
364 <input type="hidden" name="tab" value="patron-is-outside"/>
365 <input type="hidden" name="id" value="[% cp.id | html %]"/>
367 <button type="submit" class="btn btn-default mark-as-to-be-staged-btn"><i class="fa fa-undo" aria-hidden="true"></i> Mark as <i>to be staged</i></button>
377 <span>There are no patrons waiting outside.</span>
381 [% IF tab == "delivered-today" %]
382 <div id="delivered-today" role="tabpanel" class="tab-pane active">
384 <div id="delivered-today" role="tabpanel" class="tab-pane">
386 [% IF delivered_today.count %]
387 <table class="table table-striped">
390 <th>Deliver date/time</th>
392 <th>Items checked out</th>
396 [% FOREACH cp IN delivered_today %]
397 [% IF cp.delivered_datetime %]
398 <tr class="[% class | html %]">
399 <td>[% cp.delivered_datetime | $KohaDates with_hours = 1 %]</td>
401 [% PROCESS patron_info %]
404 [% FOREACH c IN cp.checkouts %]
405 [% IF date.format(c.issuedate, format = '%Y-%m-%d') == today_iso %]
406 <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% c.item.biblionumber | uri %]">[% c.item.biblio.title | html %]</a> ([% c.item.biblio.author | html %], <a href="/cgi-bin/koha/catalogue/moredetail.pl?itemnumber=[% c.itemnumber | html %]&biblionumber=[% c.item.biblionumber | html %]#item[% c.itemnumber | html %]">[% c.item.barcode | html %]</a>)<br/>
416 <span>No pickups have been delivered today.</span>
420 [% IF tab == "schedule-pickup" %]
421 <div id="schedule-pickup" role="tabpanel" class="tab-pane active">
423 <div id="schedule-pickup" role="tabpanel" class="tab-pane">
425 [% IF !patron || ( patron && existing_curbside_pickups.count >= 1 ) %]
426 [% IF existing_curbside_pickups.count >= 1 %]
427 <div class="dialog alert">
428 [% patron.firstname | html %] [% patron.surname | html %] ([% patron.cardnumber | html %]) already has a scheduled pickup for this library.
431 <div class="form-group">
432 <label class="sr-only" for="input-patron-cardnumber">Cardnumber</label>
433 <div class="input-group">
434 <div class="input-group-addon">Search a patron</div>
435 <input autocomplete="off" id="find-patron" class="form-control" type="text" style="width:25%" class="noEnterSubmit" placeholder="Enter patron cardnumber or name"//>
439 [% SET waiting_holds = patron.holds.search( found => 'W', branchcode => Branches.GetLoggedInBranchcode ) %]
440 [% IF !policy.enable_waiting_holds_only || waiting_holds.count > 0 %]
441 <form id="create-pickup" method="post">
442 <fieldset class="rows" style="float: none;">
443 <input type="hidden" name="borrowernumber" value="[% patron.id | html %]"/>
444 <input type="hidden" name="op" value="create-pickup"/>
445 <input type="hidden" name="tab" value="schedule-pickup"/>
448 <label>Patron: </label>
449 <span>[% INCLUDE 'patron-title.inc' patron=patron %]</span>
450 <a title="Search for another patron" href="/cgi-bin/koha/circ/curbside_pickups.pl?tab=schedule-pickup"><i class="fa fa-search"></i></a>
454 <label>Items ready for pickup: </label>
456 [% IF waiting_holds.count %]
457 [% FOREACH h IN waiting_holds %]
459 <a href="/cgi-bin/koha/catalogue/detail.pl?biblionumber=[% h.biblionumber | uri %]">[% h.biblio.title | html %]</a> ([% h.biblio.author | html %], <a href="/cgi-bin/koha/catalogue/moredetail.pl?itemnumber=[% h.itemnumber | html %]&biblionumber=[% h.biblionumber | html %]#item[% h.itemnumber | html %]">[% h.item.barcode | html %]</a>)
463 <span>There are no waiting holds for this patron at this library.</span>
469 <label for="pickup_date">Pickup date: </label>
470 <input id="pickup_date" name="pickup_date" required="required" class="flatpickr" data-flatpickr-futureinclusive="true" />
473 <li id="pickup-times" class="radio"></li>
476 <label for="notes">Notes: </label>
477 <input id="notes" name="notes" type="text" />
482 <fieldset class="action">
483 <input type="submit" id="schedule-pickup-button" class="btn btn-primary" value="Submit" />
487 <div class="dialog alert">The patron does not have waitings holds.</div>
493 [% IF Koha.Preference('CircSidebar') %]
494 </div> <!-- /.col-sm-10.col-sm-push-2 -->
495 <div class="col-sm-2 col-sm-pull-10">
497 [% INCLUDE 'circ-nav.inc' %]
499 </div> <!-- /.col-sm-2.col-sm-pull-10 -->
500 </div> <!-- /.row -->
504 </div> <!-- /.row -->
506 [% MACRO jsinclude BLOCK %]
507 [% Asset.js("lib/dayjs/dayjs.min.js") | $raw %]
508 [% Asset.js("lib/dayjs/plugin/isSameOrAfter.js") | $raw %]
509 [% Asset.js("lib/dayjs/plugin/customParseFormat.js") | $raw %]
510 <script>dayjs.extend(window.dayjs_plugin_isSameOrAfter)</script>
511 <script>dayjs.extend(window.dayjs_plugin_customParseFormat)</script>
512 [% INCLUDE 'calendar.inc' %]
513 [% INCLUDE 'js-patron-format.inc' %]
515 let pickups = [% To.json(curbside_pickups.unblessed) | $raw %];
516 let policy = [% To.json(policy.unblessed) | $raw %];
518 let existingPickupMoments = [];
519 pickups.forEach(function(pickup){
520 let scheduled_pickup_datetime = pickup.scheduled_pickup_datetime;
521 let pickupMoment = dayjs(scheduled_pickup_datetime);
523 existingPickupMoments.push(pickupMoment);
526 let opening_slots = [% To.json(policy.opening_slots.unblessed) | $raw %];
527 let slots_per_day = {};
528 opening_slots.forEach(function(slot){
530 if(!slots_per_day[day]) slots_per_day[day] = [];
531 slots_per_day[day].push(slot);
534 $(document).ready(function() {
536 $('#schedule-pickup-tab').on('click', function() {
537 $('#input-patron-cardnumber').focus();
540 const pickup_date = document.querySelector("#pickup_date");
542 const fp = pickup_date._flatpickr;
543 fp.set('disable', [function(date) {
544 return !slots_per_day.hasOwnProperty(date.getDay());
548 $("#pickup_date").on('change', function() {
550 $('#pickup-times').empty();
551 $('#schedule-pickup-button').prop( 'disabled', 1 );
553 var currentDate = $(this).val();
555 let selectedDate = dayjs(currentDate);
557 let pickupSlots = [];
558 let available_count = 0;
559 let dow = selectedDate.day(); // Sunday is 0 (at least for now)
560 if (!slots_per_day[dow]){
561 $('#pickup-times').html("<div>"+_("No pickup time defined for this day.")+"</div>");
565 slots_per_day[dow].sort((a, b) => a.start_hour - b.start_hour).forEach(function(slot){
566 let pickup_interval = policy.pickup_interval;
567 if (!pickup_interval) {
568 $('#pickup-times').html("<div>"+_("No pickup time defined for this day.")+"</div>");
572 let listStartMoment = selectedDate.hour(slot.start_hour).minute(slot.start_minute);
573 let listEndMoment = selectedDate.hour(slot.end_hour).minute(slot.end_minute);
575 let keep_going = true;
578 // Initialize pickup slots starting at opening time
579 let pickupIntervalStartMoment = listStartMoment;
580 let pickupIntervalEndMoment = listStartMoment.add(pickup_interval, 'minutes');
582 let available = true;
583 let display_slot = true
585 if (pickupIntervalStartMoment.isBefore(now)) {
586 // Slots in the past are unavailable
588 display_slot = false;
591 if (pickupIntervalEndMoment.isAfter(listEndMoment)) {
592 // Slots after the end of pickup times for the day are unavailable
596 let pickups_scheduled = 0;
597 existingPickupMoments.forEach(function(pickupMoment){
598 // An existing pickup time
599 if (pickupMoment.isSameOrAfter(pickupIntervalStartMoment) && pickupMoment.isBefore(pickupIntervalEndMoment)) {
600 // This calculated pickup is in use by another scheduled pickup
605 if (pickups_scheduled >= policy.patrons_per_interval) {
609 if ( display_slot ) {
612 "available": available,
613 "moment": pickupIntervalStartMoment,
614 "pickups_scheduled": pickups_scheduled
623 pickupIntervalStartMoment = pickupIntervalEndMoment;
624 pickupIntervalEndMoment = pickupIntervalStartMoment.add(pickup_interval, 'minutes');
625 if (pickupIntervalEndMoment.isAfter(listEndMoment)) {
626 // This latest slot is after the end of pickup times for the day, so we can stop
632 for (let i = 0; i < pickupSlots.length; i++) {
633 let pickupSlot = pickupSlots[i];
634 let optText = pickupSlot.moment.format("HH:mm");
635 let optValue = pickupSlot.moment.format("YYYY-MM-DD HH:mm:ss");
636 let pickups_scheduled = pickupSlot.pickups_scheduled;
637 let disabled = pickupSlot.available ? "" : "disabled";
638 $("#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>`);
641 $('#pickup-times').show();
642 $('#schedule-pickup-button').prop( 'disabled', available_count <= 0 );
645 $("#create-pickup").on('submit', function(){
646 if ( ! $("input[type='radio']:checked").length ) {
647 alert(_("Please select a date and a pickup time"))
653 if ( $("#find-patron").length ) {
654 patron_autocomplete($("#find-patron"), { 'on-select-callback': function( event, ui ) {
655 window.location.href = "/cgi-bin/koha/circ/curbside_pickups.pl?op=find-patron&borrowernumber=" + ui.item.patron_id;
661 $("#pickup-tabs a[data-toggle='tab']").on("shown.bs.tab", function (e) {
662 $("#current-tab").val($(this).attr('href').substring(1)); // Remove #
664 $("#auto_refresh,#refresh_delay").on("change", function(){
665 set_interval_if_needed();
668 set_interval_if_needed();
672 let refresh_interval_id = 0;
673 let countdown_interval_id = 0;
674 function set_interval(refresh_delay_ms){
676 let next_refresh = new Date();
677 next_refresh.setSeconds(next_refresh.getSeconds() + refresh_delay_ms / 1000);
679 countdown_interval_id = setInterval(function() {
680 const now = new Date().getTime();
681 const seconds = Math.floor(( next_refresh - now + 1 ) / 1000);
683 $("#refresh_info").text(_("Refresh in %s seconds").format(seconds));
685 $("#refresh_info").text(""); // In case something is going wrong
689 setInterval(function() {
690 $(".refresh-form:visible").submit();
691 }, refresh_delay_ms);
693 function clear_intervals(){
694 if (refresh_interval_id) {
695 clearInterval(refresh_interval_id);
696 refresh_interval_id = 0;
698 if (countdown_interval_id) {
699 clearInterval(countdown_interval_id);
700 countdown_interval_id = 0;
704 function set_interval_if_needed(){
705 const refresh_delay = $("#refresh_delay").val();
706 const auto_refresh = $("#auto_refresh").is(":checked");
708 if (auto_refresh && refresh_delay){
709 set_interval(refresh_delay * 1000);
718 [% INCLUDE 'intranet-bottom.inc' %]