ff ui : move to all flattener; needs more testing
authorBill Erickson <berick@esilibrary.com>
Tue, 5 Nov 2013 16:08:19 +0000 (11:08 -0500)
committerBill Erickson <berick@esilibrary.com>
Tue, 5 Nov 2013 22:03:13 +0000 (17:03 -0500)
Signed-off-by: Bill Erickson <berick@esilibrary.com>
Open-ILS/src/templates/staff/fulfillment/t_item_table.tt2
Open-ILS/src/templates/staff/fulfillment/t_pending.tt2
Open-ILS/web/js/ui/default/staff/fulfillment/app.js

index 335e130..7238ee7 100644 (file)
     <tr>
       <th><a href="javascript:;" ng-click="itemList.selectAll()">&#x2713;</a></th>
 
-      <!-- common columns -->
       <th>#</th>
+      <th ng-show="tab_pending">[% l('Request ID') %]</th>
+      <th ng-show="tab_inbound || tab_outbound">[% l('Transit ID') %]</th>
+      <th ng-show="tab_circulating">[% l('Circ ID') %]</th>
+
       <th>[% l('Item Barcode') %]</th>
-      <th>[% l('Owning Library') %]</th>
+      <th>[% l('Copy Library') %]</th>
+
+      <!-- holds columns -->
+      <th ng-show="tab_pending">[% l('Request Date') %]</th>
+      <th ng-show="tab_pending">[% l('Expire Date') %]</th>
+      <th ng-show="tab_pending">[% l('Requesting Library') %]</th>
+      <th ng-show="tab_pending">[% l('Pickup Library') %]</th>
 
       <!-- transit columns -->
       <th ng-show="tab_inbound || tab_outbound">[% l('Transit Date') %]</th>
     <tr ng-repeat="item in itemList.items">
       <td><input type='checkbox' ng-model="itemList.selected[item.index]"/></td>
       <td>{{item.index + 1}}</td>
+
+      <td ng-show="tab_pending">{{item.hold_id}}</td>
+      <td ng-show="tab_inbound || tab_outbound">{{item.transit_id}}</td>
+      <td ng-show="tab_circulating">{{item.circ_id}}</td>
+
       <td><a 
-        href="./fulfillment/status/{{item.item_barcode_enc}}">
-          {{item.item_barcode}}</a>
+        href="./fulfillment/status/{{item.copy_barcode_enc}}">
+          {{item.copy_barcode}}</a>
       </td>
-      <td>{{item.source_lib}}</td>
+      <td>{{item.copy_circ_lib}}</td>
+
+      <td ng-show="tab_pending">{{item.request_time | date}}</td>
+      <td ng-show="tab_pending">{{item.expire_time | date}}</td>
+      <td ng-show="tab_pending">{{item.request_lib}}</td>
+      <td ng-show="tab_pending">{{item.pickup_lib}}</td>
+
       <td ng-show="tab_inbound || tab_outbound">{{item.transit_time | date}}</td>
       <td ng-show="tab_inbound || tab_outbound">{{item.transit_source}}</td>
       <td ng-show="tab_inbound || tab_outbound">{{item.transit_dest}}</td>
       <td ng-show="tab_onshelf">{{item.hold_shelf_time | date}}</td>
-      <td ng-show="tab_circulating">{{item.patron_name}}</td>
-      <td ng-show="tab_circulating">{{item.patron_card}}</td>
-      <td ng-show="tab_circulating">{{item.circ_xact_start | date}}</td>
+      <td ng-show="tab_circulating">{{item.patron_given_name}} {{item.patron_family_name}}</td>
+      <td ng-show="tab_circulating">{{item.patron_barcode}}</td>
+      <td ng-show="tab_circulating">{{item.xact_start | date}}</td>
       <td ng-show="tab_circulating">{{item.due_date | date}}</td>
       <td ng-show="tab_circulating">{{item.circ_circ_lib}}</td>
       <td>{{item.title}}</td>
index 0ba6995..614a6ac 100644 (file)
@@ -21,6 +21,9 @@
 
   <br/>
 
+  <div ng-include="'./fulfillment/t_item_table'"></div>
+
+  <!--
   <div class="row" ng-show="lookupComplete && !itemList.count()">
     <div class="col-lg-10 col-lg-offset-1">
       <div class="alert alert-info">[% l('No Items To Display') %]</div>
@@ -62,4 +65,5 @@
         <td>{{item.title}}</td>
     </tbody>
   </table>
+  -->
 </div>
index f9d6db5..2766c06 100644 (file)
@@ -2,6 +2,7 @@
  * 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.
@@ -115,7 +116,6 @@ function ($scope,  $route,  egStartup,  orgSelector,  egAuth,  egUser) {
         egAuth.logout();
         return true;
     };
-
 }])
 
 
@@ -144,142 +144,119 @@ function ($scope,  $q,  $compile,  $timeout,  $rootScope, $location,
     $scope.illRouteParams = $routeParams;
 
     $scope.itemList = egList.create({limit : 10}); // limit TBD
-
-    // apply some local list additions
-    $scope.itemList.addItem = function(item) {
-        // TODO: id version
-        item.index = $scope.itemList.count()
-        $scope.itemList.items.push(item);
-        egNet.request(
-            'open-ils.circ',
-            'open-ils.circ.item.transaction.disposition',
-            egAuth.token(), 
-            orgSelector.current().id(), 
-            item.barcode
-        ).then(function(items) {
-            if (items[0]) {
-                $scope.itemList.flattenItem(item, items[0]);
-            } else {
-               $scope.itemList.items.pop().not_found = true;
-            }
-        });
-    }
-
-    // given an item disposition blob, flatten it for display
-    $scope.itemList.flattenItem = function(item, item_data) {
-        /*
-         * TODO: most of this is unnecessary, since we can access
-         * fields directly in the template.  Consider pairing
-         * this down to fields that need munging only
-         */
-        var copy = item_data.copy;
-        var transit = item_data.transit;
-        var circ = item_data.circ;
-        var hold = item_data.hold;
-        if (hold) {
-            if (!transit && hold.transit()) {
-                transit = item_data.hold.transit();
-            }
-        } else if (transit && transit.hold_transit_copy()) {
-            hold = transit.hold_transit_copy().hold();
-        }
-
-        item.copy = item_data.copy;
-        item.item_barcode = copy.barcode();
-        item.item_barcode_enc = encodeURIComponent(copy.barcode());
-        item.source_lib = egOrg.get(copy.source_lib()).shortname();
-        item.circ_lib = egOrg.get(copy.circ_lib()).shortname();
-        item.title = copy.call_number().record().simple_record().title();
-        item.author = copy.call_number().record().simple_record().author();
-        item.call_number = copy.call_number().label();
-        item.bib_id = copy.call_number().record().id();
-        item.remote_bib_id = copy.call_number().record().remote_id();
-        item.next_action = item_data.next_action;
-        item.can_cancel_hold = (item_data.can_cancel_hold == 1);
-        item.can_retarget_hold = (item_data.can_retarget_hold == 1);
-
-        switch(item_data.next_action) {
-            // capture lender copy for hold
-            case 'ill-home-capture' :
-                item.needs_capture = true;
-                break; 
-            // receive item at borrower
-            case 'ill-foreign-receive':
-            // receive lender copy back home
-            case 'transit-home-receive':
-            // transit item for cancelled hold back home (or next hold)
-            case 'transit-foreign-return':
-                item.needs_receive = true;
-                break; 
-            // complete borrower circ, transit item back home
-            case 'ill-foreign-checkin':
-                item.needs_checkin = true;
-                break;
-            // check out item to borrowing patron
-            case 'ill-foreign-checkout':
-                item.needs_checkout = true;
-                break;
-        }
-
-        item.status_str = copy.status().name();
-        if (copy.status().id() == 8 /* holds shelf */ &&
-            hold &&
-            hold.transit() &&
-            hold.transit().dest_recv_time()) {
-            item.status_str += ' @ ' + egOrg.get(hold.transit().dest()).shortname();
-        } else if (copy.status().id() == 1 /* checked out */ && circ) {
-            item.status_str += ' @ ' + egOrg.get(circ.circ_lib()).shortname();
+  
+    // map of flattener fields to retrieve for each query type
+    $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'},
+            request_lib : {path : 'request_lib.shortname'}, // TODO: causes query problem, wha?
+            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'}
+        },
+        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'}
+        },
+        atc : {
+            id : {path : 'id'},
+            transit_id : {path : 'id'},
+            hold_id : {path : 'hold_transit_copy.hold.id'},
+            request_lib : {path : 'hold_transit_copy.hold.request_lib'},
+            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'}
         }
+    };
 
-        item.copy_status_warning = (copy.status().holdable() == 'f');
+    // set display=true for each flattener field
+    angular.forEach($scope.flatFields, function(val) {
+        angular.forEach(val, function(val2) {
+            val2.display = true;
+        });
+    });
 
-        if (transit) {
-            item.transit = transit;
-            item.transit_source = transit.source().shortname();
-            item.transit_dest = transit.dest().shortname();
-            item.transit_time = transit.source_send_time();
-            item.transit_recv_time = transit.dest_recv_time();
-            item.open_transit = !Boolean(transit.dest_recv_time());
+    $scope.setCollector = function(class_, query) {
+        $scope.collector = {
+            query : query,
+            class_ : class_
         }
+    }
 
-        if (circ) {
-            item.circ = circ;
-            item.due_date = circ.due_date();
-            item.circ_circ_lib = egOrg.get(circ.circ_lib()).shortname();
-            item.circ_xact_start = circ.xact_start();
-            item.circ_stop_fines = circ.stop_fines();
-            // FF patrons will all have cards, but some test logins may not
-            item.patron_card = circ.usr().card() ? 
-                circ.usr().card().barcode() : circ.usr().usrname();
-            item.patron_name = circ.usr().first_given_name() + ' ' + circ.usr().family_name() // i18n
-            item.can_mark_lost = (item.circ && item.copy.status().id() == 1); // checked out
-        }
+    $scope.setMunger = function(func) {
+        $scope.munger = func;
+    }
 
-        if (hold) {
-            item.hold = hold;
-            item.patron_card = hold.usr().card() ? 
-                hold.usr().card().barcode() : hold.usr().usrname();
-            item.patron_name = hold.usr().first_given_name() + ' ' + hold.usr().family_name() // i18n
-            item.hold_request_lib = egOrg.get(hold.request_lib()).shortname();
-            item.hold_pickup_lib = egOrg.get(hold.pickup_lib()).shortname();
-            item.hold_request_time = hold.request_time();
-            item.hold_capture_time = hold.capture_time();
-            item.hold_shelf_time = hold.shelf_time();
-            if (hold.cancel_time()) {
-                item.hold_cancel_time = hold.cancel_time();
-                if (hold.cancel_cause()) {
-                    item.hold_cancel_cause = hold.cancel_cause().label();
+    $scope.collect = function() {
+        $scope.lookupComplete = false;
+        $scope.itemList.items = [];
+        egNet.request(
+            'open-ils.fielder',
+            'open-ils.fielder.flattened_search',
+            egAuth.token(), 
+            $scope.collector.class_,
+            $scope.flatFields[$scope.collector.class_],
+            $scope.collector.query,
+            {   sort : [$scope.itemList.sort],
+                limit : $scope.itemList.limit,
+                offset : $scope.itemList.offset
+            }
+        ).then(
+            null, // success/oncomplete
+            null, // error
+            function(item) { // notify/onresponse handler
+                $scope.lookupComplete = true;
+                item.index = $scope.itemList.count();
+                if (item.copy_barcode) {
+                    item.copy_barcode_enc = 
+                        encodeURIComponent(item.copy_barcode);
                 }
+                if ($scope.munger) $scope.munger(item);
+                $scope.itemList.items.push(item);
             }
-        }
-
-        // TODO: another unnecessary layer of data munging,
-        // this time to fit the print templates.  Can be unified.
-        item.barcode = item.item_barcode;
-        item.status = item.status_str;
-        item.item_circ_lib = item.circ_lib;
+        );
     }
 
+
     /* Actions
      * Performed on flattened items (see above)
      */
@@ -293,7 +270,7 @@ function ($scope,  $q,  $compile,  $timeout,  $rootScope, $location,
                 'open-ils.circ.checkin.override',
                 egAuth.token(), {
                     circ_lib : orgSelector.current().id(),
-                    copy_id: item.copy.id(),
+                    copy_id: item.copy_id,
                     ff_action: item.next_action
                 }
             ).then(function(response) {
@@ -321,8 +298,8 @@ function ($scope,  $q,  $compile,  $timeout,  $rootScope, $location,
                 'open-ils.circ.checkout.full.override',
                 egAuth.token(), {
                     circ_lib : orgSelector.current().id(),
-                    patron_id : item.hold.usr().id(),
-                    copy_id: item.copy.id(),
+                    patron_id : item.user_id,
+                    copy_id: item.copy_id,
                     ff_action: item.next_action
                 }
             ).then(function(response) {
@@ -348,7 +325,7 @@ function ($scope,  $q,  $compile,  $timeout,  $rootScope, $location,
             $scope.action_pending = true;
             egNet.request(
                 'open-ils.circ', 'open-ils.circ.hold.cancel',
-                egAuth.token(), item.hold.id()
+                egAuth.token(), item.hold_id
             ).then(function() {
                 $scope.action_pending = false;
                 deferred.resolve();
@@ -361,7 +338,7 @@ function ($scope,  $q,  $compile,  $timeout,  $rootScope, $location,
             $scope.action_pending = true;
             egNet.request(
               'open-ils.circ', 'open-ils.circ.hold.reset',
-              egAuth.token(), item.hold.id()
+              egAuth.token(), item.hold_id
             ).then(function() {
                 $scope.action_pending = false;
                 deferred.resolve();
@@ -374,7 +351,7 @@ function ($scope,  $q,  $compile,  $timeout,  $rootScope, $location,
             $scope.action_pending = true;
             egNet.request(
                 'open-ils.circ', 'open-ils.circ.transit.abort',
-                egAuth.token(), {transitid : item.transit.id()}
+                egAuth.token(), {transitid : item.transit_id}
             ).then(function() {
                 $scope.action_pending = false;
                 deferred.resolve();
@@ -407,6 +384,12 @@ function ($scope,  $q,  $compile,  $timeout,  $rootScope, $location,
                 (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;
+
             egNet.request(
                 'open-ils.actor',
                 'open-ils.actor.web_action_print_template.fetch',
@@ -452,7 +435,23 @@ function ($scope,  $q,  $compile,  $timeout,  $rootScope, $location,
     };
 
     // default batch action handlers.
-    // when a batch is done, reload the route (unless printing).
+    // when a batch is done, reload the route, unless printing.
+
+    function performAction(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);
+            }
+        );
+    }
+
     angular.forEach(
         Object.keys($scope.actions),
         function(action) {
@@ -462,228 +461,316 @@ function ($scope,  $q,  $compile,  $timeout,  $rootScope, $location,
                     function(val, idx) {
                         var item = $scope.itemList.items.filter(
                             function(i) {return i.index == idx})[0];
-                        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);
-                            }
-                        );
+                        performAction(action, item)
                     }
                 );
             };
         }
     );
 
-    // data collection and pagination
-    
-    // collector function collects the list of items
-    // and calls itemList.addItem for each one.
-    $scope.setCollector = function(colFn) {
-        $scope.collector = colFn;
-    }
-
     $scope.firstPage = function() {
         $scope.itemList.offset = 0;
-        $scope.collector();
+        $scope.collect();
     };
 
     $scope.nextPage = function() {
         $scope.itemList.incrementPage();
-        $scope.collector();
+        $scope.collect();
     };
 
     $scope.prevPage = function() {
         $scope.itemList.decrementPage();
-        $scope.collector();
+        $scope.collect();
     };
 
 }])
 
-.controller('TransitsCtrl',
-        ['$scope','$q','egPCRUD','orgSelector',
-function ($scope,  $q,  egPCRUD,  orgSelector) {
+.controller('PendingRequestsCtrl',
+        ['$scope','$q','$route','egNet','egAuth','egPCRUD','egOrg','orgSelector',
+function ($scope,  $q,  $route,  egNet,  egAuth,  egPCRUD,  egOrg,  orgSelector) {
 
-    $scope.setCollector(function() {
-        $scope.itemList.items = [];
-        $scope.lookupComplete = false;
+    $scope.itemList.sort = 'request_time';
 
-        var fullPath = orgSelector.relatedOrgs();
+    var fullPath = orgSelector.relatedOrgs();
 
-        var dest = fullPath; // inbound transits
-        var circ_lib = fullPath; // our copies
+    var query = {   
+        capture_time : null, 
+        cancel_time : null, 
+        frozen : 'f'
+    };
 
-        if ($scope.orientation_borrower) {
-            // borrower always means not-our-copies
-            circ_lib = {'not in' : fullPath};
-        }
-        if ($scope.tab_outbound) {
-            // outbound transits away from "here"
-            dest = {'not in' : fullPath};
+    if ($scope.orientation_borrower) {
+        // holds for my patrons originate "here"
+        // current_copy is not relevant
+        query.request_lib = fullPath;
+    } else {
+        // holds for other originate from not-"here" and
+        // have a current copy at "here".
+        query.request_lib = {'not in' : fullPath};
+        query.current_copy = {
+            "in" : {
+                select: {acp : ['id']},
+                from : 'acp',
+                where: {
+                    deleted : 'f',
+                    circ_lib : fullPath,
+                    id : {'=' : {'+ahr' : 'current_copy'}}
+                }
+            }
         }
-            
-        var query = {
-            dest_recv_time : null,
-            dest : dest,
-            target_copy : {
-                'in' : {
-                    select: {acp : ['id']},
-                    from : 'acp',
-                    where : {
-                        deleted : 'f',
-                        id : {'=' : {'+atc' : 'target_copy'}},
-                        circ_lib : circ_lib
-                    }
+    }
+
+    $scope.setCollector('ahr', query);
+    $scope.setMunger(function(item) {
+        if ($scope.orientation_lender) 
+            item.next_action = 'ill-home-capture';
+    });
+
+    $scope.collect();
+}])
+
+
+.controller('TransitsCtrl',
+        ['$scope','$q','egPCRUD','orgSelector',
+function ($scope,  $q,  egPCRUD,  orgSelector) {
+
+    $scope.itemList.sort = 'transit_time';
+    var fullPath = orgSelector.relatedOrgs();
+    var dest = fullPath; // inbound transits
+    var circ_lib = fullPath; // our copies
+
+    if ($scope.orientation_borrower) {
+        // borrower always means not-our-copies
+        circ_lib = {'not in' : fullPath};
+    }
+    if ($scope.tab_outbound) {
+        // outbound transits away from "here"
+        dest = {'not in' : fullPath};
+    }
+        
+    var query = {
+        dest_recv_time : null,
+        dest : dest,
+        target_copy : {
+            'in' : {
+                select: {acp : ['id']},
+                from : 'acp',
+                where : {
+                    deleted : 'f',
+                    id : {'=' : {'+atc' : 'target_copy'}},
+                    circ_lib : circ_lib
                 }
             }
-        };
-
-        return egPCRUD.search('atc', query,
-            {   limit : $scope.itemList.limit,
-                offset : $scope.itemList.offset,
-                flesh : 1, 
-                flesh_fields : {atc : ['target_copy']},
-                order_by : {'atc' : 'source_send_time, id'}
-            }, {atomic : true}
-        ).then(function(transits) {
-            $scope.lookupComplete = true;
-            angular.forEach(transits, function(transit) {
-                $scope.itemList.addItem(
-                  {barcode : transit.target_copy().barcode()});
-            });
-        });
+        }
+    };
+
+    $scope.setCollector('atc', query);
+    $scope.setMunger(function(item) {
+        if ($scope.tab_inbound) {
+            if ($scope.orientation_borrower) {
+                item.next_action = 'ill-foreign-receive';
+            } else {
+                item.next_action = 'transit-home-receive';
+            }
+        } 
     });
-    
-    return $scope.collector();
+
+    return $scope.collect();
 }])
 
 .controller('OnShelfCtrl',
         ['$scope','$q','egPCRUD','orgSelector',
 function ($scope,  $q,  egPCRUD,  orgSelector) {
 
-    $scope.setCollector(function() {
-        $scope.itemList.items = [];
-        $scope.lookupComplete = false;
+    $scope.itemList.sort = 'shelf_time';
 
-        var fullPath = orgSelector.relatedOrgs();
+    var fullPath = orgSelector.relatedOrgs();
 
-        var copy_lib = {'not in' : fullPath}; // not our copy
-        var shelf_lib = fullPath; // on our shelf
+    var copy_lib = {'not in' : fullPath}; // not our copy
+    var shelf_lib = fullPath; // on our shelf
 
-        if ($scope.orientation_lender) {
-            shelf_lib = {'not in' : fullPath};
-            copy_lib = fullPath;
-        }
-            
-        var query = {
-            frozen : 'f',
-            cancel_time : null,
-            fulfillment_time : null,
-            shelf_time : {'!=' : null},
-            current_shelf_lib : shelf_lib,
-            current_copy : {
-                'in' : {
-                    select: {acp : ['id']},
-                    from : 'acp',
-                    where : {
-                        deleted : 'f',
-                        id : {'=' : {'+ahr' : 'current_copy'}},
-                        circ_lib : copy_lib,
-                        status : 8 // On Holds Shelf
-                    }
+    if ($scope.orientation_lender) {
+        shelf_lib = {'not in' : fullPath};
+        copy_lib = fullPath;
+    }
+        
+    var query = {
+        frozen : 'f',
+        cancel_time : null,
+        fulfillment_time : null,
+        shelf_time : {'!=' : null},
+        current_shelf_lib : shelf_lib,
+        current_copy : {
+            'in' : {
+                select: {acp : ['id']},
+                from : 'acp',
+                where : {
+                    deleted : 'f',
+                    id : {'=' : {'+ahr' : 'current_copy'}},
+                    circ_lib : copy_lib,
+                    status : 8 // On Holds Shelf
                 }
             }
-        };
-
-        return egPCRUD.search('ahr', query,
-            {   limit : $scope.itemList.limit,
-                offset : $scope.itemList.offset,
-                flesh : 1, 
-                flesh_fields : {ahr : ['current_copy']},
-                order_by : {ahr : 'request_time, id'}
-            }, {atomic : true}
-        ).then(function(holds) {
-            $scope.lookupComplete = true;
-            angular.forEach(holds, function(hold) {
-                $scope.itemList.addItem(
-                  {barcode : hold.current_copy().barcode()});
-            });
-        });
+        }
+    };
+
+    $scope.setCollector('ahr', query);
+    $scope.setMunger(function(item) {
+        if ($scope.orientation_borrower) {
+            item.next_action = 'ill-foreign-checkout';
+        }
     });
-    
-    // outbound tab defaults to lender view
-    return $scope.collector();
-}])
 
+    return $scope.collect();
+}])
 
 
 .controller('CircCtrl',
-        ['$scope','$q','egPCRUD','orgSelector',
-function ($scope,  $q,  egPCRUD,  orgSelector) {
+        ['$scope','$q','egPCRUD','orgSelector','egNet','egAuth',
+function ($scope,  $q,  egPCRUD,  orgSelector,  egNet,  egAuth) {
 
-    $scope.setCollector(function() {
-        $scope.itemList.items = [];
-        $scope.lookupComplete = false;
+    $scope.itemList.sort = 'xact_start';
 
-        var fullPath = orgSelector.relatedOrgs();
+    var fullPath = orgSelector.relatedOrgs();
 
-        var copy_lib = fullPath; // our copies
-        var circ_lib = fullPath; // circulating here
+    var copy_circ_lib = fullPath; // our copies
+    var circ_circ_lib = fullPath; // circulating here
 
-        if ($scope.orientation_lender) {
-            // borrower always means not-our-copies
-            circ_lib = {'not in' : fullPath};
-        } else {
-            copy_lib = {'not in' : fullPath};
-        }
-            
-        var query = {
-            checkin_time : null,
-            circ_lib : circ_lib,
-            target_copy : {
-                'in' : {
-                    select: {acp : ['id']},
-                    from : 'acp',
-                    where : {
-                        deleted : 'f',
-                        id : {'=' : {'+circ' : 'target_copy'}},
-                        circ_lib : copy_lib
-                    }
+    if ($scope.orientation_lender) {
+        // circulating elsewhere
+        circ_circ_lib = {'not in' : fullPath};
+    } else {
+        // borrower always means not-our-copies
+        copy_circ_lib = {'not in' : fullPath};
+    }
+        
+    var query = {
+        checkin_time : null,
+        circ_lib : circ_circ_lib,
+        target_copy : {
+            'in' : {
+                select: {acp : ['id']},
+                from : 'acp',
+                where : {
+                    deleted : 'f',
+                    id : {'=' : {'+circ' : 'target_copy'}},
+                    circ_lib : copy_circ_lib
                 }
             }
-        };
-
-        return egPCRUD.search('circ', query,
-            {   limit : $scope.itemList.limit,
-                offset : $scope.itemList.offset,
-                flesh : 1, 
-                flesh_fields : {circ : ['target_copy']},
-                order_by : {'circ' : 'xact_start, id'}
-            }, {atomic : true}
-        ).then(function(circs) {
-            $scope.lookupComplete = true;
-            angular.forEach(circs, function(circ) {
-                $scope.itemList.addItem(
-                  {barcode : circ.target_copy().barcode()});
-            });
-        });
+        }
+    };
+
+    $scope.setCollector('circ', query);
+    $scope.setMunger(function(item) {
+        if ($scope.orientation_borrower) {
+            item.next_action = 'ill-foreign-checkin';
+        }
     });
-    
-    // outbound tab defaults to lender view
-    return $scope.collector();
+
+    return $scope.collect();
 }])
 
 
 .controller('ItemStatusCtrl',
-        ['$scope','$q','$route','$location','egPCRUD','orgSelector',
-function ($scope,  $q,  $route,  $location,  egPCRUD,  orgSelector) {
+        ['$scope','$q','$route','$location','egPCRUD','orgSelector','egNet','egAuth','egOrg',
+function ($scope,  $q,  $route,  $location,  egPCRUD,  orgSelector,  egNet,  egAuth,  egOrg) {
     $scope.focusMe = true;
 
+    // TODO: can we trim this down?
+    function flattenItem(item, item_data) {
+        var copy = item_data.copy;
+        var transit = item_data.transit;
+        var circ = item_data.circ;
+        var hold = item_data.hold;
+        if (hold) {
+            if (!transit && hold.transit()) {
+                transit = item_data.hold.transit();
+            }
+        } else if (transit && transit.hold_transit_copy()) {
+            hold = transit.hold_transit_copy().hold();
+        }
+
+        item.copy = item_data.copy;
+        item.item_barcode = copy.barcode();
+        item.item_barcode_enc = encodeURIComponent(copy.barcode());
+        item.source_lib = egOrg.get(copy.source_lib()).shortname();
+        item.circ_lib = egOrg.get(copy.circ_lib()).shortname();
+        item.title = copy.call_number().record().simple_record().title();
+        item.author = copy.call_number().record().simple_record().author();
+        item.call_number = copy.call_number().label();
+        item.bib_id = copy.call_number().record().id();
+        item.remote_bib_id = copy.call_number().record().remote_id();
+        item.next_action = item_data.next_action;
+        item.can_cancel_hold = (item_data.can_cancel_hold == 1);
+        item.can_retarget_hold = (item_data.can_retarget_hold == 1);
+
+        switch(item_data.next_action) {
+            // capture lender copy for hold
+            case 'ill-home-capture' :
+                item.needs_capture = true;
+                break; 
+            // receive item at borrower
+            case 'ill-foreign-receive':
+            // receive lender copy back home
+            case 'transit-home-receive':
+            // transit item for cancelled hold back home (or next hold)
+            case 'transit-foreign-return':
+                item.needs_receive = true;
+                break; 
+            // complete borrower circ, transit item back home
+            case 'ill-foreign-checkin':
+                item.needs_checkin = true;
+                break;
+            // check out item to borrowing patron
+            case 'ill-foreign-checkout':
+                item.needs_checkout = true;
+                break;
+        }
+
+        item.status_str = copy.status().name();
+        item.copy_status_warning = (copy.status().holdable() == 'f');
+
+        if (transit) {
+            item.transit = transit;
+            item.transit_source = transit.source().shortname();
+            item.transit_dest = transit.dest().shortname();
+            item.transit_time = transit.source_send_time();
+            item.transit_recv_time = transit.dest_recv_time();
+            item.open_transit = !Boolean(transit.dest_recv_time());
+        }
+
+        if (circ) {
+            item.circ = circ;
+            item.due_date = circ.due_date();
+            item.circ_circ_lib = egOrg.get(circ.circ_lib()).shortname();
+            item.circ_xact_start = circ.xact_start();
+            item.circ_stop_fines = circ.stop_fines();
+            // FF patrons will all have cards, but some test logins may not
+            item.patron_card = circ.usr().card() ? 
+                circ.usr().card().barcode() : circ.usr().usrname();
+            item.patron_name = circ.usr().first_given_name() + ' ' + circ.usr().family_name() // i18n
+            item.can_mark_lost = (item.circ && item.copy.status().id() == 1); // checked out
+        }
+
+        if (hold) {
+            item.hold = hold;
+            item.patron_card = hold.usr().card() ? 
+                hold.usr().card().barcode() : hold.usr().usrname();
+            item.patron_name = hold.usr().first_given_name() + ' ' + hold.usr().family_name() // i18n
+            item.hold_request_lib = egOrg.get(hold.request_lib()).shortname();
+            item.hold_pickup_lib = egOrg.get(hold.pickup_lib()).shortname();
+            item.hold_request_time = hold.request_time();
+            item.hold_capture_time = hold.capture_time();
+            item.hold_shelf_time = hold.shelf_time();
+            if (hold.cancel_time()) {
+                item.hold_cancel_time = hold.cancel_time();
+                if (hold.cancel_cause()) {
+                    item.hold_cancel_cause = hold.cancel_cause().label();
+                }
+            }
+        }
+    }
+
     $scope.draw = function(barcode) {
         if ($scope.illRouteParams.barcode != barcode) {
             // keep the scan box and URL in sync
@@ -691,9 +778,20 @@ function ($scope,  $q,  $route,  $location,  egPCRUD,  orgSelector) {
               encodeURIComponent(barcode));
         } else {
             $scope.itemList.items = [];
-            $scope.item = {barcode : barcode};
-            $scope.itemList.addItem($scope.item);
+            $scope.item = {index : 0, barcode : barcode};
             $scope.selectMe = true;
+            $scope.itemList.items.push($scope.item);
+            egNet.request(
+                'open-ils.circ',
+                'open-ils.circ.item.transaction.disposition',
+                egAuth.token(), orgSelector.current().id(), barcode
+            ).then(function(items) {
+                if (items[0]) {
+                    flattenItem($scope.item, items[0]);
+                } else {
+                   $scope.itemList.items.pop().not_found = true;
+                }
+            });
         }
     }
 
@@ -717,122 +815,6 @@ function ($scope,  $q,  $route,  $location,  egPCRUD,  orgSelector) {
     }
 }])
 
-
-
-/**
- * Table of pending requests 
- * This is the only table based purely on holds instead of items,
- * so the data is managed a little differently.
- */
-.controller('PendingRequestsCtrl',
-        ['$scope','$q','$route','egNet','egAuth','egPCRUD','egOrg','orgSelector',
-function ($scope,  $q,  $route,  egNet,  egAuth,  egPCRUD,  egOrg,  orgSelector) {
-
-    $scope.itemList.sort = 'request_time';
-
-    $scope.drawTable = function() {
-        $scope.itemList.items = [];
-        $scope.lookupComplete = false;
-
-        var fullPath = orgSelector.relatedOrgs();
-
-        var query = {   
-            capture_time : null, 
-            cancel_time : null, 
-            frozen : 'f'
-        };
-
-        if ($scope.orientation_borrower) {
-            // holds for my patrons originate "here"
-            // current_copy is not relevant
-            query.request_lib = fullPath;
-        } else {
-            // holds for other originate from not-"here" and
-            // have a current copy at "here".
-            query.request_lib = {'not in' : fullPath};
-            query.current_copy = {
-                "in" : {
-                    select: {acp : ['id']},
-                    from : 'acp',
-                    where: {
-                        deleted : 'f',
-                        circ_lib : fullPath,
-                        id : {'=' : {'+ahr' : 'current_copy'}}
-                    }
-                }
-            }
-        }
-
-        // circ_lib and request_lib are retrieved as IDs since 
-        // our query blob assumes they are IDs
-        var fields = {
-            id : {path : 'id'},
-            request_time : {path : 'request_time'},
-            expire_time : {path : 'expire_time'},
-            patron_barcode : {path : 'usr.card.barcode'},
-            patron_given_name : {path : 'usr.first_given_name'},
-            patron_family_name : {path : 'usr.family_name'},
-            request_lib : {path : 'request_lib'},
-            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_barcode : {path : 'current_copy.barcode'},
-            circ_lib : {path : 'current_copy.circ_lib'},
-            call_number : {path : 'current_copy.call_number.label'}
-        };
-        angular.forEach(fields, function(f) { f.display = true });
-
-        egNet.request(
-            'open-ils.fielder',
-            'open-ils.fielder.flattened_search',
-            egAuth.token(), "ahr", fields,
-            query,
-            {   sort : [$scope.itemList.sort],
-                limit : $scope.itemList.limit,
-                offset : $scope.itemList.offset
-            }
-        ).then(
-            null, // success
-            null, // error
-            function(hold) { // notify handler
-                $scope.lookupComplete = true;
-                hold.index = $scope.itemList.count();
-
-                hold.request_lib = egOrg.get(hold.request_lib).shortname();
-                if (hold.circ_lib) {
-                    hold.circ_lib = egOrg.get(hold.circ_lib).shortname();
-                    hold.copy_barcode_enc = encodeURIComponent(hold.copy_barcode);
-                }
-
-                if ($scope.orientation_lender) 
-                    hold.next_action = 'ill-home-capture';
-
-                // we don't use itemList.addItem(), 
-                // since we're managing our own data fetching
-                $scope.itemList.items.push(hold);
-            }
-        );
-    }
-
-
-    $scope.firstPage = function() {
-        $scope.itemList.offset = 0;
-        $scope.drawTable();
-    };
-
-    $scope.nextPage = function() {
-        $scope.itemList.incrementPage();
-        $scope.drawTable();
-    };
-
-    $scope.prevPage = function() {
-        $scope.itemList.decrementPage();
-        $scope.drawTable();
-    };
-
-    $scope.drawTable();
-}])
-
 // http://stackoverflow.com/questions/17629126/how-to-upload-a-file-using-angularjs-like-the-traditional-way
 .factory('formDataObject', function() {
     return function(data) {