Bug 31261: Disable dates in the past for curbside pickups
[koha.git] / koha-tmpl / opac-tmpl / bootstrap / en / modules / opac-curbside-pickups.tt
1 [% USE raw %]
2 [% USE To %]
3 [% USE Koha %]
4 [% USE KohaDates %]
5 [% USE Branches %]
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 &rsaquo; [% IF ( LibraryNameTitle ) %][% LibraryNameTitle | html %][% ELSE %]Koha online[% END %] catalog</title>
11 [% FILTER collapse %]
12     <style>
13         .pickup_time input[type='radio'] {
14             display: none;
15         }
16         .pickup_time {
17             display: inline-block;
18         }
19         #pickup-times {
20             display: flex;
21             flex-wrap: wrap;
22         }
23         #pickup-time-slots {
24             max-width: 80%;
25         }
26         fieldset.rows .pickup_time label {
27             background: #e6e6e6 linear-gradient(180deg,#f0f0f0,#e6e6e6);
28             border: 1px solid #b3b3b3;
29             border-radius: 3px;
30             cursor: pointer;
31             float: none;
32             font-weight: normal;
33             padding: .2em .1em;
34             text-align: center;
35             width: 6rem;
36         }
37         fieldset.rows .pickup_time label:hover {
38             background: #e6e6e6 linear-gradient(180deg,#F7F7F7,#F0F0F0);
39         }
40         fieldset.rows .pickup_time label::before {
41             font-family: FontAwesome;
42             color: #AAA;
43             content: "\f1db";
44             display: inline-block;
45             padding-right: .5em;
46         }
47         fieldset.rows .pickup_time input:checked + label {
48             background: #5cb85c linear-gradient(180deg,#5cb85c,#4cae4c);
49             border: 1px solid #548e54;
50             color: #FFF;
51         }
52         fieldset.rows .pickup_time input:checked + label::before {
53             font-family: FontAwesome;
54             color: #FFF;
55             content: "\f05d";
56             display: inline-block;
57             padding-right: .5em;
58         }
59         fieldset.rows .pickup_time input:disabled + label {
60             background: #F0F0F0 none;
61             border: 1px solid #E6E6E6;
62             color: #6c6c6c;
63         }
64         fieldset.rows .pickup_time input:disabled + label:hover {
65             cursor: not-allowed;
66         }
67         fieldset.rows .pickup_time input:disabled + label::before {
68             content: "";
69             display: inline-block;
70         }
71         #existing-pickup-warning {
72             color: #c00;
73             display: inline-block;
74         }
75         .pickups_available {
76             font-size: 90%;
77         }
78         .pickups_available::before {
79             content: "(";
80         }
81         .pickups_available::after {
82             content: ")";
83         }
84     </style>
85 [% END %]
86 [% INCLUDE 'doc-head-close.inc' %]
87 [% BLOCK cssinclude %][% END %]
88 </head>
89 [% INCLUDE 'bodytag.inc' bodyid='opac-curside-pickups' bodyclass='scrollto' %]
90 [% INCLUDE 'masthead.inc' %]
91 <div class="main">
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>
96             </li>
97             <li class="breadcrumb-item">
98                 <a href="/cgi-bin/koha/opac-user.pl">[% INCLUDE 'patron-title.inc' patron = logged_in_user %]</a>
99             </li>
100
101             <li class="breadcrumb-item active">
102                 <a href="#" aria-current="page">Curbside pickups</a>
103             </li>
104         </ol> <!-- / .breadcrumb -->
105     </nav> <!-- /#breadcrumbs -->
106
107     <div class="container-fluid">
108         <div class="row">
109             <div class="col col-lg-2 order-2 order-lg-1">
110                 <div id="navigation">
111                     [% INCLUDE 'navigation.inc' IsPatronPage=1 %]
112                 </div>
113             </div>
114             <div class="col-md-12 col-lg-10 order-1">
115                 <div id='pickupdetails' class="maincontent">
116                     <h2>Curbside pickups</h2>
117
118                     [% FOR m IN messages %]
119                         [% IF m.type == "error" %]
120                         <div class="alert alert-warning">
121                         [% ELSE %]
122                         <div class="alert alert-info">
123                         [% END %]
124                             [% SWITCH m.code %]
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>
141                             [% CASE %]
142                                 <span>[% m.code | html %]</span>
143                             [% END %]
144                         </div>
145                     [% END %]
146
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>
152                             [% ELSE %]
153                                 <a href="#user-pickups" class="nav-link" aria-controls="user-pickups" role="tab" data-toggle="tab">Your pickups</a>
154                             [% END %]
155                             </li>
156
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>
161                                 [% ELSE %]
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>
163                                 [% END %]
164                                 </li>
165                             [% END %]
166                         </ul>
167
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">
171                             [% ELSE %]
172                             <div role="tabpanel" class="tab-pane" id="user-pickups" aria-labelledby="tab-user-pickups">
173                             [% END %]
174                                 [% IF patron_curbside_pickups.count %]
175                                     <table id="pickups-table" class="table table-striped">
176                                         <thead>
177                                             <tr>
178                                                 <th>Pickup library</td>
179                                                 <th>Schedule</th>
180                                                 <th>Notes</th>
181                                                 <th>Actions</th>
182                                             </tr>
183                                         </thead>
184                                         <tbody>
185                                             [% FOR p IN patron_curbside_pickups %]
186                                                 <tr>
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>
190                                                     <td>
191                                                         <form method="post">
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="#" >
196                                                             [% ELSE %]
197                                                             <button type="submit" class="btn" href="#" >
198                                                             [% END %]
199                                                             <i class="fa fa-bell" aria-hidden="true"></i> Alert staff of your arrival
200                                                             </button>
201                                                         </form>
202                                                         <p>
203                                                         <form method="post">
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="#" >
208                                                             [% ELSE %]
209                                                                 <button type="submit" class="btn" href="#" >
210                                                             [% END %]
211                                                                 <i class="fa fa-ban" aria-hidden="true"></i> Cancel this pickup</button>
212                                                         </form>
213                                                     </td>
214                                                 </tr>
215                                             [% END %]
216                                         </tbody>
217                                     </table>
218                                 [% ELSE %]
219                                     <div>No curbside pickups.</div>
220                                 [% END %]
221                             </div>
222
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">
226                                 [% ELSE %]
227                                 <div role="tabpanel" class="tab-pane active" id="user-schedule-pickup" aria-labelledby="tab-user-schedule-pickup">
228                                 [% END %]
229                                     <form id="create-pickup" method="post">
230                                         <fieldset class="rows">
231                                             <ol>
232                                                 <li>
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>
238
239                                                         [% END %]
240                                                     </select>
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>
243                                                 </li>
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>
248                                                 </li>
249
250                                                 <li id="pickup-times"></li>
251
252                                                 <li id="pickup_notes_item">
253                                                     <label for="notes">Notes:</label>
254                                                     <input name="notes" id="notes" />
255                                                 </li>
256                                             </ol>
257                                         </fieldset>
258
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" />
262                                         </fieldset>
263                                     </form>
264                                 </div>
265                             [% END %]
266                         </div>
267                     </div>
268                 </div>
269             </div> <!-- / .col-lg-10 -->
270         </div> <!-- / .row -->
271     </div> <!-- / .container-fluid -->
272 </div> <!-- / .main -->
273
274 [% INCLUDE 'opac-bottom.inc' %]
275
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' %]
283     <script>
284         [% SET pickup_exists_in = [] %]
285         [% FOR p IN patron_curbside_pickups %]
286             [% UNLESS p.delivered_by  %]
287                 [% pickup_exists_in.push(p.branchcode) %]
288             [% END %]
289         [% END %]
290         let pickup_exists_in = [% To.json(pickup_exists_in.unique()) | $raw %];
291
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;
296             return map;
297         }, {});
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){
303                 let day = slot.day;
304                 if(!slots_per_day[day]) slots_per_day[day] = [];
305                 slots_per_day[day].push(slot);
306             });
307             policies['[% p.branchcode | html %]'].slots_per_day = slots_per_day;
308
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;
313                 [% END %]
314             [% END %]
315         [% END %]
316
317         let existingPickupMoments = [];
318         pickups.forEach(function(pickup){
319             let scheduled_pickup_datetime = pickup.scheduled_pickup_datetime;
320             let pickupMoment = dayjs(scheduled_pickup_datetime);
321
322             if(!existingPickupMoments[pickup.branchcode]) existingPickupMoments[pickup.branchcode] = [];
323             existingPickupMoments[pickup.branchcode].push(pickupMoment);
324         });
325
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"));
331                 }
332             });
333
334             $('#pickup-branch').on('change', function() {
335                 let branchcode = $(this).val();
336
337                 let existing_pickup = pickup_exists_in.indexOf(branchcode) != -1;
338
339                 $('#pickup-date').val("");
340                 $('#pickup-time').val("");
341                 $('#pickup-times').hide();
342                 $('#schedule-pickup-button').prop('disabled', true);
343
344                 if (existing_pickup) {
345                     $('#existing-pickup-warning').show();
346                     $("#pickup-date,#pickup_date_item,#pickup_notes_item").hide();
347                 } else {
348                     $('#existing-pickup-warning').hide();
349                     $("#pickup-date,#pickup_date_item").show();
350                 }
351             });
352
353             const pickupDate_fp = document.getElementById("pickup-date")._flatpickr;
354             pickupDate_fp.set('disable', [function(date) {
355                 return !slots_per_day.hasOwnProperty(date.getDay());
356             }]);
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 );
362
363                 var currentDate = dateStr;
364                 let branchcode = $("#pickup-branch").val();
365                 let policy = policies[branchcode];
366
367                 let selectedDate = dayjs(currentDate);
368
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>");
374                     return;
375                 }
376
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>");
381                         return;
382                     }
383
384                     let listStartMoment = selectedDate.hour(slot.start_hour).minute(slot.start_minute);
385                     let listEndMoment = selectedDate.hour(slot.end_hour).minute(slot.end_minute);
386
387                     let keep_going = true;
388                     let now = dayjs();
389
390                     // Initialize pickup slots starting at opening time
391                     let pickupIntervalStartMoment = listStartMoment;
392                     let pickupIntervalEndMoment   = listStartMoment.add(pickup_interval, 'minutes');
393                     while (keep_going) {
394                         let available = true;
395                         let display_slot = true;
396
397                         if (pickupIntervalStartMoment.isBefore(now)) {
398                             // Slots in the past are unavailable
399                             available = false;
400                             display_slot = false;
401                         }
402
403                         if (pickupIntervalEndMoment.isAfter(listEndMoment)) {
404                             // Slots after the end of pickup times for the day are unavailable
405                             available = false;
406                         }
407
408                         let pickups_scheduled = 0;
409
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
415                                     pickups_scheduled++;
416                                 }
417                             });
418                         }
419
420                         if (pickups_scheduled >= policy.patrons_per_interval) {
421                             available = false;
422                         }
423
424                         if ( display_slot ) {
425                             pickupSlots.push(
426                                 {
427                                     "available": available,
428                                     "moment": pickupIntervalStartMoment,
429                                     "pickups_scheduled": pickups_scheduled
430                                 }
431                             );
432                         }
433
434                         if ( available ) {
435                             available_count++;
436                         }
437
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
442                             keep_going = false;
443                         }
444                     }
445
446                     $('#schedule-pickup-button').prop( 'disabled', available_count <= 0 );
447                 });
448
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>`);
457                 }
458
459                 $("#pickup_notes_item,#pickup-times").show();
460             });
461
462             $("#pickup_date_item,#pickup_notes_item,#pickup-times").hide();
463
464             $("#create-pickup").on('submit', function(){
465                 if ( ! $("input[type='radio']:checked").length ) {
466                     alert(_("Please select a date and a pickup time"));
467                     return false;
468                 }
469                 return true;
470             });
471             $("#pickup-times").tooltip({
472                 selector: ".pickup_select"
473             });
474         });
475     </script>
476 [% END %]