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 [% SET to_be_staged = curbside_pickups.filter_by_to_be_staged %]
129 [% SET staged_and_ready = curbside_pickups.filter_by_staged_and_ready %]
130 [% SET patron_outside = curbside_pickups.filter_by_patron_outside %]
131 [% SET delivered_today = curbside_pickups.filter_by_delivered %]
132 <div id="pickup-tabs" class="toptabs">
133 <ul class="nav nav-tabs" role="tablist">
134 [% IF !tab OR tab == 'to-be-staged' %]
135 <li role="presentation" class="active">
137 <li role="presentation">
139 <a id="to-be-staged-tab" href="#to-be-staged" role="tab" data-toggle="tab">To be staged ([% to_be_staged.count | html %])</a>
141 [% IF tab == 'staged-and-ready' %]
142 <li role="presentation" class="active">
144 <li role="presentation">
146 <a id="staged-and-ready-tab" href="#staged-and-ready" role="tab" data-toggle="tab">Staged & ready ([% staged_and_ready.count | html %])</a>
148 [% IF tab == 'patron-is-outside' %]
149 <li role="presentation" class="active">
151 <li role="presentation">
153 <a id="patron-is-outside-tab" href="#patron-is-outside" role="tab" data-toggle="tab">Patron is outside ([% patron_outside.count | html %])</a>
155 [% IF tab == 'delivered-today' %]
156 <li role="presentation" class="active">
158 <li role="presentation">
160 <a id="delivered-today-tab" href="#delivered-today" role="tab" data-toggle="tab">Delivered today ([% delivered_today.count | html %])</a>
162 [% IF tab == 'schedule-pickup' %]
163 <li role="presentation" class="active">
165 <li role="presentation">
167 <a id="schedule-pickup-tab" href="#schedule-pickup" role="tab" data-toggle="tab">Schedule pickup</a>
171 <div class="tab-content">
172 [% IF !tab OR tab == 'to-be-staged' %]
173 <div id="to-be-staged" role="tabpanel" class="tab-pane active">
175 <div id="to-be-staged" role="tabpanel" class="tab-pane">
177 <form method="post" class="form">
179 <button type="submit" class="btn btn-default"><i class="fa fa-refresh" aria-hidden="true"></i> Refresh</button>
183 [% IF to_be_staged.count %]
184 <table class="table table-striped">
187 <th>Pickup date/time</th>
189 <th>Items for pickup</th>
194 [% FOREACH cp IN to_be_staged %]
195 [% UNLESS cp.staged_datetime %]
196 <tr class="[% class | html %]">
197 <td>[% cp.scheduled_pickup_datetime | $KohaDates with_hours = 1 %]</td>
199 [% PROCESS patron_info %]
202 [% PROCESS waiting_holds %]
205 <form method="post" class="form">
206 <input type="hidden" name="op" value="mark-as-staged"/>
207 <input type="hidden" name="tab" value="to-be-staged"/>
208 <input type="hidden" name="id" value="[% cp.id | html %]"/>
210 <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>
214 <form method="post" class="form">
215 <input type="hidden" name="op" value="cancel"/>
216 <input type="hidden" name="tab" value="to-be-staged"/>
217 <input type="hidden" name="id" value="[% cp.id | html %]"/>
219 <button type="submit" class="btn btn-default cancel-btn"><i class="fa fa-ban" aria-hidden="true"></i> Cancel</button>
229 <span>There are no pickups to be staged.</span>
233 [% IF tab == "staged-and-ready" %]
234 <div id="staged-and-ready" role="tabpanel" class="tab-pane active">
236 <div id="staged-and-ready" role="tabpanel" class="tab-pane">
238 <form method="post" class="form">
239 <input type="hidden" name="tab" value="staged-and-ready"/>
241 <button type="submit" class="btn btn-default"><i class="fa fa-refresh" aria-hidden="true"></i> Refresh</button>
245 [% IF staged_and_ready.count %]
246 <table class="table table-striped">
249 <th>Pickup date/time</th>
251 <th>Items for pickup</th>
257 [% FOREACH cp IN staged_and_ready %]
258 [% IF cp.staged_datetime && !cp.arrival_datetime %]
259 <tr class="[% class | html %]">
260 <td>[% cp.scheduled_pickup_datetime | $KohaDates with_hours = 1 %]</td>
262 [% PROCESS patron_info %]
265 [% PROCESS waiting_holds %]
268 [% cp.staged_by_staff.firstname | html %] [% cp.staged_by_staff.surname | html %]
271 <form method="post" class="form">
272 <input type="hidden" name="op" value="mark-patron-has-arrived"/>
273 <input type="hidden" name="tab" value="staged-and-ready"/>
274 <input type="hidden" name="id" value="[% cp.id | html %]"/>
276 <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>
280 <form method="post" class="form">
281 <input type="hidden" name="op" value="mark-as-delivered"/>
282 <input type="hidden" name="tab" value="staged-and-ready"/>
283 <input type="hidden" name="id" value="[% cp.id | html %]"/>
285 <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>
289 <form method="post" class="form">
290 <input type="hidden" name="op" value="mark-as-unstaged"/>
291 <input type="hidden" name="tab" value="staged-and-ready"/>
292 <input type="hidden" name="id" value="[% cp.id | html %]"/>
294 <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>
304 <span>There are no pickups staged and ready.</span>
308 [% IF tab == "patron-is-outside" %]
309 <div id="patron-is-outside" role="tabpanel" class="tab-pane active">
311 <div id="patron-is-outside" role="tabpanel" class="tab-pane">
313 <form method="post" class="form">
314 <input type="hidden" name="tab" value="patron-is-outside"/>
316 <button type="submit" class="btn btn-default"><i class="fa fa-refresh" aria-hidden="true"></i> Refresh</button>
320 [% IF patron_outside.count %]
321 <table class="table table-striped">
324 <th>Pickup date/time</th>
326 <th>Items for pickup</th>
332 [% FOREACH cp IN patron_outside %]
333 [% IF cp.arrival_datetime && !cp.delivered_datetime %]
334 <tr class="[% class | html %]">
335 <td>[% cp.scheduled_pickup_datetime | $KohaDates with_hours = 1 %]</td>
337 [% PROCESS patron_info %]
340 [% PROCESS waiting_holds %]
343 [% cp.staged_by_staff.firstname | html %] [% cp.staged_by_staff.surname | html %]
346 <form method="post" class="form">
347 <input type="hidden" name="op" value="mark-as-delivered"/>
348 <input type="hidden" name="tab" value="patron-is-outside"/>
349 <input type="hidden" name="id" value="[% cp.id | html %]"/>
351 <button type="submit" class="btn btn-default mark-as-delivered-btn"><i class="fa fa-envelope" aria-hidden="true"></i> Mark as delivered</button>
355 <form method="post" class="form">
356 <input type="hidden" name="op" value="mark-as-staged"/>
357 <input type="hidden" name="tab" value="patron-is-outside"/>
358 <input type="hidden" name="id" value="[% cp.id | html %]"/>
360 <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>
364 <form method="post" class="form">
365 <input type="hidden" name="op" value="mark-as-unstaged"/>
366 <input type="hidden" name="tab" value="patron-is-outside"/>
367 <input type="hidden" name="id" value="[% cp.id | html %]"/>
369 <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>
379 <span>There are no patrons waiting outside.</span>
383 [% IF tab == "delivered-today" %]
384 <div id="delivered-today" role="tabpanel" class="tab-pane active">
386 <div id="delivered-today" role="tabpanel" class="tab-pane">
388 <form method="post" class="form">
389 <input type="hidden" name="tab" value="delivered-today"/>
391 <button type="submit" class="btn btn-default"><i class="fa fa-refresh" aria-hidden="true"></i> Refresh</button>
395 [% IF delivered_today.count %]
396 <table class="table table-striped">
399 <th>Deliver date/time</th>
401 <th>Items checked out</th>
405 [% FOREACH cp IN delivered_today %]
406 [% IF cp.delivered_datetime %]
407 <tr class="[% class | html %]">
408 <td>[% cp.delivered_datetime | $KohaDates with_hours = 1 %]</td>
410 [% PROCESS patron_info %]
413 [% FOREACH c IN cp.checkouts %]
414 [% IF date.format(c.issuedate, format = '%Y-%m-%d') == today_iso %]
415 <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/>
425 <span>No pickups have been delivered today.</span>
429 [% IF tab == "schedule-pickup" %]
430 <div id="schedule-pickup" role="tabpanel" class="tab-pane active">
432 <div id="schedule-pickup" role="tabpanel" class="tab-pane">
434 [% IF !patron || ( patron && existing_curbside_pickups.count >= 1 ) %]
435 [% IF existing_curbside_pickups.count >= 1 %]
436 <div class="dialog alert">
437 [% patron.firstname | html %] [% patron.surname | html %] ([% patron.cardnumber | html %]) already has a scheduled pickup for this library.
440 <div class="form-group">
441 <label class="sr-only" for="input-patron-cardnumber">Cardnumber</label>
442 <div class="input-group">
443 <div class="input-group-addon">Search a patron</div>
444 <input autocomplete="off" id="find-patron" class="form-control" type="text" style="width:25%" class="noEnterSubmit" placeholder="Enter patron cardnumber or name"//>
448 [% SET waiting_holds = patron.holds.search( found => 'W', branchcode => Branches.GetLoggedInBranchcode ) %]
449 [% IF !policy.enable_waiting_holds_only || waiting_holds.count > 0 %]
450 <form id="create-pickup" method="post">
451 <fieldset class="rows" style="float: none;">
452 <input type="hidden" name="borrowernumber" value="[% patron.id | html %]"/>
453 <input type="hidden" name="op" value="create-pickup"/>
454 <input type="hidden" name="tab" value="schedule-pickup"/>
457 <label>Patron: </label>
458 <span>[% INCLUDE 'patron-title.inc' patron=patron %]</span>
459 <a title="Search for another patron" href="/cgi-bin/koha/circ/curbside_pickups.pl?tab=schedule-pickup"><i class="fa fa-search"></i></a>
463 <label>Items ready for pickup: </label>
465 [% IF waiting_holds.count %]
466 [% FOREACH h IN waiting_holds %]
468 <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>)
472 <span>There are no waiting holds for this patron at this library.</span>
478 <label for="pickup_date">Pickup date: </label>
479 <input id="pickup_date" name="pickup_date" required="required" class="flatpickr" />
482 <li id="pickup-times" class="radio"></li>
485 <label for="notes">Notes: </label>
486 <input id="notes" name="notes" type="text" />
491 <fieldset class="action">
492 <input type="submit" id="schedule-pickup-button" class="btn btn-primary" value="Submit" />
496 <div class="dialog alert">The patron does not have waitings holds.</div>
502 [% IF Koha.Preference('CircSidebar') %]
503 </div> <!-- /.col-sm-10.col-sm-push-2 -->
504 <div class="col-sm-2 col-sm-pull-10">
506 [% INCLUDE 'circ-nav.inc' %]
508 </div> <!-- /.col-sm-2.col-sm-pull-10 -->
509 </div> <!-- /.row -->
513 </div> <!-- /.row -->
515 [% MACRO jsinclude BLOCK %]
516 [% Asset.js("lib/dayjs/dayjs.min.js") | $raw %]
517 [% Asset.js("lib/dayjs/plugin/isSameOrAfter.js") | $raw %]
518 [% Asset.js("lib/dayjs/plugin/customParseFormat.js") | $raw %]
519 <script>dayjs.extend(window.dayjs_plugin_isSameOrAfter)</script>
520 <script>dayjs.extend(window.dayjs_plugin_customParseFormat)</script>
521 [% INCLUDE 'calendar.inc' %]
522 [% INCLUDE 'js-patron-format.inc' %]
524 let pickups = [% To.json(curbside_pickups.unblessed) | $raw %];
525 let policy = [% To.json(policy.unblessed) | $raw %];
527 let existingPickupMoments = [];
528 pickups.forEach(function(pickup){
529 let scheduled_pickup_datetime = pickup.scheduled_pickup_datetime;
530 let pickupMoment = dayjs(scheduled_pickup_datetime);
532 existingPickupMoments.push(pickupMoment);
535 let opening_slots = [% To.json(policy.opening_slots.unblessed) | $raw %];
536 let slots_per_day = {};
537 opening_slots.forEach(function(slot){
539 if(!slots_per_day[day]) slots_per_day[day] = [];
540 slots_per_day[day].push(slot);
544 $(document).ready(function() {
546 $('#schedule-pickup-tab').on('click', function() {
547 $('#input-patron-cardnumber').focus();
550 const pickup_date = document.querySelector("#pickup_date");
552 const fp = pickup_date._flatpickr;
553 fp.set('disable', [function(date) {
554 return !slots_per_day.hasOwnProperty(date.getDay());
558 $("#pickup_date").on('change', function() {
560 $('#pickup-times').empty();
561 $('#schedule-pickup-button').prop( 'disabled', 1 );
563 var currentDate = $(this).val();
565 let selectedDate = dayjs(currentDate);
567 let pickupSlots = [];
568 let available_count = 0;
569 let dow = selectedDate.day(); // Sunday is 0 (at least for now)
570 if (!slots_per_day[dow]){
571 $('#pickup-times').html("<div>"+_("No pickup time defined for this day.")+"</div>");
575 slots_per_day[dow].forEach(function(slot){
576 let pickup_interval = policy.pickup_interval;
577 if (!pickup_interval) {
578 $('#pickup-times').html("<div>"+_("No pickup time defined for this day.")+"</div>");
582 let listStartMoment = selectedDate.hour(slot.start_hour).minute(slot.start_minute);
583 let listEndMoment = selectedDate.hour(slot.end_hour).minute(slot.end_minute);
585 let keep_going = true;
588 // Initialize pickup slots starting at opening time
589 let pickupIntervalStartMoment = listStartMoment;
590 let pickupIntervalEndMoment = listStartMoment.add(pickup_interval, 'minutes');
592 let available = true;
593 let display_slot = true
595 if (pickupIntervalStartMoment.isBefore(now)) {
596 // Slots in the past are unavailable
598 display_slot = false;
601 if (pickupIntervalEndMoment.isAfter(listEndMoment)) {
602 // Slots after the end of pickup times for the day are unavailable
606 let pickups_scheduled = 0;
607 existingPickupMoments.forEach(function(pickupMoment){
608 // An existing pickup time
609 if (pickupMoment.isSameOrAfter(pickupIntervalStartMoment) && pickupMoment.isBefore(pickupIntervalEndMoment)) {
610 // This calculated pickup is in use by another scheduled pickup
615 if (pickups_scheduled >= policy.patrons_per_interval) {
619 if ( display_slot ) {
622 "available": available,
623 "moment": pickupIntervalStartMoment,
624 "pickups_scheduled": pickups_scheduled
633 pickupIntervalStartMoment = pickupIntervalEndMoment;
634 pickupIntervalEndMoment = pickupIntervalStartMoment.add(pickup_interval, 'minutes');
635 if (pickupIntervalEndMoment.isAfter(listEndMoment)) {
636 // This latest slot is after the end of pickup times for the day, so we can stop
642 for (let i = 0; i < pickupSlots.length; i++) {
643 let pickupSlot = pickupSlots[i];
644 let optText = pickupSlot.moment.format("HH:mm");
645 let optValue = pickupSlot.moment.format("YYYY-MM-DD HH:mm:ss");
646 let pickups_scheduled = pickupSlot.pickups_scheduled;
647 let disabled = pickupSlot.available ? "" : "disabled";
648 $("#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>`);
651 $('#pickup-times').show();
652 $('#schedule-pickup-button').prop( 'disabled', available_count <= 0 );
655 $("#create-pickup").on('submit', function(){
656 if ( ! $("input[type='radio']:checked").length ) {
657 alert(_("Please select a date and a pickup time"))
663 if ( $("#find-patron").length ) {
664 patron_autocomplete($("#find-patron"), { 'on-select-callback': function( event, ui ) {
665 window.location.href = "/cgi-bin/koha/circ/curbside_pickups.pl?op=find-patron&borrowernumber=" + ui.item.patron_id;
676 [% INCLUDE 'intranet-bottom.inc' %]