LP#1932203: serialize requests on Edit Due Date in Items Out tab
[evergreen-equinox.git] / Open-ILS / web / js / ui / default / staff / circ / patron / items_out.js
index 3bd2b2a..b2988c5 100644 (file)
@@ -5,10 +5,12 @@
 angular.module('egPatronApp')
 
 .controller('PatronItemsOutCtrl',
-       ['$scope','$q','$routeParams','$timeout','egCore','egUser','patronSvc','$location',
-        'egGridDataProvider','$modal','egCirc','egConfirmDialog','egBilling','$window',
-function($scope,  $q,  $routeParams,  $timeout,  egCore , egUser,  patronSvc , $location, 
-         egGridDataProvider , $modal , egCirc , egConfirmDialog , egBilling , $window) {
+       ['$scope','$q','$routeParams','$timeout','egCore','egUser','patronSvc',
+        '$location','egGridDataProvider','$uibModal','egCirc','egConfirmDialog',
+        'egProgressDialog','egBilling','$window','egBibDisplay',
+function($scope , $q , $routeParams , $timeout , egCore , egUser , patronSvc , 
+         $location , egGridDataProvider , $uibModal , egCirc , egConfirmDialog , 
+         egProgressDialog , egBilling , $window , egBibDisplay) {
 
     // list of noncatatloged circulations. Define before initTab to 
     // avoid any possibility of race condition, since they are loaded
@@ -28,6 +30,12 @@ function($scope,  $q,  $routeParams,  $timeout,  egCore , egUser,  patronSvc , $
 
     // list of alt circs (lost, etc.) and/or check-in with fines circs
     $scope.alt_list = []; 
+    
+    egCore.org.settings([
+        'ui.circ.suppress_checkin_popups' // add other settings as needed
+    ]).then(function(set) {
+        $scope.suppress_popups = set['ui.circ.suppress_checkin_popups'];
+    });
 
     // these are fetched during startup (i.e. .configure())
     // By default, show lost/lo/cr items in the alt list
@@ -55,25 +63,39 @@ function($scope,  $q,  $routeParams,  $timeout,  egCore , egUser,  patronSvc , $
     }
 
     $scope.items_out_display = 'main';
-    $scope.show_main_list = function() {
+    $scope.show_main_list = function(refresh_grid) {
         // don't need a full reset_page() to swap tabs
         $scope.items_out_display = 'main';
         patronSvc.items_out = [];
-        provider.refresh();
+        // only refresh the grid when navigating from a tab that 
+        // shares the same grid.
+        if (refresh_grid) provider.refresh();
     }
 
-    $scope.show_alt_list = function() {
+    $scope.show_alt_list = function(refresh_grid) {
         // don't need a full reset_page() to swap tabs
         $scope.items_out_display = 'alt';
         patronSvc.items_out = [];
-        provider.refresh();
+        // only refresh the grid when navigating from a tab that 
+        // shares the same grid.
+        if (refresh_grid) provider.refresh();
     }
 
     $scope.show_noncat_list = function() {
         // don't need a full reset_page() to swap tabs
         $scope.items_out_display = 'noncat';
         patronSvc.items_out = [];
-        provider.refresh();
+        // Grid refresh is not necessary because switching to the
+        // noncat_list always involves instantiating a new grid.
+    }
+
+    $scope.colorizeItemsOutList = {
+        apply: function(item) {
+            var duedate = item.due_date();
+            if (duedate && duedate < new Date().toISOString()) {
+                return 'overdue-row';
+            }
+        }
     }
 
     // Reload the user to pick up changes in items out, fines, etc.
@@ -84,62 +106,111 @@ function($scope,  $q,  $routeParams,  $timeout,  egCore , egUser,  patronSvc , $
         patronSvc.items_out = []; 
         $scope.main_list = [];
         $scope.alt_list = [];
-        provider.refresh() 
+        $timeout(provider.refresh);  // allow scope changes to propagate
     }
 
     var provider = egGridDataProvider.instance({});
     $scope.gridDataProvider = provider;
 
     function fetch_circs(id_list, offset, count) {
-        if (!id_list.length) return $q.when();
+        if (!id_list.length || id_list.length < offset + 1) return $q.when();
+
+        var deferred = $q.defer();
+        var rendered = 0;
+
+        egProgressDialog.open();
 
         // fetch the lot of circs and stream the results back via notify
-        return egCore.pcrud.search('circ', {id : id_list},
+        egCore.pcrud.search('circ', {id : id_list},
             {   flesh : 4,
                 flesh_fields : {
                     circ : ['target_copy', 'workstation', 'checkin_workstation'],
-                    acp : ['call_number'],
-                    acn : ['record'],
-                    bre : ['simple_record']
+                    acp : ['call_number', 'holds_count', 'status', 'circ_lib', 'location', 'floating', 'age_protect', 'parts'],
+                    acpm : ['part'],
+                    acn : ['record', 'owning_lib', 'prefix', 'suffix'],
+                    bre : ['wide_display_entry']
                 },
                 // avoid fetching the MARC blob by specifying which 
                 // fields on the bre to select.  More may be needed.
                 // note that fleshed fields are explicitly selected.
                 select : { bre : ['id'] },
-                limit  : count,
-                offset : offset,
+                // TODO: LP#1697954 Fetch all circs on grid render 
+                // to support client-side sorting.  Migrate to server-side
+                // sorting to avoid the need for fetching all items.
+                //limit  : count,
+                //offset : offset,
                 // we need an order-by to support paging
                 order_by : {circ : ['xact_start']} 
 
         }).then(null, null, function(circ) {
             circ.circ_lib(egCore.org.get(circ.circ_lib())); // local fleshing
 
+            // Translate bib display field JSON blobs to JS.
+            // Collapse multi/array fields down to comma-separated strings.
+            egBibDisplay.mwdeJSONToJS(
+                circ.target_copy().call_number().record().wide_display_entry(), true);
+
             if (circ.target_copy().call_number().id() == -1) {
                 // dummy-up a record for precat items
-                circ.target_copy().call_number().record().simple_record({
+                circ.target_copy().call_number().record().wide_display_entry({
                     title : function() {return circ.target_copy().dummy_title()},
                     author : function() {return circ.target_copy().dummy_author()},
                     isbn : function() {return circ.target_copy().dummy_isbn()}
                 })
             }
+            circ._parts = circ.target_copy().parts().map(function(part) {
+                return part.label()
+            }).join(',');
+
+           patronSvc.items_out.push(circ);
+
+        }).then(function() {
+
+            var circIds = patronSvc.items_out.map(function(circ) { return circ.id() });
+
+            egCore.net.request(
+                'open-ils.actor',
+                'open-ils.actor.user.itemsout.notices',
+                egCore.auth.token(), circIds
+
+            ).then(deferred.resolve, null, function(notice) {
 
-            patronSvc.items_out.push(circ); // toss it into the cache
-            return circ;
+                var circ = patronSvc.items_out.filter(
+                    function(circ) {return circ.id() == notice.circ_id})[0];
+
+                if (notice.numNotices) {
+                    circ.action_trigger_event_count = notice.numNotices;
+                    circ.action_trigger_latest_event_date = notice.lastDt;
+                }
+
+                if (rendered++ >= offset && rendered <= count) {
+                    egProgressDialog.close();
+                    deferred.notify(circ);
+                };
+            });
         });
+
+        return deferred.promise;
     }
 
     function fetch_noncat_circs(id_list, offset, count) {
         if (!id_list.length) return $q.when();
 
-        return egCore.pcrud.search('ancc', {id : id_list},
+        var deferred = $q.defer();
+        var rendered = 0;
+
+        egCore.pcrud.search('ancc', {id : id_list},
             {   flesh : 1,
                 flesh_fields : {ancc : ['item_type','staff']},
-                limit  : count,
-                offset : offset,
+                // TODO: LP#1697954 Fetch all circs on grid render 
+                // to support client-side sorting.  Migrate to server-side
+                // sorting to avoid the need for fetching all items.
+                //limit  : count,
+                //offset : offset,
                 // we need an order-by to support paging
                 order_by : {circ : ['circ_time']} 
 
-        }).then(null, null, function(noncat_circ) {
+        }).then(deferred.resolve, null, function(noncat_circ) {
 
             // calculate the virtual due date from the item type duration
             var seconds = egCore.date.intervalToSeconds(
@@ -152,8 +223,14 @@ function($scope,  $q,  $routeParams,  $timeout,  egCore , egUser,  patronSvc , $
             noncat_circ.circ_lib(egCore.org.get(noncat_circ.circ_lib()));
 
             patronSvc.items_out.push(noncat_circ); // cache it
-            return noncat_circ;
+
+            // We fetch all noncat circs for client-side sorting, but
+            // only notify the caller for the page of requested circs.  
+            if (rendered++ >= offset && rendered <= count)
+                deferred.notify(noncat_circ);
         });
+
+        return deferred.promise;
     }
 
 
@@ -207,6 +284,7 @@ function($scope,  $q,  $routeParams,  $timeout,  egCore , egUser,  patronSvc , $
         var id_list = $scope[$scope.items_out_display + '_list'];
 
         // see if we have the requested range cached
+        // Note this items_out list is reset w/ each items-out tab change
         if (patronSvc.items_out[offset]) {
             return provider.arrayNotifier(
                 patronSvc.items_out, offset, count);
@@ -231,7 +309,7 @@ function($scope,  $q,  $routeParams,  $timeout,  egCore , egUser,  patronSvc , $
         get_circ_ids().then(function() {
 
             id_list = $scope[$scope.items_out_display + '_list'];
-
+            $scope.gridDataProvider.grid.totalCount = id_list.length;
             // relay the notified circs back to the grid through our promise
             fetch_circs(id_list, offset, count).then(
                 deferred.resolve, null, deferred.notify);
@@ -254,11 +332,12 @@ function($scope,  $q,  $routeParams,  $timeout,  egCore , egUser,  patronSvc , $
     $scope.edit_due_date = function(items) {
         if (!items.length) return;
 
-        $modal.open({
+        $uibModal.open({
             templateUrl : './circ/patron/t_edit_due_date_dialog',
+            backdrop: 'static',
             controller : [
-                        '$scope','$modalInstance',
-                function($scope , $modalInstance) {
+                        '$scope','$uibModalInstance',
+                function($scope , $uibModalInstance) {
 
                     // if there is only one circ, default to the due date
                     // of that circ.  Otherwise, default to today.
@@ -273,19 +352,14 @@ function($scope,  $q,  $routeParams,  $timeout,  egCore , egUser,  patronSvc , $
                     // Fire off the due-date updater for each circ.
                     // When all is done, close the dialog
                     $scope.ok = function(args) {
-                        // toISOString gives us Zulu time, so
-                        // adjust for that before truncating to date
-                        var adjust_date = new Date( $scope.args.date );
-                        adjust_date.setMinutes(
-                            $scope.args.date.getMinutes() - adjust_date.getTimezoneOffset()
-                        );
-                        var due = adjust_date.toISOString().replace(/T.*/,'');
+                        var due = $scope.args.due_date.toISOString();
                         console.debug("applying due date of " + due);
+                        egProgressDialog.open();
 
-                        var promises = [];
+                        var promise = $q.when();
                         angular.forEach(items, function(circ) {
-                            promises.push(
-                                egCore.net.request(
+                            promise = promise.then(function() {
+                                return egCore.net.request(
                                     'open-ils.circ',
                                     'open-ils.circ.circulation.due_date.update',
                                     egCore.auth.token(), circ.id(), due
@@ -295,16 +369,17 @@ function($scope,  $q,  $routeParams,  $timeout,  egCore , egUser,  patronSvc , $
                                     // date from the modified circulation.
                                     circ.due_date(new_circ.due_date());
                                 })
-                            );
+                            });
                         });
 
-                        $q.all(promises).then(function() {
-                            $modalInstance.close();
+                        promise.finally(function() {
+                            egProgressDialog.close();
+                            $uibModalInstance.close();
                             provider.refresh();
                         });
                     }
                     $scope.cancel = function($event) {
-                        $modalInstance.dismiss();
+                        $uibModalInstance.dismiss();
                         $event.preventDefault();
                     }
                 }
@@ -314,18 +389,39 @@ function($scope,  $q,  $routeParams,  $timeout,  egCore , egUser,  patronSvc , $
 
     $scope.print_receipt = function(items) {
         if (items.length == 0) return $q.when();
-        var print_data = {circulations : []}
+        var print_data = {circulations : []};
+        var cusr = patronSvc.current;
 
-        angular.forEach(patronSvc.items_out, function(circ) {
+        angular.forEach(items, function(circ) {
             print_data.circulations.push({
                 circ : egCore.idl.toHash(circ),
                 copy : egCore.idl.toHash(circ.target_copy()),
                 call_number : egCore.idl.toHash(circ.target_copy().call_number()),
-                title : circ.target_copy().call_number().record().simple_record().title(),
-                author : circ.target_copy().call_number().record().simple_record().author(),
+                title : circ.target_copy().call_number().record().wide_display_entry().title(),
+                author : circ.target_copy().call_number().record().wide_display_entry().author()
             })
         });
 
+        print_data.patron = {
+            prefix : cusr.prefix(),
+            first_given_name : cusr.first_given_name(),
+            second_given_name : cusr.second_given_name(),
+            family_name : cusr.family_name(),
+            suffix : cusr.suffix(),
+            pref_prefix : cusr.pref_prefix(),
+            pref_first_given_name : cusr.pref_first_given_name(),
+            pref_second_given_name : cusr.pref_second_given_name(),
+            pref_family_name : cusr.pref_family_name(),
+            pref_suffix : cusr.pref_suffix(),
+            card : { barcode : cusr.card().barcode() },
+            money_summary : patronSvc.patron_stats.fines,
+            expire_date : cusr.expire_date(),
+            alias : cusr.alias(),
+            has_email : Boolean(patronSvc.current.email() && patronSvc.current.email().match(/.*@.*/)),
+            has_phone : Boolean(cusr.day_phone() || cusr.evening_phone() || cusr.other_phone()),
+            juvenile : cusr.juvenile()
+        };
+
         return egCore.print.print({
             context : 'default', 
             template : 'items_out', 
@@ -333,12 +429,32 @@ function($scope,  $q,  $routeParams,  $timeout,  egCore , egUser,  patronSvc , $
         });
     }
 
+    function batch_action_with_flat_copies(items, action) {
+        if (!items.length) return;
+        var copies = items.map(function(circ) 
+            { return egCore.idl.toHash(circ.target_copy()) });
+        action(copies).then(reset_page);
+    }
     function batch_action_with_barcodes(items, action) {
         if (!items.length) return;
         var barcodes = items.map(function(circ) 
             { return circ.target_copy().barcode() });
         action(barcodes).then(reset_page);
     }
+    $scope.mark_damaged = function(items) {
+        if (items.length == 0) return;
+
+        angular.forEach(items, function(circ) {
+            egCirc.mark_damaged({
+                id: circ.target_copy().id(),
+                barcode: circ.target_copy().barcode(),
+                circ_lib: circ.target_copy().circ_lib().id()
+            }).then(() => $timeout(reset_page,1000)) // reset after each, because rejecting one stops the $q.all() chain
+        });
+    }
+    $scope.mark_missing = function(items) {
+        batch_action_with_flat_copies(items, egCirc.mark_missing);
+    }
     $scope.mark_lost = function(items) {
         batch_action_with_barcodes(items, egCirc.mark_lost);
     }
@@ -363,11 +479,10 @@ function($scope,  $q,  $routeParams,  $timeout,  egCore , egUser,  patronSvc , $
     $scope.show_triggered_events = function(items) {
         var focus = items.length == 1;
         angular.forEach(items, function(item) {
-            var url = egCore.env.basePath +
-                      '/cat/item/' +
-                      item.target_copy().id() +
-                      '/triggered_events';
+            var url = '/eg2/staff/circ/item/event-log/' +
+                      item.target_copy().id();
             $timeout(function() { var x = $window.open(url, '_blank'); if (focus) x.focus() });
+
         });
     }
 
@@ -380,11 +495,35 @@ function($scope,  $q,  $routeParams,  $timeout,  egCore , egUser,  patronSvc , $
 
         return egConfirmDialog.open(msg, barcodes.join(' '), {}).result
         .then(function() {
+            window.oils_cancel_batch = false;
+            window.oils_inside_batch = true;
+            function batch_cleanup() {
+                if (window.oils_inside_batch && window.oils_op_change_within_batch) {
+                    window.oils_op_change_undo_func();
+                }
+                window.oils_inside_batch = false;
+                window.oils_op_change_within_batch = false;
+                reset_page();
+            }
             function do_one() {
                 var bc = barcodes.pop();
-                if (!bc) { reset_page(); return }
+                if (!bc) {
+                    batch_cleanup();
+                    return;
+                }
+                if (window.oils_op_change_within_batch) {
+                    window.oils_op_change_toast_func();
+                }
                 // finally -> continue even when one fails
-                egCirc.renew({copy_barcode : bc}).finally(do_one);
+                egCirc.renew({copy_barcode : bc}).finally(function() {
+                    if (!window.oils_cancel_batch) {
+                        do_one();
+                    } else {
+                        console.log('batch cancelled');
+                        batch_cleanup();
+                        return;
+                    }
+                });
             }
             do_one();
         });
@@ -406,17 +545,20 @@ function($scope,  $q,  $routeParams,  $timeout,  egCore , egUser,  patronSvc , $
         var barcodes = items.map(function(circ) 
             { return circ.target_copy().barcode() });
 
-        return $modal.open({
-            templateUrl : './circ/patron/t_edit_due_date_dialog',
+        return $uibModal.open({
             templateUrl : './circ/patron/t_renew_with_date_dialog',
+            backdrop: 'static',
             controller : [
-                        '$scope','$modalInstance',
-                function($scope , $modalInstance) {
+                        '$scope','$uibModalInstance',
+                function($scope , $uibModalInstance) {
+                    var now = new Date();
+                    $scope.outOfRange = false;
+                    $scope.minDate = new Date(now);
                     $scope.args = {
                         barcodes : barcodes,
-                        date : new Date()
+                        date : new Date(now)
                     }
-                    $scope.cancel = function() {$modalInstance.dismiss()}
+                    $scope.cancel = function() {$uibModalInstance.dismiss()}
 
                     // Fire off the due-date updater for each circ.
                     // When all is done, close the dialog
@@ -429,7 +571,7 @@ function($scope,  $q,  $routeParams,  $timeout,  egCore , egUser,  patronSvc , $
                                 egCirc.renew({copy_barcode : bc, due_date : due})
                                 .finally(do_one);
                             } else {
-                                $modalInstance.close(); 
+                                $uibModalInstance.close(); 
                                 reset_page();
                             }
                         }
@@ -442,23 +584,33 @@ function($scope,  $q,  $routeParams,  $timeout,  egCore , egUser,  patronSvc , $
 
     $scope.checkin = function(items) {
         if (!items.length) return;
-        var barcodes = items.map(function(circ) 
-            { return circ.target_copy().barcode() });
-
-        return egConfirmDialog.open(
-            egCore.strings.CHECK_IN_CONFIRM, barcodes.join(' '), {
-
-        }).result.then(function() {
-            function do_one() {
-                if (bc = barcodes.pop()) {
-                    egCirc.checkin({copy_barcode : bc})
-                    .finally(do_one);
-                } else {
-                    reset_page();
-                }
+        var copies = items.map(function(circ) { return circ.target_copy() });
+        var barcodes = copies.map(function(copy) { return copy.barcode() });
+        
+        var copy;
+        function do_one() {
+            if (copy = copies.pop()) {
+                // Checkin expects a barcode, but will pass other
+                // parameters too.  Passing the copy ID allows
+                // for the checkin of deleted copies on the server.
+                egCirc.checkin(
+                    {copy_barcode: copy.barcode(), copy_id: copy.id()},
+                    {suppress_popups: $scope.suppress_popups})
+                .finally(do_one);
+            } else {
+                reset_page();
             }
-            do_one(); // kick it off
-        });
+        }
+        if ($scope.suppress_popups) {
+            do_one();
+        } else {
+            return egConfirmDialog.open(
+                egCore.strings.CHECK_IN_CONFIRM, barcodes.join(' '), {
+
+            }).result.then(function() {
+                do_one(); // kick it off
+            });
+        }
     }
 
     $scope.add_billing = function(items) {