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

(-)a/circ/curbside_pickups.pl (+153 lines)
Line 0 Link Here
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
use Modern::Perl;
19
use CGI qw ( -utf8 );
20
use Try::Tiny;
21
use C4::Context;
22
use C4::Auth qw( get_template_and_user );
23
use C4::Output qw( output_html_with_http_headers );
24
25
use Koha::DateUtils qw( dt_from_string );
26
use Koha::CurbsidePickups;
27
use Koha::CurbsidePickupPolicies;
28
use Koha::Libraries;
29
use Koha::Patrons;
30
31
my $input       = CGI->new;
32
my $op          = $input->param('op') || 'list';
33
my $tab         = $input->param('tab'),
34
my @messages;
35
36
my ( $template, $loggedinuser, $cookie ) = get_template_and_user(
37
    {
38
        template_name => "circ/curbside_pickups.tt",
39
        query         => $input,
40
        type          => "intranet",
41
        flagsrequired => { parameters => 'manage_curbside_pickups' },
42
    }
43
);
44
45
my $branchcode = C4::Context->userenv()->{'branch'};
46
my $libraries = Koha::Libraries->search( {}, { order_by => ['branchname'] } );
47
if ( $op eq 'find-patron' ) {
48
    my $cardnumber     = $input->param('cardnumber');
49
    my $borrowernumber = $input->param('borrowernumber');
50
51
    my $patron =
52
      $cardnumber
53
      ? Koha::Patrons->find( { cardnumber => $cardnumber } )
54
      : Koha::Patrons->find($borrowernumber);
55
56
    my $existing_curbside_pickups;
57
58
    if ( $patron ){
59
        $existing_curbside_pickups = Koha::CurbsidePickups->search(
60
            {
61
                branchcode                => $branchcode,
62
                borrowernumber            => $patron->id,
63
                delivered_datetime        => undef,
64
                scheduled_pickup_datetime => { '>' => \'DATE(NOW())' },
65
            }
66
        );
67
    } else {
68
        push @messages, {
69
            type => 'error',
70
            code => 'no_patron_found',
71
            cardnumber => $cardnumber
72
        };
73
    }
74
75
    $tab = 'schedule-pickup';
76
    $template->param(
77
        patron      => $patron,
78
        existing_curbside_pickups => $existing_curbside_pickups,
79
    );
80
}
81
elsif ( $op eq 'create-pickup' ) {
82
    my $borrowernumber            = $input->param('borrowernumber');
83
    my $scheduled_pickup_datetime = $input->param('pickup_time');
84
    my $notes                     = $input->param('notes');
85
86
    try {
87
        Koha::CurbsidePickup->new(
88
            {
89
                branchcode                => $branchcode,
90
                borrowernumber            => $borrowernumber,
91
                scheduled_pickup_datetime => dt_from_string($scheduled_pickup_datetime),
92
                notes                     => $notes,
93
            }
94
        )->store();
95
    } catch {
96
        if ( $_->isa('Koha::Exceptions::CubsidePickup::TooManyPickups') ) {
97
            push @messages, {
98
                type   => 'error',
99
                code   => 'too_many_pickups',
100
                patron => Koha::Patrons->find($borrowernumber)
101
            };
102
        } else {
103
            warn $_;
104
            push @messages, {
105
                type   => 'error',
106
                code   => 'something_wrong_happened',
107
            };
108
        }
109
    }
110
        # $self->_notify_new_pickup($curbside_pickup); TODO
111
}
112
elsif ( $op eq 'cancel' ) {
113
    my $id              = $input->param('id');
114
    my $curbside_pickup = Koha::CurbsidePickups->find($id);
115
    $curbside_pickup->delete() if $curbside_pickup;
116
}
117
elsif ( $op eq 'mark-as-staged' ) {
118
    my $id              = $input->param('id');
119
    my $curbside_pickup = Koha::CurbsidePickups->find($id);
120
    $curbside_pickup->mark_as_staged if $curbside_pickup;
121
}
122
elsif ( $op eq 'mark-as-unstaged' ) {
123
    my $id              = $input->param('id');
124
    my $curbside_pickup = Koha::CurbsidePickups->find($id);
125
    $curbside_pickup->mark_as_unstaged if $curbside_pickup;
126
}
127
elsif ( $op eq 'mark-patron-has-arrived' ) {
128
    my $id              = $input->param('id');
129
    my $curbside_pickup = Koha::CurbsidePickups->find($id);
130
    $curbside_pickup->mark_patron_has_arrived if $curbside_pickup;
131
}
132
elsif ( $op eq 'mark-as-delivered' ) {
133
    my $id = $input->param('id');
134
    my $curbside_pickup = Koha::CurbsidePickups->find($id);
135
    # FIXME Add a try-catch here
136
    $curbside_pickup->mark_as_delivered if $curbside_pickup;
137
}
138
139
$template->param(
140
    messages => \@messages,
141
    op       => $op,
142
    tab      => $tab,
143
    policy => Koha::CurbsidePickupPolicies->find({ branchcode => $branchcode }),
144
    curbside_pickups => Koha::CurbsidePickups->search(
145
        {
146
            branchcode                => $branchcode,
147
            scheduled_pickup_datetime => { '>' => \'DATE(NOW())' },
148
        }
149
      ),
150
);
151
152
153
output_html_with_http_headers $input, $cookie, $template->output;
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/js-date-format.inc (-1 / +1 lines)
Lines 57-63 Link Here
57
57
58
        window.$time = function(value, options) {
58
        window.$time = function(value, options) {
59
            if(!value) return '';
59
            if(!value) return '';
60
            var tz = (opitons&&options.tz)||def_tz;
60
            var tz = (options&&options.tz)||def_tz;
61
            var m = moment(value);
61
            var m = moment(value);
62
            if(tz) m.tz(tz);
62
            if(tz) m.tz(tz);
63
63
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/admin/curbside_pickup.tt (-8 / +6 lines)
Lines 195-206 Link Here
195
            return format_hhmm(start) + _(" to ") + format_hhmm(end);
195
            return format_hhmm(start) + _(" to ") + format_hhmm(end);
196
        }
196
        }
197
        function delete_slot(node, branchcode){
197
        function delete_slot(node, branchcode){
198
            let slot = $(node).find('.pickup-slot').val();
198
            let slot = $(node).find('input').val();
199
            let splitted  = slot.split("-");
199
            opening_slots[branchcode] = $.grep(opening_slots[branchcode], function(elt, index) {
200
            let day = splitted[0];
200
                return elt !== slot;
201
            let start = splitted[1];
201
            });
202
            let end = splitted[2];
203
            opening_slots[branchcode].splice($.inArray(start+'-'+end, opening_slots), 1);
204
            refresh_pickup_hours(branchcode);
202
            refresh_pickup_hours(branchcode);
205
        }
203
        }
206
        function refresh_pickup_hours(branchcode) {
204
        function refresh_pickup_hours(branchcode) {
Lines 218-227 Link Here
218
                let li_node = $('<li><label>'+get_day_lib(day)+'<label></li>');
216
                let li_node = $('<li><label>'+get_day_lib(day)+'<label></li>');
219
                slots_per_day[day].forEach(function(slot) {
217
                slots_per_day[day].forEach(function(slot) {
220
                    let span_node = $('<span class="pickup-slot"></span>');
218
                    let span_node = $('<span class="pickup-slot"></span>');
221
                    span_node.append('<input type="hidden" class="pickup-slot" name="pickup-slot-'+branchcode+'" value="'+slot+'"/>');
219
                    span_node.append('<input type="hidden" name="pickup-slot-'+branchcode+'" value="'+slot+'"/>');
222
                    span_node.append('<span>'+format_slot(slot)+'</span>');
220
                    span_node.append('<span>'+format_slot(slot)+'</span>');
223
221
224
                    let delete_link = $('<a href="#" on><i class="fa fa-trash" aria-hidden="true"></i>').on('click', function(e){ e.preventDefault; delete_slot($(this).closest('li'), branchcode); });
222
                    let delete_link = $('<a href="#" on><i class="fa fa-trash" aria-hidden="true"></i>').on('click', function(e){ e.preventDefault(); delete_slot($(this).closest('li'), branchcode); });
225
                    span_node.append(delete_link);
223
                    span_node.append(delete_link);
226
224
227
                    span_node.appendTo(li_node);
225
                    span_node.appendTo(li_node);
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/circulation-home.tt (+5 lines)
Lines 83-88 Link Here
83
                    <li>
83
                    <li>
84
                        <a class="circ-button" href="/cgi-bin/koha/circ/waitingreserves.pl"><i class="fa fa-calendar"></i> Holds awaiting pickup</a>
84
                        <a class="circ-button" href="/cgi-bin/koha/circ/waitingreserves.pl"><i class="fa fa-calendar"></i> Holds awaiting pickup</a>
85
                    </li>
85
                    </li>
86
                    [% IF ( Koha.Preference('CurbsidePickup') && CAN_user_parameters_manage_curbside_pickups ) %]
87
                    <li>
88
                        <a class="circ-button" href="/cgi-bin/koha/circ/curbside_pickups.pl"><i class="fa fa-refresh"></i> Curbside Pickups</a>
89
                    </li>
90
                    [% END %]
86
                    <li>
91
                    <li>
87
                        <a class="circ-button" href="/cgi-bin/koha/circ/reserveratios.pl"><i class="fa fa-line-chart"></i> Hold ratios</a>
92
                        <a class="circ-button" href="/cgi-bin/koha/circ/reserveratios.pl"><i class="fa fa-line-chart"></i> Hold ratios</a>
88
                    </li>
93
                    </li>
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/curbside_pickups.tt (-1 / +623 lines)
Line 0 Link Here
0
- 
1
[% USE KohaDates %]
2
[% USE ItemTypes %]
3
[% USE Branches %]
4
[% USE AuthorisedValues %]
5
[% USE Asset %]
6
[% USE raw %]
7
[% USE To %]
8
[% SET footerjs = 1 %]
9
[% INCLUDE 'doc-head-open.inc' %]
10
<title>Curbside pickups &rsaquo; Circulation &rsaquo; Koha</title>
11
<style>
12
    .pickup_time input[type='radio'] {
13
        display: none;
14
    }
15
    .pickup_time {
16
        margin: .2em;
17
    }
18
    .pickup_time label {
19
        background-color: #ffffcc;
20
        display: inline-block;
21
        cursor: pointer;
22
    }
23
    .pickup_time input[type='radio']:checked + label {
24
        background-color: #bcdb89;
25
    }
26
    .pickup_time input[type='radio']:disabled+ label {
27
        background-color: #ff9090;
28
    }
29
</style>
30
[% INCLUDE 'doc-head-close.inc' %]
31
</head>
32
33
[% SET today_iso = date.format(date.now, format = '%Y-%m-%d') %]
34
35
<body id="circ_curbside-pickups" class="circ">
36
    [% INCLUDE 'header.inc' %]
37
    [% INCLUDE 'cat-search.inc' %]
38
39
40
<nav id="breadcrumbs" aria-label="Breadcrumb" class="breadcrumb">
41
    <ol>
42
        <li>
43
            <a href="/cgi-bin/koha/mainpage.pl">Home</a>
44
        </li>
45
        <li>
46
            <a href="/cgi-bin/koha/circ/circulation-home.pl">Circulation</a>
47
        </li>
48
        <li>
49
            <a href="#" aria-current="page">Curbside pickups</a>
50
        </li>
51
    </ol>
52
</nav>
53
54
<div class="main container-fluid">
55
    <div class="row">
56
        <div class="col-md-10 col-md-offset-1 col-lg-10 col-lg-offset-1">
57
            <main>
58
59
                <h1>Curbside pickups</h1>
60
61
                [% UNLESS policy.enabled %]
62
                    <div class="dialog alert">
63
                        Curbside pickups are not enabled for your location.
64
                    </div>
65
                    [% INCLUDE 'intranet-bottom.inc' %]
66
                    [% STOP %]
67
                [% END %]
68
69
                [% FOR m IN messages %]
70
                    <div class="dialog [% m.type | html %]">
71
                        [% SWITCH m.code %]
72
                        [% CASE 'too_many_pickups' %]
73
                            <span>This patron already has a scheduled pickup for this library.</span>
74
                        [% CASE 'cannot_checkout' %]
75
                            <span>Unable to check the items out to [% INCLUDE 'patron-title.inc' patron=m.patron %]</span>
76
                        [% CASE 'no_patron_found' %]
77
                            <span>No patron found with cardnumber [% m.cardnumber | html %].</span>
78
                        [% CASE %]
79
                            <span>[% m.code | html %]</span>
80
                        [% END %]
81
                    </div>
82
                [% END %]
83
84
                [% SET to_be_staged = curbside_pickups.filter_by_to_be_staged %]
85
                [% SET staged_and_ready = curbside_pickups.filter_by_staged_and_ready %]
86
                [% SET patron_outside = curbside_pickups.filter_by_patron_outside %]
87
                [% SET delivered_today = curbside_pickups.filter_by_delivered %]
88
                <div id="pickup-tabs" class="toptabs">
89
                    <ul class="nav nav-tabs" role="tablist">
90
                        [% IF !tab OR tab == 'to-be-staged' %]
91
                        <li role="presentation" class="active">
92
                        [% ELSE %]
93
                        <li role="presentation">
94
                        [% END %]
95
                            <a id="to-be-staged-tab"      href="#to-be-staged" role="tab" data-toggle="tab">To be staged ([% to_be_staged.count | html %])</a>
96
                        </li>
97
                        [% IF tab == 'staged-and-ready' %]
98
                        <li role="presentation" class="active">
99
                        [% ELSE %]
100
                        <li role="presentation">
101
                        [% END %]
102
                            <a id="staged-and-ready-tab"  href="#staged-and-ready" role="tab" data-toggle="tab">Staged & ready ([% staged_and_ready.count | html %])</a>
103
                        </li>
104
                        [% IF tab == 'patron-is-outside' %]
105
                        <li role="presentation" class="active">
106
                        [% ELSE %]
107
                        <li role="presentation">
108
                        [% END %]
109
                            <a id="patron-is-outside-tab" href="#patron-is-outside" role="tab" data-toggle="tab">Patron is outside ([% patron_outside.count | html %])</a>
110
                        </li>
111
                        [% IF tab == 'delivered-today' %]
112
                        <li role="presentation" class="active">
113
                        [% ELSE %]
114
                        <li role="presentation">
115
                        [% END %]
116
                            <a id="delivered-today-tab"   href="#delivered-today" role="tab" data-toggle="tab">Delivered today ([% delivered_today.count | html %])</a>
117
                        </li>
118
                        [% IF tab == 'schedule-pickup' %]
119
                        <li role="presentation" class="active">
120
                        [% ELSE %]
121
                        <li role="presentation">
122
                        [% END %]
123
                            <a id="schedule-pickup-tab"   href="#schedule-pickup" role="tab" data-toggle="tab">Schedule pickup</a>
124
                        </li>
125
                    </ul>
126
127
                    <div class="tab-content">
128
                        [% IF !tab OR tab == 'to-be-staged' %]
129
                        <div id="to-be-staged" role="tabpanel" class="tab-pane active">
130
                        [% ELSE %]
131
                        <div id="to-be-staged" role="tabpanel" class="tab-pane">
132
                        [% END %]
133
                            <form method="post" class="form">
134
                                <p>
135
                                    <button type="submit" class="btn btn-default"><i class="fa fa-refresh" aria-hidden="true"></i> Refresh</button>
136
                                </p>
137
                            </form>
138
139
                            [% IF to_be_staged.count %]
140
                                <table class="table table-striped">
141
                                    <thead>
142
                                        <tr>
143
                                            <th>Pickup Date/Time</th>
144
                                            <th>Patron</th>
145
                                            <th>Items for pickup</th>
146
                                            <th>Action</th>
147
                                        </tr>
148
                                    </thead>
149
                                    <tbody>
150
                                        [% FOREACH cp IN to_be_staged %]
151
                                            [% UNLESS cp.staged_datetime %]
152
                                                <tr class="[% class | html %]">
153
                                                    <td>[% cp.scheduled_pickup_datetime | $KohaDates with_hours = 1 %]</td>
154
                                                    <td>
155
                                                        <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>
156
                                                        [% IF cp.notes %]
157
                                                            <br/>
158
                                                            Notes: [% cp.notes | html %]
159
                                                        [% END %]
160
                                                    </td>
161
                                                    <td>
162
                                                        [% SET waiting_holds = cp.patron.holds.search( found => 'W', branchcode => logged_in_user.branchcode ) %]
163
                                                        [% FOREACH h IN waiting_holds %]
164
                                                           <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/>
165
                                                        [% END %]
166
                                                    </td>
167
                                                    <td>
168
                                                        <form method="post" class="form">
169
                                                            <input type="hidden" name="op" value="mark-as-staged"/>
170
                                                            <input type="hidden" name="tab" value="to-be-staged"/>
171
                                                            <input type="hidden" name="id" value="[% cp.id | html %]"/>
172
                                                            <p>
173
                                                                <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>
174
                                                            </p>
175
                                                        </form>
176
177
                                                        <form method="post" class="form">
178
                                                            <input type="hidden" name="op" value="cancel"/>
179
                                                            <input type="hidden" name="tab" value="to-be-staged"/>
180
                                                            <input type="hidden" name="id" value="[% cp.id | html %]"/>
181
                                                            <p>
182
                                                                <button type="submit" class="btn btn-default cancel-btn"><i class="fa fa-ban" aria-hidden="true"></i> Cancel</button>
183
                                                            </p>
184
                                                        </form>
185
                                                    </td>
186
                                                </tr>
187
                                            [% END %]
188
                                        [% END %]
189
                                    </tbody>
190
                                </table>
191
                            [% ELSE %]
192
                                <span>There are no pickups to be staged.</span>
193
                            [% END %]
194
                        </div>
195
196
                        [% IF tab == "staged-and-ready" %]
197
                        <div id="staged-and-ready" role="tabpanel" class="tab-pane active">
198
                        [% ELSE %]
199
                        <div id="staged-and-ready" role="tabpanel" class="tab-pane">
200
                        [% END %]
201
                            <form method="post" class="form">
202
                                <input type="hidden" name="tab" value="staged-and-ready"/>
203
                                <p>
204
                                    <button type="submit" class="btn btn-default"><i class="fa fa-refresh" aria-hidden="true"></i> Refresh</button>
205
                                </p>
206
                            </form>
207
208
                            [% IF staged_and_ready.count %]
209
                                <table class="table table-striped">
210
                                    <thead>
211
                                        <tr>
212
                                            <th>Pickup Date/Time</th>
213
                                            <th>Patron</th>
214
                                            <th>Items for pickup</th>
215
                                            <th>Staged by</th>
216
                                            <th>Action</th>
217
                                        </tr>
218
                                    </thead>
219
                                    <tbody>
220
                                        [% FOREACH cp IN staged_and_ready %]
221
                                            [% IF cp.staged_datetime && !cp.arrival_datetime %]
222
                                                <tr class="[% class | html %]">
223
                                                    <td>[% cp.scheduled_pickup_datetime | $KohaDates with_hours = 1 %]</td>
224
                                                    <td>
225
                                                        <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>
226
                                                        [% IF cp.notes %]
227
                                                            <br/>
228
                                                            Notes: [% cp.notes | html %]
229
                                                        [% END %]
230
                                                    </td>
231
                                                    <td>
232
                                                        [% SET waiting_holds = cp.patron.holds.search( found => 'W', branchcode => logged_in_user.branchcode ) %]
233
                                                        [% FOREACH h IN waiting_holds %]
234
                                                           <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/>
235
                                                        [% END %]
236
                                                    </td>
237
                                                    <td>
238
                                                        [% cp.staged_by_staff.firstname | html %] [% cp.staged_by_staff.surname | html %]
239
                                                    </td>
240
                                                    <td>
241
                                                        <form method="post" class="form">
242
                                                            <input type="hidden" name="op" value="mark-patron-has-arrived"/>
243
                                                            <input type="hidden" name="tab" value="staged-and-ready"/>
244
                                                            <input type="hidden" name="id" value="[% cp.id | html %]"/>
245
                                                            <p>
246
                                                                <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>
247
                                                            </p>
248
                                                        </form>
249
250
                                                        <form method="post" class="form">
251
                                                            <input type="hidden" name="op" value="mark-as-delivered"/>
252
                                                            <input type="hidden" name="tab" value="staged-and-ready"/>
253
                                                            <input type="hidden" name="id" value="[% cp.id | html %]"/>
254
                                                            <p>
255
                                                                <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>
256
                                                            </p>
257
                                                        </form>
258
259
                                                        <form method="post" class="form">
260
                                                            <input type="hidden" name="op" value="mark-as-unstaged"/>
261
                                                            <input type="hidden" name="tab" value="staged-and-ready"/>
262
                                                            <input type="hidden" name="id" value="[% cp.id | html %]"/>
263
                                                            <p>
264
                                                                <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>
265
                                                            </p>
266
                                                        </form>
267
                                                    </td>
268
                                                </tr>
269
                                            [% END %]
270
                                        [% END %]
271
                                    </tbody>
272
                                </table>
273
                            [% ELSE %]
274
                                <span>There are no pickups staged and ready.</span>
275
                            [% END %]
276
                        </div>
277
278
                        [% IF tab == "patron-is-outside" %]
279
                        <div id="patron-is-outside" role="tabpanel" class="tab-pane active">
280
                        [% ELSE %]
281
                        <div id="patron-is-outside" role="tabpanel" class="tab-pane">
282
                        [% END %]
283
                            <form method="post" class="form">
284
                                <input type="hidden" name="tab" value="patron-is-outside"/>
285
                                <p>
286
                                    <button type="submit" class="btn btn-default"><i class="fa fa-refresh" aria-hidden="true"></i> Refresh</button>
287
                                </p>
288
                            </form>
289
290
                            [% IF patron_outside.count %]
291
                                <table class="table table-striped">
292
                                    <thead>
293
                                        <tr>
294
                                            <th>Pickup Date/Time</th>
295
                                            <th>Patron</th>
296
                                            <th>Items for pickup</th>
297
                                            <th>Staged by</th>
298
                                            <th>Action</th>
299
                                        </tr>
300
                                    </thead>
301
                                    <tbody>
302
                                        [% FOREACH cp IN patron_outside %]
303
                                            [% IF cp.arrival_datetime && !cp.delivered_datetime %]
304
                                                <tr class="[% class | html %]">
305
                                                    <td>[% cp.scheduled_pickup_datetime | $KohaDates with_hours = 1 %]</td>
306
                                                    <td>
307
                                                        <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>
308
                                                        [% IF cp.notes %]
309
                                                            <br/>
310
                                                            Notes: [% cp.notes | html %]
311
                                                        [% END %]
312
                                                    </td>
313
                                                    <td>
314
                                                        [% SET waiting_holds = cp.patron.holds.search( found => 'W', branchcode => logged_in_user.branchcode ) %]
315
                                                        [% FOREACH h IN waiting_holds %]
316
                                                           <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/>
317
                                                        [% END %]
318
                                                    </td>
319
                                                    <td>
320
                                                        [% cp.staged_by_staff.firstname | html %] [% cp.staged_by_staff.surname | html %]
321
                                                    </td>
322
                                                    <td>
323
                                                        <form method="post" class="form">
324
                                                            <input type="hidden" name="op" value="mark-as-delivered"/>
325
                                                            <input type="hidden" name="tab" value="patron-is-outside"/>
326
                                                            <input type="hidden" name="id" value="[% cp.id | html %]"/>
327
                                                            <p>
328
                                                                <button type="submit" class="btn btn-default mark-as-delivered-btn"><i class="fa fa-envelope" aria-hidden="true"></i> Mark as delivered</button>
329
                                                            </p>
330
                                                        </form>
331
332
                                                        <form method="post" class="form">
333
                                                            <input type="hidden" name="op" value="mark-as-staged"/>
334
                                                            <input type="hidden" name="tab" value="patron-is-outside"/>
335
                                                            <input type="hidden" name="id" value="[% cp.id | html %]"/>
336
                                                            <p>
337
                                                                <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>
338
                                                            </p>
339
                                                        </form>
340
341
                                                        <form method="post" class="form">
342
                                                            <input type="hidden" name="op" value="mark-as-unstaged"/>
343
                                                            <input type="hidden" name="tab" value="patron-is-outside"/>
344
                                                            <input type="hidden" name="id" value="[% cp.id | html %]"/>
345
                                                            <p>
346
                                                                <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>
347
                                                            </p>
348
                                                        </form>
349
                                                    </td>
350
                                                </tr>
351
                                            [% END %]
352
                                        [% END %]
353
                                    </tbody>
354
                                </table>
355
                            [% ELSE %]
356
                                <span>There are no patrons waiting outside.</span>
357
                            [% END %]
358
                        </div>
359
360
                        [% IF tab == "delivered-today" %]
361
                        <div id="delivered-today" role="tabpanel" class="tab-pane active">
362
                        [% ELSE %]
363
                        <div id="delivered-today" role="tabpanel" class="tab-pane">
364
                        [% END %]
365
                            <form method="post" class="form">
366
                                <input type="hidden" name="tab" value="delivered-today"/>
367
                                <p>
368
                                    <button type="submit" class="btn btn-default"><i class="fa fa-refresh" aria-hidden="true"></i> Refresh</button>
369
                                </p>
370
                            </form>
371
372
                            [% IF delivered_today.count %]
373
                                <table class="table table-striped">
374
                                    <thead>
375
                                        <tr>
376
                                            <th>Deliver Date/Time</th>
377
                                            <th>Patron</th>
378
                                            <th>Items checked out</th>
379
                                        </tr>
380
                                    </thead>
381
                                    <tbody>
382
                                        [% FOREACH cp IN delivered_today %]
383
                                            [% IF cp.delivered_datetime %]
384
                                                <tr class="[% class | html %]">
385
                                                    <td>[% cp.delivered_datetime | $KohaDates with_hours = 1 %]</td>
386
                                                    <td>
387
                                                        <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>
388
                                                        [% IF cp.notes %]
389
                                                            <br/>
390
                                                            Notes: [% cp.notes | html %]
391
                                                        [% END %]
392
                                                    </td>
393
                                                    <td>
394
                                                    [% FOREACH c IN cp.checkouts %]
395
                                                        [% IF date.format(c.issuedate, format = '%Y-%m-%d') == today_iso %]
396
                                                            <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/>
397
                                                        [% END %]
398
                                                    [% END %]
399
                                                </td>
400
                                            </tr>
401
                                        [% END %]
402
                                    [% END %]
403
                                </tbody>
404
                            </table>
405
                        [% ELSE %]
406
                            <span>No pickups have been delivered today.</span>
407
                        [% END %]
408
                    </div>
409
410
                    [% IF tab == "schedule-pickup" %]
411
                    <div id="schedule-pickup" role="tabpanel" class="tab-pane active">
412
                    [% ELSE %]
413
                    <div id="schedule-pickup" role="tabpanel" class="tab-pane">
414
                    [% END %]
415
                        [% IF !patron || ( patron && existing_curbside_pickups.count >= 1 ) %]
416
                            [% IF existing_curbside_pickups.count >= 1 %]
417
                                <div class="dialog alert">
418
                                    [% patron.firstname | html %] [% patron.surname | html %] ([% patron.cardnumber | html %]) already has a scheduled pickup for this library.
419
                                </div>
420
                            [% END %]
421
                            <form method="post" class="form-inline">
422
                                <input type="hidden" name="op" value="find-patron"/>
423
                                <input type="hidden" name="tab" value="schedule-pickup"/>
424
425
                                <div class="form-group">
426
                                    <label class="sr-only" for="input-patron-cardnumber">Cardnumber</label>
427
                                    <div class="input-group">
428
                                        <div class="input-group-addon">Card number</div>
429
                                        <input type="text" class="form-control" name="cardnumber" id="input-patron-cardnumber" placeholder="Enter patron cardnumber"/>
430
                                    </div>
431
                                </div>
432
433
                                <button type="submit" class="btn btn-default">Submit</button>
434
                            </form>
435
                        [% ELSE %]
436
                            <form id="create-pickup" method="post" class="form-horizontal">
437
                                <fieldset class="rows" style="float: none;">
438
                                    <input type="hidden" name="borrowernumber" value="[% patron.id | html %]"/>
439
                                    <input type="hidden" name="op" value="create-pickup"/>
440
                                    <input type="hidden" name="tab" value="schedule-pickup"/>
441
                                    <ol>
442
                                        <li>
443
                                            <label>Patron: </label>
444
                                            <span>[% INCLUDE 'patron-title.inc' patron=patron %]</span>
445
                                        </li>
446
447
                                        <li>
448
                                            <label>Items ready for pickup: </label>
449
                                            <span>
450
                                                [% SET waiting_holds = patron.holds.search( found => 'W', branchcode => logged_in_user.branchcode ) %]
451
                                                [% IF waiting_holds.count %]
452
                                                    [% FOREACH h IN waiting_holds %]
453
                                                            <p>
454
                                                                <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>)
455
                                                            </p>
456
                                                        [% END %]
457
                                                    [% ELSE %]
458
                                                        <span>There is no waiting holds for this patron at this location.</span>
459
                                                    [% END %]
460
                                                </span>
461
                                            </li>
462
463
                                            <li>
464
                                                <label for="pickup_date">Pickup date: </label>
465
                                                <input id="pickup_date" name="pickup_date" required="required" class="flatpickr" />
466
                                            </li>
467
468
                                            <li id="pickup-times" class="radio"></li>
469
470
                                            <li>
471
                                                <label for="notes">Notes: </label>
472
                                                <input id="notes" name="notes" type="text" />
473
                                            </li>
474
                                        </ol>
475
                                    </fieldset>
476
477
                                    <fieldset class="action">
478
                                        <input id="schedule-pickup-button" type="submit" value="Submit" />
479
                                    </fieldset>
480
481
                                </form>
482
                            [% END %]
483
                        </div>
484
                    </div>
485
                </div>
486
            </main>
487
        </div>
488
     </div> <!-- /.row -->
489
490
[% MACRO jsinclude BLOCK %]
491
    [% Asset.js("lib/dayjs/dayjs.min.js") | $raw %]
492
    [% Asset.js("lib/dayjs/plugin/isSameOrAfter.js") | $raw %]
493
    <script>dayjs.extend(window.dayjs_plugin_isSameOrAfter)</script>
494
    [% INCLUDE 'calendar.inc' %]
495
    <script>
496
        let pickups = [% To.json(curbside_pickups.unblessed) | $raw %];
497
        let policy = [% To.json(policy.unblessed) | $raw %];
498
499
        let existingPickupMoments = [];
500
        pickups.forEach(function(pickup){
501
            let scheduled_pickup_datetime = pickup.scheduled_pickup_datetime;
502
            let pickupMoment = dayjs(scheduled_pickup_datetime);
503
504
            existingPickupMoments.push(pickupMoment);
505
        });
506
507
        let opening_slots = [% To.json(policy.opening_slots.unblessed) | $raw %];
508
        let slots_per_day = {};
509
        opening_slots.forEach(function(slot){
510
            let day = slot.day;
511
            if(!slots_per_day[day]) slots_per_day[day] = [];
512
            slots_per_day[day].push(slot);
513
        });
514
515
516
        $(document).ready(function() {
517
518
            $('#schedule-pickup-tab').on('click', function() {
519
                $('#input-patron-cardnumber').focus();
520
            });
521
522
            $("#pickup_date").on('change', function() {
523
524
                $('#pickup-times').empty();
525
                $('#schedule-pickup-button').prop( 'disabled', 1 );
526
527
                var currentDate = $(this).val();
528
529
                let selectedDate = dayjs(currentDate);
530
531
                let pickupSlots = [];
532
                let available_count = 0;
533
                let dow = selectedDate.day(); // Sunday is 0 (at least for now)
534
                if (!slots_per_day[dow]){
535
                    $('#pickup-times').html("<div>"+_("No pickup time define for this day.")+"</div>");
536
                    return;
537
                }
538
539
                slots_per_day[dow].forEach(function(slot){
540
                    let pickup_interval = policy.pickup_interval;
541
542
                    let listStartMoment = selectedDate.hour(slot.start_hour).minute(slot.start_minute);
543
                    let listEndMoment = selectedDate.hour(slot.end_hour).minute(slot.end_minute);
544
545
                    let keep_going = true;
546
                    let now = dayjs();
547
548
                    // Initialize pickup slots starting at opening time
549
                    let pickupIntervalStartMoment = listStartMoment;
550
                    let pickupIntervalEndMoment   = listStartMoment.add(pickup_interval, 'minutes');
551
                    while (keep_going) {
552
                        let available = true;
553
554
                        if (pickupIntervalStartMoment.isBefore(now)) {
555
                            // Slots in the past are unavailable
556
                            available = false;
557
                        }
558
559
                        if (pickupIntervalEndMoment.isAfter(listEndMoment)) {
560
                            // Slots after the end of pickup times for the day are unavailable
561
                            available = false;
562
                        }
563
564
                        let pickups_scheduled = 0;
565
                        existingPickupMoments.forEach(function(pickupMoment){
566
                            // An existing pickup time
567
                            if (pickupMoment.isSameOrAfter(pickupIntervalStartMoment) && pickupMoment.isBefore(pickupIntervalEndMoment)) {
568
                                // This calculated pickup is in use by another scheduled pickup
569
                                pickups_scheduled++;
570
                            }
571
                        });
572
573
                        if (pickups_scheduled >= policy.patrons_per_interval) {
574
                            available = false;
575
                        }
576
577
                        pickupSlots.push(
578
                            {
579
                                "available": available,
580
                                "moment": pickupIntervalStartMoment,
581
                                "pickups_scheduled": pickups_scheduled
582
                            }
583
                        );
584
585
                        if ( available ) {
586
                            available_count++;
587
                        }
588
589
                        pickupIntervalStartMoment = pickupIntervalEndMoment;
590
                        pickupIntervalEndMoment = pickupIntervalStartMoment.add(pickup_interval, 'minutes');
591
                        if (pickupIntervalEndMoment.isAfter(listEndMoment)) {
592
                            // This latest slot is after the end of pickup times for the day, so we can stop
593
                            keep_going = false;
594
                        }
595
                    }
596
                });
597
598
                for (let i = 0; i < pickupSlots.length; i++) {
599
                    let pickupSlot = pickupSlots[i];
600
                    let optText = pickupSlot.moment.format("HH:mm");
601
                    let optValue = pickupSlot.moment.format("YYYY-MM-DD HH:mm:ss");
602
                    let pickups_scheduled = pickupSlot.pickups_scheduled;
603
                    let disabled = pickupSlot.available ? "" : "disabled";
604
                    $("#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>`);
605
                }
606
607
                $('#pickup-times').show();
608
                $('#schedule-pickup-button').prop( 'disabled', available_count <= 0 );
609
            });
610
611
            $("#create-pickup").on('submit', function(){
612
                if ( ! $("input[type='radio']:checked").length ) {
613
                    alert(_("Please select a date and a pickup time"))
614
                    return false;
615
                }
616
                return true;
617
            });
618
        });
619
    </script>
620
[% END %]
621
622
623
[% INCLUDE 'intranet-bottom.inc' %]

Return to bug 30650