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

(-)a/circ/curbside_pickups.pl (-3 / +17 lines)
Lines 28-34 use Koha::CurbsidePickupPolicies; Link Here
28
use Koha::Libraries;
28
use Koha::Libraries;
29
use Koha::Patrons;
29
use Koha::Patrons;
30
30
31
my $input         = CGI->new;
31
my $input = CGI->new;
32
33
unless ( C4::Context->preference('CurbsidePickup') ) {
34
    print $input->redirect("/cgi-bin/koha/errors/404.pl");
35
    exit;
36
}
37
32
my $op            = $input->param('op') || 'list';
38
my $op            = $input->param('op') || 'list';
33
my $tab           = $input->param('tab');
39
my $tab           = $input->param('tab');
34
my $auto_refresh  = $input->param('auto_refresh');
40
my $auto_refresh  = $input->param('auto_refresh');
Lines 45-51 my ( $template, $loggedinuser, $cookie ) = get_template_and_user( Link Here
45
);
51
);
46
52
47
my $branchcode = C4::Context->userenv()->{'branch'};
53
my $branchcode = C4::Context->userenv()->{'branch'};
48
my $libraries  = Koha::Libraries->search( {}, { order_by => ['branchname'] } );
54
55
my $policy = Koha::CurbsidePickupPolicies->find( { branchcode => $branchcode } );
56
unless ( $policy && $policy->enabled ) {
57
    $template->param( disabled_for_branch => 1 );
58
    output_html_with_http_headers $input, $cookie, $template->output;
59
    exit;
60
}
61
62
my $libraries = Koha::Libraries->search( {}, { order_by => ['branchname'] } );
49
if ( $op eq 'find-patron' ) {
63
if ( $op eq 'find-patron' ) {
50
    my $borrowernumber = $input->param('borrowernumber');
64
    my $borrowernumber = $input->param('borrowernumber');
51
65
Lines 150-156 $template->param( Link Here
150
    tab              => $tab,
164
    tab              => $tab,
151
    auto_refresh     => $auto_refresh,
165
    auto_refresh     => $auto_refresh,
152
    refresh_delay    => $refresh_delay,
166
    refresh_delay    => $refresh_delay,
153
    policy           => Koha::CurbsidePickupPolicies->find( { branchcode => $branchcode } ),
167
    policy           => $policy,
154
    curbside_pickups => Koha::CurbsidePickups->search(
168
    curbside_pickups => Koha::CurbsidePickups->search(
155
        {
169
        {
156
            branchcode => $branchcode,
170
            branchcode => $branchcode,
(-)a/koha-tmpl/intranet-tmpl/prog/en/modules/circ/curbside_pickups.tt (-195 / +201 lines)
Lines 84-99 Link Here
84
    [% END %]
84
    [% END %]
85
[% END %]
85
[% END %]
86
86
87
[% SET aside = Koha.Preference('CircSidebar') ? 'circ-nav' : '' %]
87
[% BLOCK curbside_pickups_enabled %]
88
[% WRAPPER 'main-container.inc' aside=aside %]
89
    <h1>Curbside pickups</h1>
88
    <h1>Curbside pickups</h1>
90
89
91
    [% UNLESS policy.enabled %]
92
        <div class="alert alert-warning"> Curbside pickups are not enabled for your library. </div>
93
        [% INCLUDE 'intranet-bottom.inc' %]
94
        [% STOP %]
95
    [% END %]
96
97
    [% FOR m IN messages %]
90
    [% FOR m IN messages %]
98
        <div class="alert alert-[% m.type | html %]">
91
        <div class="alert alert-[% m.type | html %]">
99
            [% SWITCH m.code %]
92
            [% SWITCH m.code %]
Lines 457-695 Link Here
457
            [% END # /tab_panel %]
450
            [% END # /tab_panel %]
458
        [% END # /WRAPPER tab_panels %]
451
        [% END # /WRAPPER tab_panels %]
459
    [% END # /WRAPPER tabs %]
452
    [% END # /WRAPPER tabs %]
460
[% END %]
461
453
462
[% MACRO jsinclude BLOCK %]
454
    [% MACRO jsinclude BLOCK %]
463
    [% Asset.js("lib/dayjs/plugin/isSameOrAfter.js") | $raw %]
455
        [% Asset.js("lib/dayjs/plugin/isSameOrAfter.js") | $raw %]
464
    <script>
456
        <script>
465
        dayjs.extend(window.dayjs_plugin_isSameOrAfter);
457
            dayjs.extend(window.dayjs_plugin_isSameOrAfter);
466
    </script>
458
        </script>
467
    <script>
459
        <script>
468
        dayjs.extend(window.dayjs_plugin_customParseFormat);
460
            dayjs.extend(window.dayjs_plugin_customParseFormat);
469
    </script>
461
        </script>
470
    [% INCLUDE 'calendar.inc' %]
462
        [% INCLUDE 'calendar.inc' %]
471
    [% INCLUDE 'js-patron-format.inc' %]
463
        [% INCLUDE 'js-patron-format.inc' %]
472
    [% INCLUDE 'datatables.inc' %]
464
        [% INCLUDE 'datatables.inc' %]
473
    <script>
465
        <script>
474
        let pickups = [% To.json(curbside_pickups.unblessed) | $raw %];
466
            let pickups = [% To.json(curbside_pickups.unblessed) | $raw %];
475
        let policy = [% To.json(policy.unblessed) | $raw %];
467
            let policy = [% To.json(policy.unblessed) | $raw %];
476
468
477
        let existingPickupMoments = [];
469
            let existingPickupMoments = [];
478
        pickups.forEach(function(pickup){
470
            pickups.forEach(function(pickup){
479
            let scheduled_pickup_datetime = pickup.scheduled_pickup_datetime;
471
                let scheduled_pickup_datetime = pickup.scheduled_pickup_datetime;
480
            let pickupMoment = dayjs(scheduled_pickup_datetime);
472
                let pickupMoment = dayjs(scheduled_pickup_datetime);
481
473
482
            existingPickupMoments.push(pickupMoment);
474
                existingPickupMoments.push(pickupMoment);
483
        });
484
485
        let opening_slots = [% To.json(policy.opening_slots.unblessed) | $raw %];
486
        let slots_per_day = {};
487
        opening_slots.forEach(function(slot){
488
            let day = slot.day;
489
            if(!slots_per_day[day]) slots_per_day[day] = [];
490
            slots_per_day[day].push(slot);
491
        });
492
493
        $(document).ready(function() {
494
495
            $('#schedule-pickup-tab').on('click', function() {
496
                $('#find-patron').focus();
497
            });
475
            });
498
476
499
            [% IF tab == 'schedule-pickup_panel' %]
477
            let opening_slots = [% To.json(policy.opening_slots.unblessed) | $raw %];
500
                $('#find-patron').focus();
478
            let slots_per_day = {};
501
            [% END %]
479
            opening_slots.forEach(function(slot){
480
                let day = slot.day;
481
                if(!slots_per_day[day]) slots_per_day[day] = [];
482
                slots_per_day[day].push(slot);
483
            });
502
484
503
            const pickup_date = document.querySelector("#pickup_date");
485
            $(document).ready(function() {
504
            if ( pickup_date ) {
505
                const fp = pickup_date._flatpickr;
506
                fp.set('disable', [function(date) {
507
                    return !slots_per_day.hasOwnProperty(date.getDay());
508
                }]);
509
            }
510
486
511
            $("#pickup_date").on('change', function() {
487
                $('#schedule-pickup-tab').on('click', function() {
488
                    $('#find-patron').focus();
489
                });
512
490
513
                $('#pickup-times').empty();
491
                [% IF tab == 'schedule-pickup_panel' %]
514
                $('#schedule-pickup-button').prop( 'disabled', 1 );
492
                    $('#find-patron').focus();
493
                [% END %]
515
494
516
                var currentDate = $(this).val();
495
                const pickup_date = document.querySelector("#pickup_date");
496
                if ( pickup_date ) {
497
                    const fp = pickup_date._flatpickr;
498
                    fp.set('disable', [function(date) {
499
                        return !slots_per_day.hasOwnProperty(date.getDay());
500
                    }]);
501
                }
517
502
518
                let selectedDate = dayjs(currentDate);
503
                $("#pickup_date").on('change', function() {
519
504
520
                let pickupSlots = [];
505
                    $('#pickup-times').empty();
521
                let available_count = 0;
506
                    $('#schedule-pickup-button').prop( 'disabled', 1 );
522
                let dow = selectedDate.day(); // Sunday is 0 (at least for now)
523
                if (!slots_per_day[dow]){
524
                    $('#pickup-times').html("<div>"+_("No pickup time defined for this day.")+"</div>");
525
                    return;
526
                }
527
507
528
                slots_per_day[dow].sort((a, b) => a.start_hour - b.start_hour).forEach(function(slot){
508
                    var currentDate = $(this).val();
529
                    let pickup_interval = policy.pickup_interval;
509
530
                    if (!pickup_interval) {
510
                    let selectedDate = dayjs(currentDate);
511
512
                    let pickupSlots = [];
513
                    let available_count = 0;
514
                    let dow = selectedDate.day(); // Sunday is 0 (at least for now)
515
                    if (!slots_per_day[dow]){
531
                        $('#pickup-times').html("<div>"+_("No pickup time defined for this day.")+"</div>");
516
                        $('#pickup-times').html("<div>"+_("No pickup time defined for this day.")+"</div>");
532
                        return;
517
                        return;
533
                    }
518
                    }
534
519
535
                    let listStartMoment = selectedDate.hour(slot.start_hour).minute(slot.start_minute);
520
                    slots_per_day[dow].sort((a, b) => a.start_hour - b.start_hour).forEach(function(slot){
536
                    let listEndMoment = selectedDate.hour(slot.end_hour).minute(slot.end_minute);
521
                        let pickup_interval = policy.pickup_interval;
537
522
                        if (!pickup_interval) {
538
                    let keep_going = true;
523
                            $('#pickup-times').html("<div>"+_("No pickup time defined for this day.")+"</div>");
539
                    let now = dayjs();
524
                            return;
525
                        }
540
526
541
                    // Initialize pickup slots starting at opening time
527
                        let listStartMoment = selectedDate.hour(slot.start_hour).minute(slot.start_minute);
542
                    let pickupIntervalStartMoment = listStartMoment;
528
                        let listEndMoment = selectedDate.hour(slot.end_hour).minute(slot.end_minute);
543
                    let pickupIntervalEndMoment   = listStartMoment.add(pickup_interval, 'minutes');
544
                    while (keep_going) {
545
                        let available = true;
546
                        let display_slot = true
547
529
548
                        if (pickupIntervalStartMoment.isBefore(now)) {
530
                        let keep_going = true;
549
                            // Slots in the past are unavailable
531
                        let now = dayjs();
550
                            available = false;
551
                            display_slot = false;
552
                        }
553
532
554
                        if (pickupIntervalEndMoment.isAfter(listEndMoment)) {
533
                        // Initialize pickup slots starting at opening time
555
                            // Slots after the end of pickup times for the day are unavailable
534
                        let pickupIntervalStartMoment = listStartMoment;
556
                            available = false;
535
                        let pickupIntervalEndMoment   = listStartMoment.add(pickup_interval, 'minutes');
557
                        }
536
                        while (keep_going) {
537
                            let available = true;
538
                            let display_slot = true
558
539
559
                        let pickups_scheduled = 0;
540
                            if (pickupIntervalStartMoment.isBefore(now)) {
560
                        existingPickupMoments.forEach(function(pickupMoment){
541
                                // Slots in the past are unavailable
561
                            // An existing pickup time
542
                                available = false;
562
                            if (pickupMoment.isSameOrAfter(pickupIntervalStartMoment) && pickupMoment.isBefore(pickupIntervalEndMoment)) {
543
                                display_slot = false;
563
                                // This calculated pickup is in use by another scheduled pickup
564
                                pickups_scheduled++;
565
                            }
544
                            }
566
                        });
567
545
568
                        if (pickups_scheduled >= policy.patrons_per_interval) {
546
                            if (pickupIntervalEndMoment.isAfter(listEndMoment)) {
569
                            available = false;
547
                                // Slots after the end of pickup times for the day are unavailable
570
                        }
548
                                available = false;
549
                            }
571
550
572
                        if ( display_slot ) {
551
                            let pickups_scheduled = 0;
573
                            pickupSlots.push(
552
                            existingPickupMoments.forEach(function(pickupMoment){
574
                                {
553
                                // An existing pickup time
575
                                    "available": available,
554
                                if (pickupMoment.isSameOrAfter(pickupIntervalStartMoment) && pickupMoment.isBefore(pickupIntervalEndMoment)) {
576
                                    "moment": pickupIntervalStartMoment,
555
                                    // This calculated pickup is in use by another scheduled pickup
577
                                    "pickups_scheduled": pickups_scheduled
556
                                    pickups_scheduled++;
578
                                }
557
                                }
579
                            );
558
                            });
580
                        }
581
559
582
                        if ( available ) {
560
                            if (pickups_scheduled >= policy.patrons_per_interval) {
583
                            available_count++;
561
                                available = false;
584
                        }
562
                            }
585
563
586
                        pickupIntervalStartMoment = pickupIntervalEndMoment;
564
                            if ( display_slot ) {
587
                        pickupIntervalEndMoment = pickupIntervalStartMoment.add(pickup_interval, 'minutes');
565
                                pickupSlots.push(
588
                        if (pickupIntervalEndMoment.isAfter(listEndMoment)) {
566
                                    {
589
                            // This latest slot is after the end of pickup times for the day, so we can stop
567
                                        "available": available,
590
                            keep_going = false;
568
                                        "moment": pickupIntervalStartMoment,
569
                                        "pickups_scheduled": pickups_scheduled
570
                                    }
571
                                );
572
                            }
573
574
                            if ( available ) {
575
                                available_count++;
576
                            }
577
578
                            pickupIntervalStartMoment = pickupIntervalEndMoment;
579
                            pickupIntervalEndMoment = pickupIntervalStartMoment.add(pickup_interval, 'minutes');
580
                            if (pickupIntervalEndMoment.isAfter(listEndMoment)) {
581
                                // This latest slot is after the end of pickup times for the day, so we can stop
582
                                keep_going = false;
583
                            }
591
                        }
584
                        }
585
                    });
586
587
                    for (let i = 0; i < pickupSlots.length; i++) {
588
                        let pickupSlot = pickupSlots[i];
589
                        let optText = pickupSlot.moment.format("HH:mm");
590
                        let optValue = pickupSlot.moment.format("YYYY-MM-DD HH:mm:ss");
591
                        let pickups_scheduled = pickupSlot.pickups_scheduled;
592
                        let disabled = pickupSlot.available ? "" : "disabled";
593
                        $("#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>`);
592
                    }
594
                    }
593
                });
594
595
595
                for (let i = 0; i < pickupSlots.length; i++) {
596
                    $('#pickup-times').show();
596
                    let pickupSlot = pickupSlots[i];
597
                    $('#schedule-pickup-button').prop( 'disabled', available_count <= 0 );
597
                    let optText = pickupSlot.moment.format("HH:mm");
598
                });
598
                    let optValue = pickupSlot.moment.format("YYYY-MM-DD HH:mm:ss");
599
                    let pickups_scheduled = pickupSlot.pickups_scheduled;
600
                    let disabled = pickupSlot.available ? "" : "disabled";
601
                    $("#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>`);
602
                }
603
599
604
                $('#pickup-times').show();
600
                $("#create-pickup").on('submit', function(){
605
                $('#schedule-pickup-button').prop( 'disabled', available_count <= 0 );
601
                    if ( ! $("input[type='radio']:checked").length ) {
606
            });
602
                        alert(_("Please select a date and a pickup time"))
603
                        return false;
604
                    }
605
                    return true;
606
                });
607
607
608
            $("#create-pickup").on('submit', function(){
608
                if ( $("#find-patron").length ) {
609
                if ( ! $("input[type='radio']:checked").length ) {
609
                    patron_autocomplete($("#find-patron"), { 'on-select-callback': function( event, ui ) {
610
                    alert(_("Please select a date and a pickup time"))
610
                            window.location.href = "/cgi-bin/koha/circ/curbside_pickups.pl?op=find-patron&borrowernumber=" + ui.item.patron_id;
611
                    return false;
611
                            return false;
612
                        }
613
                    });
612
                }
614
                }
613
                return true;
614
            });
615
615
616
            if ( $("#find-patron").length ) {
616
                $("#pickup-tabs a[data-bs-toggle='tab']").on("shown.bs.tab", function (e) {
617
                patron_autocomplete($("#find-patron"), { 'on-select-callback': function( event, ui ) {
617
                    $("#current-tab").val($(this).attr('href').substring(1)); // Remove #
618
                        window.location.href = "/cgi-bin/koha/circ/curbside_pickups.pl?op=find-patron&borrowernumber=" + ui.item.patron_id;
618
                });
619
                        return false;
619
                $("#auto_refresh,#refresh_delay").on("change", function(){
620
                    }
620
                    set_interval_if_needed();
621
                });
621
                });
622
            }
623
622
624
            $("#pickup-tabs a[data-bs-toggle='tab']").on("shown.bs.tab", function (e) {
625
                $("#current-tab").val($(this).attr('href').substring(1)); // Remove #
626
            });
627
            $("#auto_refresh,#refresh_delay").on("change", function(){
628
                set_interval_if_needed();
623
                set_interval_if_needed();
629
            });
630
624
631
            set_interval_if_needed();
632
625
626
                let dt_settings = [];
627
                dt_settings.push( [% TablesSettings.GetTableSettings('circ', 'curbside_pickups', 'to_be_stagedt', 'json') | $raw %] );
628
                dt_settings.push( [% TablesSettings.GetTableSettings('circ', 'curbside_pickups', 'staged_and_readyt', 'json') | $raw %] );
629
                dt_settings.push( [% TablesSettings.GetTableSettings('circ', 'curbside_pickups', 'patron_is_outsidet', 'json') | $raw %] );
630
                dt_settings.push( [% TablesSettings.GetTableSettings('circ', 'curbside_pickups', 'delivered_todayt', 'json') | $raw %] );
631
632
                let dt_tables = new Array("to_be_stagedt", "staged_and_readyt", "patron_is_outsidet", "delivered_todayt");
633
                dt_tables.forEach(function( id, index ){
634
                    $("#" + id).kohaTable({
635
                        "autoWidth": false
636
                    }, dt_settings[ index ] );
637
                });
633
638
634
            let dt_settings = [];
635
            dt_settings.push( [% TablesSettings.GetTableSettings('circ', 'curbside_pickups', 'to_be_stagedt', 'json') | $raw %] );
636
            dt_settings.push( [% TablesSettings.GetTableSettings('circ', 'curbside_pickups', 'staged_and_readyt', 'json') | $raw %] );
637
            dt_settings.push( [% TablesSettings.GetTableSettings('circ', 'curbside_pickups', 'patron_is_outsidet', 'json') | $raw %] );
638
            dt_settings.push( [% TablesSettings.GetTableSettings('circ', 'curbside_pickups', 'delivered_todayt', 'json') | $raw %] );
639
639
640
            let dt_tables = new Array("to_be_stagedt", "staged_and_readyt", "patron_is_outsidet", "delivered_todayt");
641
            dt_tables.forEach(function( id, index ){
642
                $("#" + id).kohaTable({
643
                    "autoWidth": false
644
                }, dt_settings[ index ] );
645
            });
640
            });
646
641
642
            let refresh_interval_id = 0;
643
            let countdown_interval_id = 0;
644
            function set_interval(refresh_delay_ms){
645
                clear_intervals();
646
                let next_refresh = new Date();
647
                next_refresh.setSeconds(next_refresh.getSeconds() + refresh_delay_ms / 1000);
648
649
                countdown_interval_id = setInterval(function() {
650
                    const now = new Date().getTime();
651
                    const seconds = Math.floor(( next_refresh - now + 1 ) / 1000);
652
                    if ( seconds > 0 ) {
653
                        $("#refresh_info").text(_("Refresh in %s seconds").format(seconds));
654
                    } else {
655
                        $("#refresh_info").text(""); // In case something is going wrong
656
                    }
657
                }, 1000);
647
658
648
        });
659
                setInterval(function() {
660
                    $(".refresh-form:visible").submit();
661
                }, refresh_delay_ms);
662
            }
663
            function clear_intervals(){
664
                if (refresh_interval_id) {
665
                    clearInterval(refresh_interval_id);
666
                    refresh_interval_id = 0;
667
                }
668
                if (countdown_interval_id) {
669
                    clearInterval(countdown_interval_id);
670
                    countdown_interval_id = 0;
671
                }
672
            }
649
673
650
        let refresh_interval_id = 0;
674
            function set_interval_if_needed(){
651
        let countdown_interval_id = 0;
675
                const refresh_delay = $("#refresh_delay").val();
652
        function set_interval(refresh_delay_ms){
676
                const auto_refresh = $("#auto_refresh").is(":checked");
653
            clear_intervals();
654
            let next_refresh = new Date();
655
            next_refresh.setSeconds(next_refresh.getSeconds() + refresh_delay_ms / 1000);
656
677
657
            countdown_interval_id = setInterval(function() {
678
                if (auto_refresh && refresh_delay){
658
                const now = new Date().getTime();
679
                    set_interval(refresh_delay * 1000);
659
                const seconds = Math.floor(( next_refresh - now + 1 ) / 1000);
660
                if ( seconds > 0 ) {
661
                    $("#refresh_info").text(_("Refresh in %s seconds").format(seconds));
662
                } else {
680
                } else {
663
                    $("#refresh_info").text(""); // In case something is going wrong
681
                    clear_intervals();
664
                }
682
                }
665
            }, 1000);
666
667
            setInterval(function() {
668
                $(".refresh-form:visible").submit();
669
            }, refresh_delay_ms);
670
        }
671
        function clear_intervals(){
672
            if (refresh_interval_id) {
673
                clearInterval(refresh_interval_id);
674
                refresh_interval_id = 0;
675
            }
683
            }
676
            if (countdown_interval_id) {
684
        </script>
677
                clearInterval(countdown_interval_id);
685
    [% END %]
678
                countdown_interval_id = 0;
686
[% END # /BLOCK enabled %]
679
            }
680
        }
681
687
682
        function set_interval_if_needed(){
688
[% BLOCK curbside_pickups_disabled %]
683
            const refresh_delay = $("#refresh_delay").val();
689
    <h1>Curbside pickups</h1>
684
            const auto_refresh = $("#auto_refresh").is(":checked");
690
    <div class="alert alert-warning">Curbside pickups are not enabled for your library.</div>
691
[% END # /BLOCK disabled %]
685
692
686
            if (auto_refresh && refresh_delay){
693
[% SET aside = Koha.Preference('CircSidebar') ? 'circ-nav' : '' %]
687
                set_interval(refresh_delay * 1000);
694
[% WRAPPER 'main-container.inc' aside=aside %]
688
            } else {
695
    [% IF !disabled_for_branch %]
689
                clear_intervals();
696
        [% PROCESS curbside_pickups_enabled %]
690
            }
697
    [% ELSE %]
691
        }
698
        [% PROCESS curbside_pickups_disabled %]
692
    </script>
699
    [% END %]
693
[% END %]
700
[% END # /WRAPPER %]
694
701
695
[% INCLUDE 'intranet-bottom.inc' %]
702
[% INCLUDE 'intranet-bottom.inc' %]
696
- 

Return to bug 40206