initial hold pull list; hold grid action consolidation
authorBill Erickson <berick@esilibrary.com>
Wed, 9 Jul 2014 19:00:44 +0000 (15:00 -0400)
committerBill Erickson <berick@esilibrary.com>
Wed, 9 Jul 2014 19:00:44 +0000 (15:00 -0400)
Signed-off-by: Bill Erickson <berick@esilibrary.com>
Open-ILS/src/templates/staff/circ/holds/t_pull.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/circ/holds/t_pull_list.tt2 [new file with mode: 0644]
Open-ILS/src/templates/staff/circ/holds/t_shelf.tt2
Open-ILS/src/templates/staff/circ/holds/t_shelf_list.tt2
Open-ILS/src/templates/staff/circ/patron/t_holds_list.tt2
Open-ILS/src/templates/staff/navbar.tt2
Open-ILS/web/js/ui/default/staff/circ/holds/app.js
Open-ILS/web/js/ui/default/staff/circ/patron/holds.js
Open-ILS/web/js/ui/default/staff/circ/services/holds.js

diff --git a/Open-ILS/src/templates/staff/circ/holds/t_pull.tt2 b/Open-ILS/src/templates/staff/circ/holds/t_pull.tt2
new file mode 100644 (file)
index 0000000..de2988b
--- /dev/null
@@ -0,0 +1,26 @@
+<div class="container-fluid" style="text-align:center">
+  <div class="alert alert-info alert-less-pad strong-text-2">
+    <span>[% l('Holds Pull List') %]</span>
+  </div>
+</div>
+
+<div class="pad-vert"></div>
+
+<div ng-if="!detail_hold_id">
+[% INCLUDE 'staff/circ/holds/t_pull_list.tt2' %]
+</div>
+
+<!-- hold details -->
+<div ng-if="detail_hold_id">
+  <div class="row">
+    <div class="col-md-2">
+      <button class="btn btn-default" ng-click="list_view()">
+        [% l('List View') %]
+      </button>
+    </div>
+  </div>
+  <div class="pad-vert"></div>
+  <eg-record-summary record='detail_hold_record' 
+    record-id="detail_hold_record_id"></eg-record-summary>
+  <eg-hold-details hold-retrieved="set_hold" hold-id="detail_hold_id"></eg-hold-details>
+</div>
diff --git a/Open-ILS/src/templates/staff/circ/holds/t_pull_list.tt2 b/Open-ILS/src/templates/staff/circ/holds/t_pull_list.tt2
new file mode 100644 (file)
index 0000000..b7e7ba1
--- /dev/null
@@ -0,0 +1,71 @@
+<eg-grid
+  id-field="id"
+  features="-sort,-multisort"
+  items-provider="gridDataProvider"
+  persist-key="circ.holds.pull">
+
+  <eg-grid-menu-item handler="detail_view" 
+    label="[% l('Detail View') %]"></eg-grid-menu-item>
+
+  <eg-grid-action handler="grid_actions.show_recent_circs"
+    label="[% l('Show Last Few Circulations') %]"></eg-grid-action>
+  <eg-grid-action divider="true"></eg-grid-action>
+  <eg-grid-action handler="grid_actions.set_copy_quality"
+    label="[% l('Set Desired Copy Quality') %]"></eg-grid-action>
+  <eg-grid-action handler="grid_actions.edit_pickup_lib"
+    label="[% l('Edit Pickup Library') %]"></eg-grid-action>
+  <eg-grid-action handler="grid_actions.edit_notify_prefs"
+    label="[% l('Edit Notification Settings') %]"></eg-grid-action>
+  <eg-grid-action handler="grid_actions.edit_dates"
+    label="[% l('Edit Hold Dates') %]"></eg-grid-action>
+  <eg-grid-action handler="grid_actions.activate"
+    label="[% l('Activate') %]"></eg-grid-action>
+  <eg-grid-action handler="grid_actions.suspend"
+    label="[% l('Suspend') %]"></eg-grid-action>
+  <eg-grid-action handler="grid_actions.set_top_of_queue"
+    label="[% l('Set Top of Queue') %]"></eg-grid-action>
+  <eg-grid-action handler="grid_actions.clear_top_of_queue"
+    label="[% l('Un-Set Top of Queue') %]"></eg-grid-action>
+  <eg-grid-action handler="grid_actions.transfer_to_marked_title"
+    label="[% l('Transfer To Marked Title') %]"></eg-grid-action>
+  <eg-grid-action handler="grid_actions.mark_damaged"
+    label="[% l('Mark Item Damaged') %]"></eg-grid-action>
+  <eg-grid-action handler="grid_actions.mark_missing"
+    label="[% l('Mark Item Missing') %]"></eg-grid-action>
+  <eg-grid-action divider="true"></eg-grid-action>
+  <eg-grid-action handler="grid_actions.retarget"
+    label="[% l('Find Another Target') %]"></eg-grid-action>
+  <eg-grid-action handler="grid_actions.cancel_hold"
+    label="[% l('Cancel Hold') %]"></eg-grid-action>
+
+  <eg-grid-field label="[% l('Hold ID') %]" path='hold.id'></eg-grid-field>
+  <eg-grid-field label="[% l('Current Copy') %]" 
+    path='hold.current_copy.barcode'>
+    <a href="./cat/item/{{item.hold.current_copy().id()}}/summary" target="_self">
+      {{item.hold.current_copy().barcode()}}
+    </a>
+  </eg-grid-field>
+
+  <eg-grid-field label="[% l('Request Date') %]" path='hold.request_time'></eg-grid-field>
+  <eg-grid-field label="[% l('Hold Type') %]" path='hold.hold_type'></eg-grid-field>
+  <eg-grid-field label="[% l('Pickup Library') %]" path='hold.pickup_lib.shortname'></eg-grid-field>
+  <eg-grid-field label="[% l('Copy Location') %]" path='copy.location.name'></eg-grid-field>
+  <eg-grid-field label="[% l('Call Number') %]" path='volume.label'></eg-grid-field>
+
+  <eg-grid-field label="[% l('Title') %]" path='mvr.title'>
+    <a href="[% ctx.base_path %]/opac/record/{{item.mvr.doc_id()}}">
+      {{item.mvr.title()}}
+    </a>
+  </eg-grid-field>
+
+  <eg-grid-field label="[% l('Author') %]" path='mvr.author'></eg-grid-field>
+  <eg-grid-field label="[% l('Potential Copies') %]" path='potential_copies'></eg-grid-field>
+  <eg-grid-field label="[% l('Status') %]" path='status_string' hidden></eg-grid-field>
+
+  <eg-grid-field label="[% l('Queue Position') %]" path='queue_position' hidden></eg-grid-field>
+  <eg-grid-field path='hold.*' parent-idl-class="ahr" hidden></eg-grid-field>
+  <eg-grid-field path='copy.*' parent-idl-class="acp" hidden></eg-grid-field>
+  <eg-grid-field path='volume.*' parent-idl-class="acn" hidden></eg-grid-field>
+  <eg-grid-field path='mvr.*' parent-idl-class="mvr" hidden></eg-grid-field>
+</eg-grid>
+
index a3c4005..b0a4aaf 100644 (file)
@@ -1,3 +1,9 @@
+<div class="container-fluid" style="text-align:center">
+  <div class="alert alert-info alert-less-pad strong-text-2">
+    <span>[% l('Holds Shelf') %]</span>
+  </div>
+</div>
+
 <div class="row">
   <div class="col-md-3">
     <div class="input-group">
index 1ccf6b6..33e5320 100644 (file)
   <eg-grid-menu-item handler="clear_holds" disabled="disable_clear"
     label="[% l('Clear These Holds') %]"></eg-grid-menu-item>
 
-  <eg-grid-action handler="show_recent_circs"
+  <eg-grid-action handler="grid_actions.show_recent_circs"
     label="[% l('Show Last Few Circulations') %]"></eg-grid-action>
   <eg-grid-action divider="true"></eg-grid-action>
-  <eg-grid-action handler="set_copy_quality"
+  <eg-grid-action handler="grid_actions.set_copy_quality"
     label="[% l('Set Desired Copy Quality') %]"></eg-grid-action>
-  <eg-grid-action handler="edit_pickup_lib"
+  <eg-grid-action handler="grid_actions.edit_pickup_lib"
     label="[% l('Edit Pickup Library') %]"></eg-grid-action>
-  <eg-grid-action handler="edit_notify_prefs"
+  <eg-grid-action handler="grid_actions.edit_notify_prefs"
     label="[% l('Edit Notification Settings') %]"></eg-grid-action>
-  <eg-grid-action handler="edit_dates"
+  <eg-grid-action handler="grid_actions.edit_dates"
     label="[% l('Edit Hold Dates') %]"></eg-grid-action>
-  <eg-grid-action handler="activate"
+  <eg-grid-action handler="grid_actions.activate"
     label="[% l('Activate') %]"></eg-grid-action>
-  <eg-grid-action handler="suspend"
+  <eg-grid-action handler="grid_actions.suspend"
     label="[% l('Suspend') %]"></eg-grid-action>
-  <eg-grid-action handler="set_top_of_queue"
+  <eg-grid-action handler="grid_actions.set_top_of_queue"
     label="[% l('Set Top of Queue') %]"></eg-grid-action>
-  <eg-grid-action handler="clear_top_of_queue"
+  <eg-grid-action handler="grid_actions.clear_top_of_queue"
     label="[% l('Un-Set Top of Queue') %]"></eg-grid-action>
-  <eg-grid-action handler="transfer_to_marked_title"
+  <eg-grid-action handler="grid_actions.transfer_to_marked_title"
     label="[% l('Transfer To Marked Title') %]"></eg-grid-action>
-  <eg-grid-action handler="mark_damaged"
+  <eg-grid-action handler="grid_actions.mark_damaged"
     label="[% l('Mark Item Damaged') %]"></eg-grid-action>
-  <eg-grid-action handler="mark_missing"
+  <eg-grid-action handler="grid_actions.mark_missing"
     label="[% l('Mark Item Missing') %]"></eg-grid-action>
   <eg-grid-action divider="true"></eg-grid-action>
-  <eg-grid-action handler="retarget"
+  <eg-grid-action handler="grid_actions.retarget"
     label="[% l('Find Another Target') %]"></eg-grid-action>
-  <eg-grid-action handler="cancel_hold"
+  <eg-grid-action handler="grid_actions.cancel_hold"
     label="[% l('Cancel Hold') %]"></eg-grid-action>
 
   <eg-grid-field label="[% l('Hold ID') %]" path='hold.id'></eg-grid-field>
index 6e0d065..d3bbae0 100644 (file)
@@ -7,35 +7,35 @@
   <eg-grid-menu-item handler="detail_view" 
     label="[% l('Detail View') %]"></eg-grid-menu-item>
 
-  <eg-grid-action handler="show_recent_circs"
+  <eg-grid-action handler="grid_actions.show_recent_circs"
     label="[% l('Show Last Few Circulations') %]"></eg-grid-action>
   <eg-grid-action divider="true"></eg-grid-action>
-  <eg-grid-action handler="set_copy_quality"
+  <eg-grid-action handler="grid_actions.set_copy_quality"
     label="[% l('Set Desired Copy Quality') %]"></eg-grid-action>
-  <eg-grid-action handler="edit_pickup_lib"
+  <eg-grid-action handler="grid_actions.edit_pickup_lib"
     label="[% l('Edit Pickup Library') %]"></eg-grid-action>
-  <eg-grid-action handler="edit_notify_prefs"
+  <eg-grid-action handler="grid_actions.edit_notify_prefs"
     label="[% l('Edit Notification Settings') %]"></eg-grid-action>
-  <eg-grid-action handler="edit_dates"
+  <eg-grid-action handler="grid_actions.edit_dates"
     label="[% l('Edit Hold Dates') %]"></eg-grid-action>
-  <eg-grid-action handler="activate"
+  <eg-grid-action handler="grid_actions.activate"
     label="[% l('Activate') %]"></eg-grid-action>
-  <eg-grid-action handler="suspend"
+  <eg-grid-action handler="grid_actions.suspend"
     label="[% l('Suspend') %]"></eg-grid-action>
-  <eg-grid-action handler="set_top_of_queue"
+  <eg-grid-action handler="grid_actions.set_top_of_queue"
     label="[% l('Set Top of Queue') %]"></eg-grid-action>
-  <eg-grid-action handler="clear_top_of_queue"
+  <eg-grid-action handler="grid_actions.clear_top_of_queue"
     label="[% l('Un-Set Top of Queue') %]"></eg-grid-action>
-  <eg-grid-action handler="transfer_to_marked_title"
+  <eg-grid-action handler="grid_actions.transfer_to_marked_title"
     label="[% l('Transfer To Marked Title') %]"></eg-grid-action>
-  <eg-grid-action handler="mark_damaged"
+  <eg-grid-action handler="grid_actions.mark_damaged"
     label="[% l('Mark Item Damaged') %]"></eg-grid-action>
-  <eg-grid-action handler="mark_missing"
+  <eg-grid-action handler="grid_actions.mark_missing"
     label="[% l('Mark Item Missing') %]"></eg-grid-action>
   <eg-grid-action divider="true"></eg-grid-action>
-  <eg-grid-action handler="retarget"
+  <eg-grid-action handler="grid_actions.retarget"
     label="[% l('Find Another Target') %]"></eg-grid-action>
-  <eg-grid-action handler="cancel_hold"
+  <eg-grid-action handler="grid_actions.cancel_hold"
     label="[% l('Cancel Hold') %]"></eg-grid-action>
 
   <eg-grid-field label="[% l('Hold ID') %]" path='hold.id'></eg-grid-field>
index 917672f..3ce1a65 100644 (file)
             </a>
           </li>
           <li>
+            <a href="./circ/holds/pull" target="_self">
+              <span class="glyphicon glyphicon-th-list"></span>
+              [% l('Pull List for Hold Requests') %]
+            </a>
+          </li>
+          <li>
             <a href="./circ/renew/renew" target="_self">
               <span class="glyphicon glyphicon-refresh"></span>
               [% l('Renew Items') %]
index 20915c7..bf0bc5b 100644 (file)
@@ -20,18 +20,37 @@ angular.module('egHoldsApp',
         resolve : resolver
     });
 
+    $routeProvider.when('/circ/holds/pull', {
+        templateUrl: './circ/holds/t_pull',
+        controller: 'HoldsPullListCtrl',
+        resolve : resolver
+    });
+
+    $routeProvider.when('/circ/holds/pull/:hold_id', {
+        templateUrl: './circ/holds/t_pull',
+        controller: 'HoldsPullListCtrl',
+        resolve : resolver
+    });
+
     $routeProvider.otherwise({redirectTo : '/circ/holds/shelf'});
 })
 
+.factory('holdUiSvc', function() {
+    return {
+        holds : [] // cache
+    }
+})
+
 .controller('HoldsShelfCtrl',
-       ['$scope','$q','$routeParams','$window','$location','egCore','egHolds','egCirc','egGridDataProvider',
-function($scope , $q , $routeParams , $window , $location , egCore , egHolds , egCirc , egGridDataProvider)  {
+       ['$scope','$q','$routeParams','$window','$location','egCore','egHolds','egHoldGridActions','egCirc','egGridDataProvider',
+function($scope , $q , $routeParams , $window , $location , egCore , egHolds , egHoldGridActions , egCirc , egGridDataProvider)  {
     $scope.detail_hold_id = $routeParams.hold_id;
 
     var hold_ids = [];
     var holds = [];
     var clear_mode = false;
     $scope.gridControls = {};
+    $scope.grid_actions = egHoldGridActions;
 
     function fetch_holds(offset, count) {
         var ids = hold_ids.slice(offset, offset + count);
@@ -46,6 +65,14 @@ function($scope , $q , $routeParams , $window , $location , egCore , egHolds , e
     var provider = egGridDataProvider.instance({});
     $scope.gridDataProvider = provider;
 
+    function refresh_page() {
+        holds = [];
+        hold_ids = [];
+        provider.refresh();
+    }
+    // called after any egHoldGridActions action occurs
+    $scope.grid_actions.refresh = refresh_page;
+
     provider.get = function(offset, count) {
 
         // see if we have the requested range cached
@@ -84,13 +111,6 @@ function($scope , $q , $routeParams , $window , $location , egCore , egHolds , e
         return deferred.promise;
     }
 
-    function refresh_page() {
-        holds = [];
-        hold_ids = [];
-        provider.refresh();
-    }
-
-
     // re-draw the grid when user changes the org selector
     $scope.pickup_ou = egCore.org.get(egCore.auth.user().ws_ou());
     $scope.$watch('pickup_ou', function(newVal, oldVal) {
@@ -123,74 +143,6 @@ function($scope , $q , $routeParams , $window , $location , egCore , egHolds , e
     $scope.show_active = function() { clear_mode = false; refresh_page() }
     $scope.disable_clear = function() { return clearing || !clear_mode }
 
-    // action handlers
-    // TODO: These are copied directly from patron/holds.js.  
-    // Consider refactoring / consolidating.
-    $scope.cancel_hold = function(items) {
-        var hold_ids = items.filter(function(item) {
-            return !item.hold.cancel_time();
-        }).map(function(item) {return item.hold.id()});
-
-        return egHolds.cancel_holds(hold_ids).then(refresh_page);
-    }
-
-    // jump to circ list for either 1) the targeted copy or
-    // 2) the hold target copy for copy-level holds
-    $scope.show_recent_circs = function(items) {
-        if (items.length && (copy = items[0].copy)) {
-            var url = $location.path(
-                '/cat/item/' + copy.id() + '/circ_list').absUrl();
-            $window.open(url, '_blank').focus();
-        }
-    }
-
-    function generic_update(items, action) {
-        if (!items.length) return $q.when();
-        var hold_ids = items.map(function(item) {return item.hold.id()});
-        return egHolds[action](hold_ids).then(refresh_page);
-    }
-
-    $scope.set_copy_quality = function(items) {
-        generic_update(items, 'set_copy_quality'); }
-    $scope.edit_pickup_lib = function(items) {
-        generic_update(items, 'edit_pickup_lib'); }
-    $scope.edit_notify_prefs = function(items) {
-        generic_update(items, 'edit_notify_prefs'); }
-    $scope.edit_dates = function(items) {
-        generic_update(items, 'edit_dates'); }
-    $scope.suspend = function(items) {
-        generic_update(items, 'suspend_holds'); }
-    $scope.activate = function(items) {
-        generic_update(items, 'activate_holds'); }
-    $scope.set_top_of_queue = function(items) {
-        generic_update(items, 'set_top_of_queue'); }
-    $scope.clear_top_of_queue = function(items) {
-        generic_update(items, 'clear_top_of_queue'); }
-    $scope.transfer_to_marked_title = function(items) {
-        generic_update(items, 'transfer_to_marked_title'); }
-
-    $scope.mark_damaged = function(items) {
-        var copy_ids = items
-            .filter(function(item) { return Boolean(item.copy) })
-            .map(function(item) { return item.copy.id() });
-        if (copy_ids.length) 
-            egCirc.mark_damaged(copy_ids).then(refresh_page);
-    }
-
-    $scope.mark_missing = function(items) {
-        var copy_ids = items
-            .filter(function(item) { return Boolean(item.copy) })
-            .map(function(item) { return item.copy.id() });
-        if (copy_ids.length) 
-            egCirc.mark_missing(copy_ids).then(refresh_page);
-    }
-
-    $scope.retarget = function(items) {
-        var hold_ids = items.map(function(item) { return item.hold.id() });
-        egHolds.retarget(hold_ids).then(refresh_page);
-    }
-
-
     // udpate the in-grid hold with the clear-shelf cached response info.
     function handle_clear_cache_resp(resp) {
         if (!angular.isArray(resp)) resp = [resp];
@@ -252,5 +204,63 @@ function($scope , $q , $routeParams , $window , $location , egCore , egHolds , e
 
     refresh_page();
 
-}]);
+}])
+
+.controller('HoldsPullListCtrl',
+       ['$scope','$q','$routeParams','$window','$location','egCore','egHolds','egCirc','egGridDataProvider','egHoldGridActions','holdUiSvc',
+function($scope , $q , $routeParams , $window , $location , egCore , egHolds , egCirc , egGridDataProvider , egHoldGridActions , holdUiSvc)  {
+    $scope.detail_hold_id = $routeParams.hold_id;
+
+    var provider = egGridDataProvider.instance({});
+    $scope.gridDataProvider = provider;
+
+    $scope.grid_actions = egHoldGridActions;
+    $scope.grid_actions.refresh = function() {
+        holdUiSvc.holds = [];
+        provider.refresh();
+    }
+
+    provider.get = function(offset, count) {
+
+        if (holdUiSvc.holds[offset]) {
+            return provider.arrayNotifier(holdUiSvc.holds, offset, count);
+        }
+
+        var deferred = $q.defer();
+        var recv_index = 0;
+
+        // fetch the IDs
+        egCore.net.request(
+            'open-ils.circ',
+            'open-ils.circ.hold_pull_list.fleshed.stream',
+            egCore.auth.token(), count, offset
+        ).then(
+            deferred.resolve, null, 
+            function(hold_data) {
+                egHolds.local_flesh(hold_data);
+                holdUiSvc.holds[offset + recv_index++] = hold_data;
+                deferred.notify(hold_data);
+            }
+        );
+
+        return deferred.promise;
+    }
+
+    $scope.detail_view = function(action, user_data, items) {
+        if (h = items[0]) {
+            $location.path('/circ/holds/pull/' + h.hold.id());
+        }
+    }
+
+    $scope.list_view = function(items) {
+        $location.path('/circ/holds/pull');
+    }
+
+    // when the detail hold is fetched (and updated), update the bib
+    // record summary display record id.
+    $scope.set_hold = function(hold_data) {
+        $scope.detail_hold_record_id = hold_data.mvr.doc_id();
+    }
+
+}])
 
index 9933c42..d49c15a 100644 (file)
@@ -5,13 +5,14 @@
 angular.module('egPatronApp').controller('PatronHoldsCtrl',
 
        ['$scope','$q','$routeParams','egCore','egUser','patronSvc',
-        'egGridDataProvider','egHolds','$window','$location','egCirc',
+        'egGridDataProvider','egHolds','$window','$location','egCirc','egHoldGridActions',
 function($scope,  $q,  $routeParams,  egCore,  egUser,  patronSvc,  
-        egGridDataProvider , egHolds , $window , $location , egCirc) {
+        egGridDataProvider , egHolds , $window , $location , egCirc, egHoldGridActions) {
 
     $scope.initTab('holds', $routeParams.id);
     $scope.holds_display = 'main';
     $scope.detail_hold_id = $routeParams.hold_id;
+    $scope.grid_actions = egHoldGridActions;
 
     function refresh_all() {
         patronSvc.refreshPrimary();
@@ -19,6 +20,7 @@ function($scope,  $q,  $routeParams,  egCore,  egUser,  patronSvc,
         patronSvc.hold_ids = [];
         provider.refresh() 
     }
+    $scope.grid_actions.refresh = refresh_all;
 
     $scope.show_main_list = function() {
         // don't need a full reset_page() to swap tabs
@@ -41,10 +43,11 @@ function($scope,  $q,  $routeParams,  egCore,  egUser,  patronSvc,
 
     function fetchHolds(offset, count) {
         var ids = patronSvc.hold_ids.slice(offset, offset + count);
-        return egHolds.fetch_holds(ids).then(
-            function() { $scope.loading = false; },
-            null,
-            function(hold_data) { patronSvc.holds.push(hold_data) }
+        return egHolds.fetch_holds(ids).then(null, null,
+            function(hold_data) { 
+                patronSvc.holds.push(hold_data);
+                return hold_data;
+            }
         );
     }
 
@@ -82,69 +85,6 @@ function($scope,  $q,  $routeParams,  egCore,  egUser,  patronSvc,
         return deferred.promise;
     }
 
-    $scope.cancel_hold = function(items) {
-        var hold_ids = items.filter(function(item) {
-            return !item.hold.cancel_time();
-        }).map(function(item) {return item.hold.id()});
-
-        return egHolds.cancel_holds(hold_ids).then(refresh_all);
-    }
-
-    // jump to circ list for either 1) the targeted copy or
-    // 2) the hold target copy for copy-level holds
-    $scope.show_recent_circs = function(items) {
-        if (items.length && (copy = items[0].copy)) {
-            var url = $location.path(
-                '/cat/item/' + copy.id() + '/circ_list').absUrl();
-            $window.open(url, '_blank').focus();
-        }
-    }
-
-    function generic_update(items, action) {
-        if (!items.length) return $q.when();
-        var hold_ids = items.map(function(item) {return item.hold.id()});
-        return egHolds[action](hold_ids).then(refresh_all);
-    }
-
-    $scope.set_copy_quality = function(items) {
-        generic_update(items, 'set_copy_quality'); }
-    $scope.edit_pickup_lib = function(items) {
-        generic_update(items, 'edit_pickup_lib'); }
-    $scope.edit_notify_prefs = function(items) {
-        generic_update(items, 'edit_notify_prefs'); }
-    $scope.edit_dates = function(items) {
-        generic_update(items, 'edit_dates'); }
-    $scope.suspend = function(items) {
-        generic_update(items, 'suspend_holds'); }
-    $scope.activate = function(items) {
-        generic_update(items, 'activate_holds'); }
-    $scope.set_top_of_queue = function(items) {
-        generic_update(items, 'set_top_of_queue'); }
-    $scope.clear_top_of_queue = function(items) {
-        generic_update(items, 'clear_top_of_queue'); }
-    $scope.transfer_to_marked_title = function(items) {
-        generic_update(items, 'transfer_to_marked_title'); }
-
-    $scope.mark_damaged = function(items) {
-        var copy_ids = items
-            .filter(function(item) { return Boolean(item.copy) })
-            .map(function(item) { return item.copy.id() });
-        if (copy_ids.length) 
-            egCirc.mark_damaged(copy_ids).then(refresh_all);
-    }
-    $scope.mark_missing = function(items) {
-        var copy_ids = items
-            .filter(function(item) { return Boolean(item.copy) })
-            .map(function(item) { return item.copy.id() });
-        if (copy_ids.length) 
-            egCirc.mark_missing(copy_ids).then(refresh_all);
-    }
-
-    $scope.retarget = function(items) {
-        var hold_ids = items.map(function(item) { return item.hold.id() });
-        egHolds.retarget(hold_ids).then(refresh_all);
-    }
-
     $scope.print = function() {
         var holds = [];
         angular.forEach(patronSvc.holds, function(item) {
index 6ae72fd..161722c 100644 (file)
@@ -12,18 +12,47 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog , egAlertDialog)
     var service = {};
 
     service.fetch_holds = function(hold_ids) {
+        var deferred = $q.defer();
 
-        return egCore.net.request(
-            'open-ils.circ',
-            'open-ils.circ.hold.details.batch.retrieve.authoritative',
-            egCore.auth.token(), hold_ids
-
-        ).then(null, null, function(hold_data) {
-            var hold = hold_data.hold;
-            hold_data.id = hold.id();
-            service.local_flesh(hold_data);
-            return hold_data;
-        });
+        // FIXME: large batches using .authoritative result in many 
+        // stranded cstore backends on the server.  Needs investigation.
+        // For now, collect holds in a series of small batches.
+        // Fetch them serially both to avoid the above problem and
+        // to maintain order.
+        var batch_size = 5;
+        var index = 0;
+
+        function one_batch() {
+            var ids = hold_ids.slice(index, index + batch_size)
+                .filter(function(id) {return Boolean(id)}) // avoid nulls
+
+            console.debug('egHolds.fetch_holds => ' + ids);
+            index += batch_size;
+
+            if (!ids.length) {
+                deferred.resolve();
+                return;
+            }
+
+            egCore.net.request(
+                'open-ils.circ',
+                'open-ils.circ.hold.details.batch.retrieve.authoritative',
+                egCore.auth.token(), ids
+
+            ).then(
+                one_batch,  // kick off the next batch
+                null, 
+                function(hold_data) {
+                    var hold = hold_data.hold;
+                    hold_data.id = hold.id();
+                    service.local_flesh(hold_data);
+                    deferred.notify(hold_data);
+                }
+            );
+        }
+
+        one_batch(); // kick it off
+        return deferred.promise;
     }
 
 
@@ -335,6 +364,7 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog , egAlertDialog)
         var hold = hold_data.hold;
         hold.pickup_lib(egCore.org.get(hold.pickup_lib()));
         hold.current_shelf_lib(egCore.org.get(hold.current_shelf_lib()));
+        hold_data.id = hold.id();
 
         // current_copy is not always fleshed in the API
         if (hold.current_copy() && typeof hold.current_copy() != 'object')
@@ -344,6 +374,91 @@ function($modal , $q , egCore , egAlertDialog , egConfirmDialog , egAlertDialog)
     return service;
 }])
 
+/**  
+ * Action handlers for the common Hold grid UI.
+ * These generally scrub the data for valid input then pass the
+ * holds / copies / etc. off to the relevant action in egHolds or egCirc.
+ *
+ * Caller must apply a reset_page function, which is called after 
+ * most actionis are performed.
+ */
+.factory('egHoldGridActions', 
+       ['$window','$location','egCore','egHolds','egCirc',
+function($window , $location , egCore , egHolds , egCirc) {
+    
+    var service = {};
+
+    service.refresh = function() {
+        console.error('egHoldGridActions.refresh not defined!');
+    }
+
+    service.cancel_hold = function(items) {
+        var hold_ids = items.filter(function(item) {
+            return !item.hold.cancel_time();
+        }).map(function(item) {return item.hold.id()});
+
+        return egHolds.cancel_holds(hold_ids).then(service.refresh);
+    }
+
+    // jump to circ list for either 1) the targeted copy or
+    // 2) the hold target copy for copy-level holds
+    service.show_recent_circs = function(items) {
+        if (items.length && (copy = items[0].copy)) {
+            var url = $location.path(
+                '/cat/item/' + copy.id() + '/circ_list').absUrl();
+            $window.open(url, '_blank').focus();
+        }
+    }
+
+    function generic_update(items, action) {
+        if (!items.length) return $q.when();
+        var hold_ids = items.map(function(item) {return item.hold.id()});
+        return egHolds[action](hold_ids).then(service.refresh);
+    }
+
+    service.set_copy_quality = function(items) {
+        generic_update(items, 'set_copy_quality'); }
+    service.edit_pickup_lib = function(items) {
+        generic_update(items, 'edit_pickup_lib'); }
+    service.edit_notify_prefs = function(items) {
+        generic_update(items, 'edit_notify_prefs'); }
+    service.edit_dates = function(items) {
+        generic_update(items, 'edit_dates'); }
+    service.suspend = function(items) {
+        generic_update(items, 'suspend_holds'); }
+    service.activate = function(items) {
+        generic_update(items, 'activate_holds'); }
+    service.set_top_of_queue = function(items) {
+        generic_update(items, 'set_top_of_queue'); }
+    service.clear_top_of_queue = function(items) {
+        generic_update(items, 'clear_top_of_queue'); }
+    service.transfer_to_marked_title = function(items) {
+        generic_update(items, 'transfer_to_marked_title'); }
+
+    service.mark_damaged = function(items) {
+        var copy_ids = items
+            .filter(function(item) { return Boolean(item.copy) })
+            .map(function(item) { return item.copy.id() });
+        if (copy_ids.length) 
+            egCirc.mark_damaged(copy_ids).then(service.refresh);
+    }
+
+    service.mark_missing = function(items) {
+        var copy_ids = items
+            .filter(function(item) { return Boolean(item.copy) })
+            .map(function(item) { return item.copy.id() });
+        if (copy_ids.length) 
+            egCirc.mark_missing(copy_ids).then(service.refresh);
+    }
+
+    service.retarget = function(items) {
+        var hold_ids = items.map(function(item) { return item.hold.id() });
+        egHolds.retarget(hold_ids).then(service.refresh);
+    }
+
+    return service;
+}])
+
 /**
  * Hold details interface 
  */