ff ui / column sorting and more
authorBill Erickson <berick@esilibrary.com>
Wed, 6 Nov 2013 14:44:30 +0000 (09:44 -0500)
committerBill Erickson <berick@esilibrary.com>
Wed, 6 Nov 2013 14:44:30 +0000 (09:44 -0500)
Signed-off-by: Bill Erickson <berick@esilibrary.com>
Open-ILS/src/templates/staff/fulfillment/t_item_table.tt2
Open-ILS/web/js/ui/default/staff/fulfillment/app.js

index eb7ff3a..481c5d1 100644 (file)
@@ -36,9 +36,13 @@ COLUMNS = [
 <table class="table table-striped table-hover table-condensed" ng-show="itemList.count()">
   <thead>
     <tr>
-      <th><a href="javascript:;" ng-click="itemList.selectAll()">&#x2713;</a></th>
+      <th><a href="javascript:;" ng-click="itemList.toggleSelectAll()">&#x2713;</a></th>
       [% FOREACH col IN COLUMNS %]
-      <th [% IF col.tabs %]ng-show="[% col.tabs %]"[% END %]>[% col.label %]</th>
+      <th [% IF col.tabs %]ng-show="[% col.tabs %]"[% END %]>
+        [% IF col.name == 'index'; col.label; ELSE %]
+        <a href='javascript:;' ng-click="sort('[% col.name %]')">[% col.label %]</a>
+        [% END %]
+      </th>
       [% END %]
     </tr>
   </thead>
index c475d0a..6c9e00b 100644 (file)
@@ -1,21 +1,14 @@
 /**
- * TODO:
- * Instead of making pcrud calls, followed by per-item server calls,
- * consider a server-side API for all of this stuff for speed, etc.
- * OR consider using all flattener-based calls.
- *
- * Consolidate the various item structures into one common,
- * authoritative structure for display and print templates.
+ * FulfILLment application.
+ * Includes pending items, inbound/outbound transits, on shelf,
+ * circulating, item status, and bib record file upload.
  */
 
-
 angular.module('ffMain', 
 ['ngRoute', 'egCoreMod', 'egUiMod', 'egUserMod', 'egListMod'])
 
 .config(function($routeProvider, $locationProvider) {
-
-    // The route-specified controller will not get instantiated 
-    // until the promise returned by this function is resolved 
+    $locationProvider.html5Mode(true);
     var resolver = {delay : function(egStartup) {return egStartup.go()}};
 
     // record management UI
@@ -25,18 +18,21 @@ angular.module('ffMain',
         resolve : resolver
     });
 
+    // item status by barcode
     $routeProvider.when('/fulfillment/status/:barcode', {
         templateUrl: './fulfillment/t_ill',
         controller: 'ILLCtrl',
         resolve : resolver
     });
 
+    // item status, barcode pending
     $routeProvider.when('/fulfillment/status', {
         templateUrl: './fulfillment/t_ill',
         controller: 'ILLCtrl',
         resolve : resolver
     });
 
+    // transaction-focused tabs
     $routeProvider.when('/fulfillment/:orientation/:tab', {
         templateUrl: './fulfillment/t_ill',
         controller: 'ILLCtrl',
@@ -47,8 +43,6 @@ angular.module('ffMain',
     $routeProvider.otherwise({
         redirectTo : '/fulfillment/borrower/pending'
     });
-
-    $locationProvider.html5Mode(true);
 })
 
 /**
@@ -143,82 +137,92 @@ function ($scope,  $q,  $compile,  $timeout,  $rootScope, $location,
     // so our child controllers can access our route info
     $scope.illRouteParams = $routeParams;
 
-    $scope.itemList = egList.create({limit : 10}); // limit TBD
+    $scope.itemList = egList.create({limit : 10}); // UI?
     $scope.columns = [];
     $scope.addColumn = function(col) {
         $scope.columns.push(col);
     }
 
+    // sort by column name. if already sorting on the selected column,
+    // sort 'desc' instead.
+    $scope.sort = function(colname) {
+        if (typeof $scope.itemList.sort == 'string' &&
+                $scope.itemList.sort == colname) {
+            $scope.itemList.sort = {};
+            $scope.itemList.sort[colname] = 'desc';
+        } else {
+            $scope.itemList.sort = colname;
+        }
+        $scope.collect();
+    }
+
     // map of flattener fields to retrieve for each query type
+    // TODO: there's some duplication here, since the fields
+    // are also defined in the template.
     $scope.flatFields = {
         ahr : {
-            id : {path : 'id'},
-            hold_id : {path : 'id'},
-            request_time : {path : 'request_time'},
-            expire_time : {path : 'expire_time'},
-            patron_id : {path : 'usr.id'},
-            patron_barcode : {path : 'usr.card.barcode'},
-            patron_given_name : {path : 'usr.first_given_name'},
-            patron_family_name : {path : 'usr.family_name'},
-            hold_request_lib : {path : 'request_lib.shortname'}, // TODO: causes query problem, wha?
-            hold_pickup_lib : {path : 'pickup_lib.shortname'},
-            title : {path : 'bib_rec.bib_record.simple_record.title'},
-            author : {path : 'bib_rec.bib_record.simple_record.author'},
-            copy_id : {path : 'current_copy.id'},
-            copy_status : {path : 'current_copy.status.name'},
-            copy_barcode : {path : 'current_copy.barcode'},
-            copy_circ_lib : {path : 'current_copy.circ_lib.shortname'},
-            call_number : {path : 'current_copy.call_number.label'}
+            id : 'id',
+            hold_id : 'id',
+            request_time : 'request_time',
+            expire_time : 'expire_time',
+            patron_id : 'usr.id',
+            patron_barcode : 'usr.card.barcode',
+            patron_given_name : 'usr.first_given_name',
+            patron_family_name : 'usr.family_name',
+            hold_request_lib : 'request_lib.shortname',
+            hold_pickup_lib : 'pickup_lib.shortname',
+            title : 'bib_rec.bib_record.simple_record.title',
+            author : 'bib_rec.bib_record.simple_record.author',
+            copy_id : 'current_copy.id',
+            copy_status : 'current_copy.status.name',
+            copy_barcode : 'current_copy.barcode',
+            copy_circ_lib : 'current_copy.circ_lib.shortname',
+            call_number : 'current_copy.call_number.label'
         },
         circ : {
-            id : {path : 'id'},
-            circ_id : {path : 'id'},
-            patron_id : {path : 'usr.id'},
-            patron_barcode : {path : 'usr.card.barcode'},
-            patron_given_name : {path : 'usr.first_given_name'},
-            patron_family_name : {path : 'usr.family_name'},
-            title : {path : 'target_copy.call_number.record.simple_record.title'},
-            author : {path : 'target_copy.call_number.record.simple_record.author'},
-            copy_id : {path : 'target_copy.id'},
-            copy_status : {path : 'target_copy.status.name'},
-            copy_barcode : {path : 'target_copy.barcode'},
-            copy_circ_lib : {path : 'target_copy.circ_lib.shortname'},
-            call_number : {path : 'target_copy.call_number.label'},
-            circ_circ_lib : {path : 'circ_lib.shortname'},
-            due_date : {path : 'due_date'},
-            xact_start : {path : 'xact_start'},
-            checkin_time : {path : 'checkin_time'}
+            id : 'id',
+            circ_id : 'id',
+            patron_id : 'usr.id',
+            patron_barcode : 'usr.card.barcode',
+            patron_given_name : 'usr.first_given_name',
+            patron_family_name : 'usr.family_name',
+            title : 'target_copy.call_number.record.simple_record.title',
+            author : 'target_copy.call_number.record.simple_record.author',
+            copy_id : 'target_copy.id',
+            copy_status : 'target_copy.status.name',
+            copy_barcode : 'target_copy.barcode',
+            copy_circ_lib : 'target_copy.circ_lib.shortname',
+            call_number : 'target_copy.call_number.label',
+            circ_circ_lib : 'circ_lib.shortname',
+            due_date : 'due_date',
+            xact_start : 'xact_start',
+            checkin_time : 'checkin_time'
         },
         atc : {
-            id : {path : 'id'},
-            transit_id : {path : 'id'},
-            hold_id : {path : 'hold_transit_copy.hold.id'},
-            hold_request_lib : {path : 'hold_transit_copy.hold.request_lib.shortname'},
-            hold_pickup_lib : {path : 'hold_transit_copy.hold.pickup_lib.shortname'},
-            patron_id : {path : 'hold_transit_copy.hold.usr.id'},
-            patron_barcode : {path : 'hold_transit_copy.hold.usr.card.barcode'},
-            patron_given_name : {path : 'hold_transit_copy.hold.usr.first_given_name'},
-            patron_family_name : {path : 'hold_transit_copy.hold.usr.family_name'},
-            title : {path : 'target_copy.call_number.record.simple_record.title'},
-            author : {path : 'target_copy.call_number.record.simple_record.author'},
-            copy_status : {path : 'target_copy.status.name'},
-            copy_id : {path : 'target_copy.id'},
-            copy_barcode : {path : 'target_copy.barcode'},
-            copy_circ_lib : {path : 'target_copy.circ_lib.shortname'},
-            call_number : {path : 'target_copy.call_number.label'},
-            transit_time : {path : 'source_send_time'},
-            transit_source : {path : 'source.shortname'},
-            transit_dest : {path : 'dest.shortname'}
+            id : 'id',
+            transit_id : 'id',
+            hold_id : 'hold_transit_copy.hold.id',
+            hold_request_lib : 'hold_transit_copy.hold.request_lib.shortname',
+            hold_pickup_lib : 'hold_transit_copy.hold.pickup_lib.shortname',
+            patron_id : 'hold_transit_copy.hold.usr.id',
+            patron_barcode : 'hold_transit_copy.hold.usr.card.barcode',
+            patron_given_name : 'hold_transit_copy.hold.usr.first_given_name',
+            patron_family_name : 'hold_transit_copy.hold.usr.family_name',
+            title : 'target_copy.call_number.record.simple_record.title',
+            author : 'target_copy.call_number.record.simple_record.author',
+            copy_status : 'target_copy.status.name',
+            copy_id : 'target_copy.id',
+            copy_barcode : 'target_copy.barcode',
+            copy_circ_lib : 'target_copy.circ_lib.shortname',
+            call_number : 'target_copy.call_number.label',
+            transit_time : 'source_send_time',
+            transit_source : 'source.shortname',
+            transit_dest : 'dest.shortname'
         }
     };
 
-    // set display=true for each flattener field
-    angular.forEach($scope.flatFields, function(val) {
-        angular.forEach(val, function(val2) {
-            val2.display = true;
-        });
-    });
-
+    // apply the route-specific data loading class/query
+    // query == flattener query
     $scope.setCollector = function(class_, query) {
         $scope.collector = {
             query : query,
@@ -226,13 +230,31 @@ function ($scope,  $q,  $compile,  $timeout,  $rootScope, $location,
         }
     }
 
+    // called on each item as it's received
     $scope.setMunger = function(func) {
         $scope.munger = func;
     }
 
+    // table paging...
+    $scope.firstPage = function() {
+        $scope.itemList.offset = 0;
+        $scope.collect();
+    };
+
+    $scope.nextPage = function() {
+        $scope.itemList.incrementPage();
+        $scope.collect();
+    };
+
+    $scope.prevPage = function() {
+        $scope.itemList.decrementPage();
+        $scope.collect();
+    };
+
+    // fire the flattened search query to collect items
     $scope.collect = function() {
         $scope.lookupComplete = false;
-        $scope.itemList.items = [];
+        $scope.itemList.resetPageData();
         egNet.request(
             'open-ils.fielder',
             'open-ils.fielder.flattened_search',
@@ -261,198 +283,183 @@ function ($scope,  $q,  $compile,  $timeout,  $rootScope, $location,
     }
 
 
-    /* Actions
-     * Performed on flattened items (see above)
-     */
-    $scope.actions = {
+    // Actions
+    // Performed on flattened items
+    $scope.actions = {};
 
-        checkin : function(item) {
-            $scope.action_pending = true;
-            var deferred = $q.defer();
-            egNet.request(
-                'open-ils.circ', 
-                'open-ils.circ.checkin.override',
-                egAuth.token(), {
-                    circ_lib : orgSelector.current().id(),
-                    copy_id: item.copy_id,
-                    ff_action: item.next_action
-                }
-            ).then(function(response) {
-                $scope.action_pending = false;
-                // do some basic sanity checking before passing 
-                // the response to the caller.
-                if (response) {
-                    if (angular.isArray(response))
-                        response = response[0];
-                    // TODO: check for failure events
-                    deferred.resolve(response);
-                } else {
-                    // warn that checkin failed
-                    deferred.reject();
-                }
-            });
-            return deferred.promise;
-        },
+    $scope.actions.checkin = function(item) {
+        $scope.action_pending = true;
+        var deferred = $q.defer();
+        egNet.request('open-ils.circ', 
+            'open-ils.circ.checkin.override',
+            egAuth.token(), {
+                circ_lib : orgSelector.current().id(),
+                copy_id: item.copy_id, ff_action: item.next_action
+            }
+        ).then(function(response) {
+            $scope.action_pending = false;
+            // do some basic sanity checking before passing 
+            // the response to the caller.
+            if (response) {
+                if (angular.isArray(response))
+                    response = response[0];
+                // TODO: check for failure events
+                deferred.resolve(response);
+            } else {
+                // warn that checkin failed
+                deferred.reject();
+            }
+        });
+        return deferred.promise;
+    }
 
-        checkout : function(item) {
-            $scope.action_pending = true;
-            var deferred = $q.defer();
-            egNet.request(
-                'open-ils.circ', 
-                'open-ils.circ.checkout.full.override',
-                egAuth.token(), {
-                    circ_lib : orgSelector.current().id(),
-                    patron_id : item.user_id,
-                    copy_id: item.copy_id,
-                    ff_action: item.next_action
-                }
-            ).then(function(response) {
-                $scope.action_pending = false;
-                // do some basic sanity checking before passing 
-                // the response to the caller.
-                if (response) {
-                    if (angular.isArray(response))
-                        response = response[0];
-                    // TODO: check for failure events
-                    deferred.resolve(response);
-                } else {
-                    // warn that checkin failed
-                    deferred.reject();
-                }
-            });
-            return deferred.promise;
-        },
+    $scope.actions.checkout = function(item) {
+        $scope.action_pending = true;
+        var deferred = $q.defer();
+        egNet.request('open-ils.circ', 
+            'open-ils.circ.checkout.full.override',
+            egAuth.token(), {
+                circ_lib : orgSelector.current().id(),
+                patron_id : item.user_id,
+                copy_id: item.copy_id,
+                ff_action: item.next_action
+            }
+        ).then(function(response) {
+            $scope.action_pending = false;
+            // do some basic sanity checking before passing 
+            // the response to the caller.
+            if (response) {
+                if (angular.isArray(response))
+                    response = response[0];
+                // TODO: check for failure events
+                deferred.resolve(response);
+            } else {
+                // warn that checkin failed
+                deferred.reject();
+            }
+        });
+        return deferred.promise;
+    }
 
 
-        cancel : function(item) {
-            var deferred = $q.defer();
-            $scope.action_pending = true;
-            egNet.request(
-                'open-ils.circ', 'open-ils.circ.hold.cancel',
-                egAuth.token(), item.hold_id
-            ).then(function() {
-                $scope.action_pending = false;
-                deferred.resolve();
-            });
-            return deferred.promise;
-        },
+    $scope.actions.cancel = function(item) {
+        var deferred = $q.defer();
+        $scope.action_pending = true;
+        egNet.request('open-ils.circ', 
+            'open-ils.circ.hold.cancel',
+            egAuth.token(), item.hold_id
+        ).then(function() {
+            $scope.action_pending = false;
+            deferred.resolve();
+        });
+        return deferred.promise;
+    }
 
-        retarget : function(item) {
-            var deferred = $q.defer();
-            $scope.action_pending = true;
-            egNet.request(
-              'open-ils.circ', 'open-ils.circ.hold.reset',
-              egAuth.token(), item.hold_id
-            ).then(function() {
-                $scope.action_pending = false;
-                deferred.resolve();
-            });
-            return deferred.promise;
-        },
+    $scope.actions.retarget = function(item) {
+        var deferred = $q.defer();
+        $scope.action_pending = true;
+        egNet.request('open-ils.circ', 
+            'open-ils.circ.hold.reset',
+          egAuth.token(), item.hold_id
+        ).then(function() {
+            $scope.action_pending = false;
+            deferred.resolve();
+        });
+        return deferred.promise;
+    }
 
-        abort_transit : function(item) {
-            var deferred = $q.defer();
-            $scope.action_pending = true;
-            egNet.request(
-                'open-ils.circ', 'open-ils.circ.transit.abort',
-                egAuth.token(), {transitid : item.transit_id}
-            ).then(function() {
-                $scope.action_pending = false;
-                deferred.resolve();
-            });
-            return deferred.promise;
-        },
+    $scope.actions.abort_transit = function(item) {
+        var deferred = $q.defer();
+        $scope.action_pending = true;
+        egNet.request('open-ils.circ', 
+            'open-ils.circ.transit.abort',
+            egAuth.token(), {transitid : item.transit_id}
+        ).then(function() {
+            $scope.action_pending = false;
+            deferred.resolve();
+        });
+        return deferred.promise;
+    }
 
-        mark_lost : function(item) {
-            var deferred = $q.defer();
-            $scope.action_pending = true;
-            egNet.request(
-                'open-ils.circ', 'open-ils.circ.circulation.set_lost',
-                egAuth.token(), {barcode : item.item_barcode}
-            ).then(function(resp) {
-                $scope.action_pending = false;
-                if (resp == 1) {
-                    deferred.resolve();
-                } else {
-                    console.error('mark lost failed: ' + js2JSON(resp));
-                    deferred.reject();
-                }
-            });
-            return deferred.promise;
-        },
+    $scope.actions.mark_lost = function(item) {
+        var deferred = $q.defer();
+        $scope.action_pending = true;
+        egNet.request('open-ils.circ', 
+            'open-ils.circ.circulation.set_lost',
+            egAuth.token(), {barcode : item.item_barcode}
+        ).then(function(resp) {
+            $scope.action_pending = false;
+            if (resp == 1) {
+                deferred.resolve();
+            } else {
+                console.error('mark lost failed: ' + js2JSON(resp));
+                deferred.reject();
+            }
+        });
+        return deferred.promise;
+    }
 
-        print : function(item) {
-            var deferred = $q.defer();
-            $scope.action_pending = true;
-            var focus = item.hold ? 'hold' :
-                (item.circ ? 'circ' :
-                    (item.transit ? 'transit' : 'copy'));
+    $scope.actions.print = function(item) {
+        var deferred = $q.defer();
+        $scope.action_pending = true;
+        var focus = item.hold ? 'hold' :
+            (item.circ ? 'circ' : (item.transit ? 'transit' : 'copy'));
 
-            // TODO: line up print template variables with
-            // local data structures
-            item.barcode = item.copy_barcode || item.item_barcode;
-            item.status = item.status_str || item.copy_status;
-            item.item_circ_lib = item.copy_circ_lib || item.circ_lib;
+        // TODO: line up print template variables with
+        // local data structures
+        item.barcode = item.copy_barcode || item.item_barcode;
+        item.status = item.status_str || item.copy_status;
+        item.item_circ_lib = item.copy_circ_lib || item.circ_lib;
 
-            egNet.request(
-                'open-ils.actor',
-                'open-ils.actor.web_action_print_template.fetch',
-                orgSelector.current().id(), focus
-            ).then(function(template) {
-
-                if (!template || !(template = template.template())) { // assign
-                    console.warn('unable to find template for ' + 
-                        item.copy_barcode + ' : ' + focus);
-                    return;
-                }
+        egNet.request(
+            'open-ils.actor',
+            'open-ils.actor.web_action_print_template.fetch',
+            orgSelector.current().id(), focus
+        ).then(function(template) {
+
+            if (!template || !(template = template.template())) { // assign
+                console.warn('unable to find template for ' + 
+                    item.copy_barcode + ' : ' + focus);
+                return;
+            }
 
-                // NOTE: templates stored for now as dojo-style
-                // template.  mangle to angular templates manually.
-                template = template.replace(/\${([^}]+)}/g, '{{$1}}');
+            // TODO: templates stored for now as dojo-style
+            // template.  mangle to angular templates manually.
+            template = template.replace(/\${([^}]+)}/g, '{{$1}}');
 
-                // compile the template w/ a temporary print scope
-                var printScope = $rootScope.$new();
-                angular.forEach(item, function(val, key) {
-                    printScope[key] = val;
-                });
-                var element = angular.element(template);
-                $compile(element)(printScope);
-
-                // append the compiled element to the new window and print
-                var w = window.open();
-                $(w.document.body).append(element);
-                w.document.close();
-                
-                // $timeout needed in some environments (Mac confirmed) 
-                // to allow the new window to fully digest before printing.
-                $timeout(
-                    function() {
-                        w.print();
-                        w.close();
-                        $scope.action_pending = false;
-                        deferred.resolve();
-                    }
-                );
+            // compile the template w/ a temporary print scope
+            var printScope = $rootScope.$new();
+            angular.forEach(item, function(val, key) {
+                printScope[key] = val;
             });
-            return deferred.promise;
-        }
-    };
+            var element = angular.element(template);
+            $compile(element)(printScope);
+
+            // append the compiled element to the new window and print
+            var w = window.open();
+            $(w.document.body).append(element);
+            w.document.close();
+            
+            // $timeout needed in some environments (Mac confirmed) 
+            // to allow the new window to fully $digest() before printing.
+            $timeout(
+                function() {
+                    w.print();
+                    w.close();
+                    $scope.action_pending = false;
+                    deferred.resolve();
+                }
+            );
+        });
+        return deferred.promise;
+    }
 
     // default batch action handlers.
-    // when a batch is done, reload the route, unless printing.
-
-    function performAction(action, item) {
+    function performOneAction(action, item) {
         console.debug(item.index + ' => ' + action);
-        $scope.actions[action](item).then(
-            // when all items are done processing, reload the route
-            function(resp) {
-                console.debug(item.index + ' => ' + action + ' : done');
-                if (--total == 0 && action != 'print') 
-                    $route.reload()
-            }, 
-            function(resp) {
-                console.error("error in " + action + ": " + resp);
-            }
+        return $scope.actions[action](item).then(
+            function(resp) {console.debug(item.index + ' => ' + action + ' : done')}, 
+            function(resp) {console.error("error in " + action + " : " + resp)}
         );
     }
 
@@ -460,33 +467,21 @@ function ($scope,  $q,  $compile,  $timeout,  $rootScope, $location,
         Object.keys($scope.actions),
         function(action) {
             $scope[action] = function() {
-                var total = Object.keys($scope.itemList.selected).length;
-                angular.forEach($scope.itemList.selected,
-                    function(val, idx) {
-                        var item = $scope.itemList.items.filter(
-                            function(i) {return i.index == idx})[0];
-                        performAction(action, item)
+                var promises = [];
+                angular.forEach(
+                    $scope.itemList.selectedItems(),
+                    function(item) {
+                        promises.push(performOneAction(action, item))
                     }
                 );
-            };
+                // when a batch has successfully completed, 
+                // reload the route, unless printing.
+                $q.all(promises).then(function() { 
+                    if (action != 'print') $route.reload();
+                });
+            }
         }
     );
-
-    $scope.firstPage = function() {
-        $scope.itemList.offset = 0;
-        $scope.collect();
-    };
-
-    $scope.nextPage = function() {
-        $scope.itemList.incrementPage();
-        $scope.collect();
-    };
-
-    $scope.prevPage = function() {
-        $scope.itemList.decrementPage();
-        $scope.collect();
-    };
-
 }])
 
 .controller('PendingRequestsCtrl',