View | Details | Raw Unified | Return to bug 30650
Collapse All | Expand All

(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/usermenu.inc (+9 lines)
Lines 139-144 Link Here
139
                <a href="/cgi-bin/koha/opac-illrequests.pl">your interlibrary loan requests</a></li>
139
                <a href="/cgi-bin/koha/opac-illrequests.pl">your interlibrary loan requests</a></li>
140
            [% END %]
140
            [% END %]
141
141
142
            [% IF Koha.Preference( 'CurbsidePickup' ) %]
143
                [% IF ( ermview ) %]
144
                    <li class="active">
145
                [% ELSE %]
146
                    <li>
147
                [% END %]
148
                <a href="/cgi-bin/koha/opac-curbside-pickups.pl">your curbside pickups</a></li>
149
            [% END %]
150
142
            [% IF Koha.Preference('UseRecalls') %]
151
            [% IF Koha.Preference('UseRecalls') %]
143
                [% IF ( recallsview ) %]
152
                [% IF ( recallsview ) %]
144
                    <li class="active">
153
                    <li class="active">
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/modules/opac-curbside-pickups.tt (+354 lines)
Line 0 Link Here
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
12
<style>
13
    .pickup_time input[type='radio'] {
14
        display: none;
15
    }
16
    .pickup_time {
17
        /*margin: .2em;*/
18
    }
19
    .pickup_time label {
20
        background-color: #ffffcc;
21
        display: inline-block;
22
        cursor: pointer;
23
    }
24
    .pickup_time input[type='radio']:checked + label {
25
        background-color: #bcdb89;
26
    }
27
</style>
28
29
[% INCLUDE 'doc-head-close.inc' %]
30
[% BLOCK cssinclude %][% END %]
31
</head>
32
[% INCLUDE 'bodytag.inc' bodyid='opac-curside-pickups' bodyclass='scrollto' %]
33
[% INCLUDE 'masthead.inc' %]
34
<div class="main">
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>
39
            </li>
40
            <li class="breadcrumb-item">
41
                <a href="/cgi-bin/koha/opac-user.pl">[% INCLUDE 'patron-title.inc' patron = logged_in_user %]</a>
42
            </li>
43
44
            <li class="breadcrumb-item active">
45
                <a href="#" aria-current="page">Curbside pickups</a>
46
            </li>
47
        </ol> <!-- / .breadcrumb -->
48
    </nav> <!-- /#breadcrumbs -->
49
50
    <div class="container-fluid">
51
        <div class="row">
52
            <div class="col col-lg-2 order-2 order-lg-1">
53
                <div id="navigation">
54
                    [% INCLUDE 'navigation.inc' IsPatronPage=1 %]
55
                </div>
56
            </div>
57
            <div class="col-md-12 col-lg-10 order-1">
58
                <div id='pickupdetails' class="maincontent">
59
                    <h2>Curbside pickups</h2>
60
61
                    [% FOR m IN messages %]
62
                        <div class="dialog [% m.type | html %]">
63
                            [% SWITCH m.code %]
64
                            [% CASE 'too_many_pickups' %]
65
                                <span>You already have a scheduled pickup for this library.</span>
66
                            [% CASE 'cannot_checkout' %]
67
                                <span>Unable to check the items out. Please contact the library.</span>
68
                            [% CASE 'library_notified' %]
69
                                <span>The library has been notified of your arrival.</span>
70
                            [% CASE %]
71
                                <span>[% m.code | html %]</span>
72
                            [% END %]
73
                        </div>
74
                    [% END %]
75
76
                    <div id="opac-pickups-views" class="toptabs">
77
                        <ul class="nav nav-tabs" role="tablist">
78
                            <li class="nav-item" role="presentation" id="tab-user-pickups"><a href="#user-pickups" class="nav-link" aria-controls="user-pickups" aria-selected="true" role="tab" data-toggle="tab">Your pickups</a></li>
79
                            [% IF policies.count %]
80
                                <li class="nav-item" role="presentation" id="tab-user-schedule-pickup"><a href="#user-schedule-pickup" class="nav-link" aria-controls="user-schedule-pickup" aria-selected="false" role="tab" data-toggle="tab">Schedule a pickup</a></li>
81
                            [% END %]
82
                        </ul>
83
84
                        <div class="tab-content">
85
                            <div role="tabpanel" class="tab-pane" id="user-pickups" aria-labelledby="tab-user-pickups">
86
                                [% IF patron_curbside_pickups.count %]
87
                                    <table id="pickups-table" class="table table-striped">
88
                                        <thead>
89
                                            <tr>
90
                                                <th>Pickup library</td>
91
                                                <th>Schedule</th>
92
                                                <th>Notes</th>
93
                                                <th>Actions</th>
94
                                            </tr>
95
                                        </thead>
96
                                        <tbody>
97
                                            [% FOR p IN patron_curbside_pickups %]
98
                                                <tr>
99
                                                    <td>[% Branches.GetName(p.branchcode) | html %]</td>
100
                                                    <td>[% p.scheduled_pickup_datetime | $KohaDates with_hours => 1 %]</td>
101
                                                    <td>[% p.notes | html %]</td>
102
                                                    <td>
103
                                                        <form method="post">
104
                                                            <input type="hidden" name="op" value="arrival-alert" />
105
                                                            <input type="hidden" name="pickup_id" value="[% p.id | html %]" />
106
                                                            [% IF ! p.staged_datetime || p.arrival_datetime %]
107
                                                            <button class="btn disabled" disabled href="#" >
108
                                                            [% ELSE %]
109
                                                            <button type="submit" class="btn" href="#" >
110
                                                            [% END %]
111
                                                            <i class="fa fa-bell" aria-hidden="true"></i> Alert staff of your arrival
112
                                                            </button>
113
                                                        </form>
114
                                                        <p>
115
                                                        <form method="post">
116
                                                            <input type="hidden" name="op" value="cancel-pickup" />
117
                                                            <input type="hidden" name="pickup_id" value="[% p.id | html %]" />
118
                                                            [% IF p.delivered_datetime %]
119
                                                                <button class="btn disabled" disabled href="#" >
120
                                                            [% ELSE %]
121
                                                                <button type="submit" class="btn" href="#" >
122
                                                            [% END %]
123
                                                                <i class="fa fa-ban" aria-hidden="true"></i> Cancel this pickup</button>
124
                                                        </form>
125
                                                    </td>
126
                                                </tr>
127
                                            [% END %]
128
                                        </tbody>
129
                                    </table>
130
                                [% ELSE %]
131
                                    <div>No curbside pickups.</div>
132
                                [% END %]
133
                            </div>
134
135
                            [% IF policies.count %]
136
                                <div role="tabpanel" class="tab-pane" id="user-schedule-pickup" aria-labelledby="tab-user-schedule-pickup">
137
                                    <form id="create-pickup" method="post">
138
                                        <fieldset class="rows">
139
                                            <ol>
140
                                                <li>
141
                                                    <label for="pickup_branch">Pickup library:</label>
142
                                                    <select name="pickup_branch" id="pickup-branch">
143
                                                        <option value="">Select a library</option>
144
                                                        [% FOR p IN policies %]
145
                                                            <option value="[% p.branchcode | html %]">[% Branches.GetName(p.branchcode) | html %]</option>
146
147
                                                        [% END %]
148
                                                    </select>
149
                                                    <span id="existing-pickup-warning" class="required">You already have a pickup scheduled for this library.</span>
150
                                                </li>
151
                                                <li>
152
                                                    <label for="pickup_date">Pickup date:</label>
153
                                                    <input name="pickup_date" type="text" class="flatpickr" id="pickup-date" disabled required="required"/>
154
                                                    <span class="required">Required</span>
155
                                                </li>
156
157
                                                <li id="pickup-times" class="radio"></li>
158
159
                                                <li>
160
                                                    <label for="notes">Notes:</label>
161
                                                    <input name="notes" id="notes" />
162
                                                </li>
163
                                            </ol>
164
                                        </fieldset>
165
166
                                        <fieldset class="action">
167
                                            <input type="hidden" name="op" value="create-pickup" />
168
                                            <input type="submit" id="schedule-pickup-button" class="btn btn-default" disabled value="Schedule pickup" />
169
                                        </fieldset>
170
                                    </form>
171
                                </div>
172
                            [% END %]
173
                        </div>
174
                    </div>
175
                </div>
176
            </div> <!-- / .col-lg-10 -->
177
        </div> <!-- / .row -->
178
    </div> <!-- / .container-fluid -->
179
</div> <!-- / .main -->
180
181
[% INCLUDE 'opac-bottom.inc' %]
182
183
[% BLOCK jsinclude %]
184
    [% Asset.js("lib/dayjs/dayjs.min.js") | $raw %]
185
    [% Asset.js("lib/dayjs/plugin/isSameOrAfter.js") | $raw %]
186
    <script>dayjs.extend(window.dayjs_plugin_isSameOrAfter)</script>
187
    [% INCLUDE 'calendar.inc' %]
188
    <script>
189
        [% SET pickup_exists_in = [] %]
190
        [% FOR p IN patron_curbside_pickups %]
191
            [% UNLESS p.delivered_by  %]
192
                [% pickup_exists_in.push(p.branchcode) %]
193
            [% END %]
194
        [% END %]
195
        let pickup_exists_in = [% To.json(pickup_exists_in.unique()) | $raw %];
196
197
        let pickups = [% To.json(curbside_pickups.unblessed) | $raw %];
198
        let policies = [% To.json(policies.unblessed) | $raw %];
199
        policies = policies.reduce((map, e) => {
200
            map[e.branchcode] = e;
201
            return map;
202
        }, {});
203
        [% FOR p IN policies %]
204
            var opening_slots = [% To.json(p.opening_slots.unblessed) | $raw %];
205
            var slots_per_day = {};
206
            opening_slots.forEach(function(slot){
207
                let day = slot.day;
208
                if(!slots_per_day[day]) slots_per_day[day] = [];
209
                slots_per_day[day].push(slot);
210
            });
211
            policies['[% p.branchcode | html %]'].slots_per_day = slots_per_day;
212
        [% END %]
213
214
        let existingPickupMoments = [];
215
        pickups.forEach(function(pickup){
216
            let scheduled_pickup_datetime = pickup.scheduled_pickup_datetime;
217
            let pickupMoment = dayjs(scheduled_pickup_datetime);
218
219
            if(!existingPickupMoments[pickup.branchcode]) existingPickupMoments[pickup.branchcode] = [];
220
            existingPickupMoments[pickup.branchcode].push(pickupMoment);
221
        });
222
223
        $('#pickup-branch').on('change', function() {
224
            let branchcode = $(this).val();
225
226
            let existing_pickup = pickup_exists_in.indexOf(branchcode) != -1;
227
228
            $('#pickup-date').val("");
229
            $('#pickup-time').val("");
230
            $('#pickup-times').hide();
231
            $('#schedule-pickup-button').prop('disabled', true);
232
233
            if (existing_pickup) {
234
                $('#existing-pickup-warning').show();
235
                $('#pickup-date').prop("disabled", true);
236
            } else {
237
                $('#existing-pickup-warning').hide();
238
                $('#pickup-date').prop("disabled", branchcode == "");
239
            }
240
        });
241
242
        $("#pickup-date").on('change', function() {
243
244
            $('#pickup-times').empty();
245
            $('#schedule-pickup-button').prop( 'disabled', 1 );
246
247
            var currentDate = $(this).val();
248
            let branchcode = $("#pickup-branch").val();
249
            let policy = policies[branchcode];
250
251
            let selectedDate = dayjs(currentDate);
252
253
            let pickupSlots = [];
254
            let available_count = 0;
255
            let dow = selectedDate.day(); // Sunday is 0 (at least for now)
256
            if (!policy.slots_per_day[dow]){
257
                $('#pickup-times').html("<div>"+_("No pickup time define for this day.")+"</div>");
258
                return;
259
            }
260
261
            policy.slots_per_day[dow].forEach(function(slot){
262
                let pickup_interval = policy.pickup_interval;
263
264
                let listStartMoment = selectedDate.hour(slot.start_hour).minute(slot.start_minute);
265
                let listEndMoment = selectedDate.hour(slot.end_hour).minute(slot.end_minute);
266
267
                let keep_going = true;
268
                let now = dayjs();
269
270
                // Initialize pickup slots starting at opening time
271
                let pickupIntervalStartMoment = listStartMoment;
272
                let pickupIntervalEndMoment   = listStartMoment.add(pickup_interval, 'minutes');
273
                while (keep_going) {
274
                    let available = true;
275
276
                    if (pickupIntervalStartMoment.isBefore(now)) {
277
                        // Slots in the past are unavailable
278
                        available = false;
279
                    }
280
281
                    if (pickupIntervalEndMoment.isAfter(listEndMoment)) {
282
                        // Slots after the end of pickup times for the day are unavailable
283
                        available = false;
284
                    }
285
286
                    let pickups_scheduled = 0;
287
288
                    if (existingPickupMoments[branchcode]){
289
                        existingPickupMoments[branchcode].forEach(function(pickupMoment){
290
                            // An existing pickup time
291
                            if (pickupMoment.isSameOrAfter(pickupIntervalStartMoment) && pickupMoment.isBefore(pickupIntervalEndMoment)) {
292
                                // This calculated pickup is in use by another scheduled pickup
293
                                pickups_scheduled++;
294
                            }
295
                        });
296
                    }
297
298
                    if (pickups_scheduled >= policy.patrons_per_interval) {
299
                        available = false;
300
                    }
301
302
                    pickupSlots.push(
303
                        {
304
                            "available": available,
305
                            "moment": pickupIntervalStartMoment,
306
                            "pickups_scheduled": pickups_scheduled
307
                        }
308
                    );
309
310
                    if ( available ) {
311
                        available_count++;
312
                    }
313
314
                    pickupIntervalStartMoment = pickupIntervalEndMoment;
315
                    pickupIntervalEndMoment = pickupIntervalStartMoment.add(pickup_interval, 'minutes');
316
                    if (pickupIntervalEndMoment.isAfter(listEndMoment)) {
317
                        // This latest slot is after the end of pickup times for the day, so we can stop
318
                        keep_going = false;
319
                    }
320
                }
321
322
                $('#schedule-pickup-button').prop( 'disabled', available_count <= 0 );
323
            });
324
325
            for (let i = 0; i < pickupSlots.length; i++) {
326
                let pickupSlot = pickupSlots[i];
327
                let optText = pickupSlot.moment.format("HH:mm");
328
                let optValue = pickupSlot.moment.format("YYYY-MM-DD HH:mm:ss");
329
                let pickups_scheduled = pickupSlot.pickups_scheduled;
330
                let disabled = pickupSlot.available ? "" : "disabled";
331
                $("#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>`);
332
            }
333
334
            $('#pickup-times').show();
335
        });
336
337
338
        $(document).ready(function() {
339
            $("#existing-pickup-warning").hide();
340
            $("#pickup-times").hide();
341
            if( $("#opac-pickups-views .tab-pane.active").length < 1 ){
342
                $("#opac-pickups-views a:first").tab("show");
343
            }
344
345
            $("#create-pickup").on('submit', function(){
346
                if ( ! $("input[type='radio']:checked").length ) {
347
                    alert(_("Please select a date and a pickup time"))
348
                    return false;
349
                }
350
                return true;
351
            });
352
        });
353
    </script>
354
[% END %]
(-)a/opac/opac-curbside-pickups.pl (-1 / +128 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/perl
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
19
use Modern::Perl;
20
use CGI qw ( -utf8 );
21
use Try::Tiny;
22
use C4::Members;
23
use C4::Auth qw( get_template_and_user );
24
use C4::Output qw( output_html_with_http_headers );
25
26
use Koha::DateUtils qw( dt_from_string );
27
use Koha::CurbsidePickups;
28
use Koha::CurbsidePickupPolicies;
29
use Koha::Libraries;
30
use Koha::Patrons;
31
32
my $input = CGI->new;
33
my ( $template, $borrowernumber, $cookie ) = get_template_and_user(
34
    {
35
        template_name   => "opac-curbside-pickups.tt",
36
        query           => $input,
37
        type            => "opac",
38
    }
39
);
40
41
unless( C4::Context->preference('CurbsidePickup') ) {
42
    print $input->redirect("/cgi-bin/koha/errors/404.pl");
43
    exit;
44
}
45
46
my $op = $input->param('op') || 'list';
47
48
my $logged_in_patron = Koha::Patrons->find($borrowernumber);
49
my $branchcode = $input->param('pickup_branch');
50
my @messages;
51
52
if ( $op eq 'create-pickup' ) {
53
    my $scheduled_pickup_datetime = $input->param('pickup_time');
54
    my $notes                     = $input->param('notes');
55
56
    try {
57
        Koha::CurbsidePickup->new(
58
            {
59
                branchcode                => $branchcode,
60
                borrowernumber            => $borrowernumber,
61
                scheduled_pickup_datetime => dt_from_string($scheduled_pickup_datetime),
62
                notes                     => $notes,
63
            }
64
        )->store();
65
    } catch {
66
        if ( $_->isa('Koha::Exceptions::CubsidePickup::TooManyPickups') ) {
67
            push @messages, {
68
                type   => 'error',
69
                code   => 'too_many_pickups',
70
                patron => Koha::Patrons->find($borrowernumber)
71
            };
72
        } else {
73
            warn $_;
74
            push @messages, {
75
                type   => 'error',
76
                code   => 'something_wrong_happened',
77
            };
78
        }
79
    }
80
        # $self->_notify_new_pickup($curbside_pickup); TODO
81
}
82
elsif ( $op eq 'cancel-pickup' ) {
83
    my $id              = $input->param('pickup_id');
84
    my $curbside_pickup = Koha::CurbsidePickups->search(
85
        { borrowernumber => $borrowernumber } )->find($id);
86
    $curbside_pickup->delete
87
      if $curbside_pickup
88
      && !$curbside_pickup->delivered_datetime;
89
}
90
elsif ( $op eq 'arrival-alert' ) {
91
    my $id              = $input->param('pickup_id');
92
    my $curbside_pickup = Koha::CurbsidePickups->search(
93
        { borrowernumber => $borrowernumber } )->find($id);
94
    $curbside_pickup->mark_patron_has_arrived if $curbside_pickup;
95
    push @messages, {
96
        type => 'message',
97
        code => 'library_notified',
98
    }
99
}
100
101
$template->param(
102
    policies => Koha::CurbsidePickupPolicies->search(
103
        {
104
            enabled                 => 1,
105
            patron_scheduled_pickup => 1,
106
        }
107
    ),
108
    patron_curbside_pickups => Koha::CurbsidePickups->search(
109
        {
110
            borrowernumber            => $logged_in_patron->borrowernumber,
111
            scheduled_pickup_datetime => { '>' => \'DATE(NOW())' },
112
        },
113
        {
114
            order_by => { -asc => 'scheduled_pickup_datetime' }
115
        }
116
    ),
117
    curbside_pickups => Koha::CurbsidePickups->search(
118
        {
119
            scheduled_pickup_datetime => { '>' => \'DATE(NOW())' },
120
        },
121
        {
122
            order_by => { -asc => 'scheduled_pickup_datetime' }
123
        }
124
    ),
125
);
126
127
output_html_with_http_headers $input, $cookie, $template->output, undef,
128
  { force_no_caching => 1 };

Return to bug 30650