webstaff: Make Items Bookable and Book Item Now for Holdings View
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / cat / catalog / app.js
index 5c8f917..1f1ac58 100644 (file)
@@ -7,7 +7,14 @@
  *
  */
 
-angular.module('egCatalogApp', ['ui.bootstrap','ngRoute','egCoreMod','egGridMod', 'egMarcMod', 'egUserMod'])
+angular.module('egCatalogApp', ['ui.bootstrap','ngRoute','ngLocationUpdate','egCoreMod','egGridMod', 'egMarcMod', 'egUserMod', 'egHoldingsMod', 'ngToast'])
+
+.config(['ngToastProvider', function(ngToastProvider) {
+  ngToastProvider.configure({
+    verticalPosition: 'bottom',
+    animation: 'fade'
+  });
+}])
 
 .config(function($routeProvider, $locationProvider, $compileProvider) {
     $locationProvider.html5Mode(true);
@@ -33,6 +40,14 @@ angular.module('egCatalogApp', ['ui.bootstrap','ngRoute','egCoreMod','egGridMod'
         resolve : resolver
     });
 
+    // Jump directly to the results page.  Any URL parameter 
+    // supported by the embedded catalog is supported here.
+    $routeProvider.when('/cat/catalog/results', {
+        templateUrl: './cat/catalog/t_catalog',
+        controller: 'CatalogCtrl',
+        resolve : resolver
+    });
+
     $routeProvider.when('/cat/catalog/retrieve_by_id', {
         templateUrl: './cat/catalog/t_retrieve_by_id',
         controller: 'CatalogRecordRetrieve',
@@ -190,9 +205,12 @@ function($scope , $routeParams , $location , $window , $q , egCore) {
         });
         $scope.template_list.sort();
     });
-    egCore.hatch.getItem('cat.default_bib_marc_template').then(function(template) {
-        $scope.template_name = template;
-    });
+    $scope.template_name = egCore.hatch.getSessionItem('eg.cat.last_bib_marc_template');
+    if (!$scope.template_name) {
+        egCore.hatch.getItem('cat.default_bib_marc_template').then(function(template) {
+            $scope.template_name = template;
+        });
+    }
 
     $scope.loadTemplate = function() {
         if ($scope.template_name) {
@@ -203,6 +221,7 @@ function($scope , $routeParams , $location , $window , $q , egCore) {
             ).then(function(template) {
                 $scope.marc_template = template;
                 $scope.have_template = true;
+                egCore.hatch.setSessionItem('eg.cat.last_bib_marc_template', $scope.template_name);
             });
         }
     }
@@ -224,41 +243,76 @@ function($scope , $routeParams , $location , $window , $q , egCore) {
     
 
 }])
-
 .controller('CatalogCtrl',
-       ['$scope','$routeParams','$location','$window','$q','egCore','egHolds','egCirc','egConfirmDialog',
-        'egGridDataProvider','egHoldGridActions','$timeout','$modal','holdingsSvc','egUser',
-function($scope , $routeParams , $location , $window , $q , egCore , egHolds , egCirc,  egConfirmDialog,
-         egGridDataProvider , egHoldGridActions , $timeout , $modal , holdingsSvc , egUser) {
+       ['$scope','$routeParams','$location','$window','$q','egCore','egHolds','egCirc','egConfirmDialog','ngToast',
+        'egGridDataProvider','egHoldGridActions','egProgressDialog','$timeout','$uibModal','holdingsSvc','egUser','conjoinedSvc',
+        '$cookies',
+function($scope , $routeParams , $location , $window , $q , egCore , egHolds , egCirc , egConfirmDialog , ngToast ,
+         egGridDataProvider , egHoldGridActions , egProgressDialog , $timeout , $uibModal , holdingsSvc , egUser , conjoinedSvc,
+         $cookies
+) {
+
+    var holdingsSvcInst = new holdingsSvc();
 
     // set record ID on page load if available...
     $scope.record_id = $routeParams.record_id;
+    $scope.summary_pane_record;
 
     if ($routeParams.record_id) $scope.from_route = true;
     else $scope.from_route = false;
 
+    // set search and preferred library cookies
+    egCore.hatch.getItem('eg.search.search_lib').then(function(val) {
+        $cookies.put('eg_search_lib', val, { path : '/' });
+    });
+    egCore.hatch.getItem('eg.search.pref_lib').then(function(val) {
+        $cookies.put('eg_pref_lib', val, { path : '/' });
+    });
+
     // will hold a ref to the opac iframe
     $scope.opac_iframe = null;
     $scope.parts_iframe = null;
 
+    $scope.search_result_index = 1;
+    $scope.search_result_hit_count = 1;
+
+    $scope.$watch(
+        'opac_iframe.dom.contentWindow.search_result_index',
+        function (n,o) {
+            if (!isNaN(parseInt(n)))
+                $scope.search_result_index = n + 1;
+        }
+    );
+
+    $scope.$watch(
+        'opac_iframe.dom.contentWindow.search_result_hit_count',
+        function (n,o) {
+            if (!isNaN(parseInt(n)))
+                $scope.search_result_hit_count = n;
+        }
+    );
+
     $scope.in_opac_call = false;
     $scope.opac_call = function (opac_frame_function, force_opac_tab) {
         if ($scope.opac_iframe) {
             if (force_opac_tab) $scope.record_tab = 'catalog';
             $scope.in_opac_call = true;
             $scope.opac_iframe.dom.contentWindow[opac_frame_function]();
+            if (opac_frame_function == 'rdetailBackToResults') {
+                $location.update_path('/cat/catalog/index');
+            }
         }
     }
 
     $scope.add_to_record_bucket = function() {
         var recId = $scope.record_id;
-        return $modal.open({
+        return $uibModal.open({
             templateUrl: './cat/catalog/t_add_to_bucket',
             animation: true,
             size: 'md',
             controller:
-                   ['$scope','$modalInstance',
-            function($scope , $modalInstance) {
+                   ['$scope','$uibModalInstance',
+            function($scope , $uibModalInstance) {
 
                 $scope.bucket_id = 0;
                 $scope.newBucketName = '';
@@ -279,7 +333,7 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
                         'open-ils.actor.container.item.create',
                         egCore.auth.token(), 'biblio', item
                     ).then(function(resp) {
-                        $modalInstance.close();
+                        $uibModalInstance.close();
                     });
                 }
 
@@ -301,12 +355,45 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
                 }
 
                 $scope.cancel = function() {
-                    $modalInstance.dismiss();
+                    $uibModalInstance.dismiss();
                 }
             }]
         });
     }
 
+    $scope.current_overlay_target     = egCore.hatch.getLocalItem('eg.cat.marked_overlay_record');
+    $scope.current_voltransfer_target = egCore.hatch.getLocalItem('eg.cat.marked_volume_transfer_record');
+    $scope.current_conjoined_target   = egCore.hatch.getLocalItem('eg.cat.marked_conjoined_record');
+
+    $scope.markConjoined = function () {
+        $scope.current_conjoined_target = $scope.record_id;
+        egCore.hatch.setLocalItem('eg.cat.marked_conjoined_record',$scope.record_id);
+        ngToast.create(egCore.strings.MARK_CONJ_TARGET);
+    };
+
+    $scope.markVolTransfer = function () {
+        ngToast.create(egCore.strings.MARK_VOL_TARGET);
+        $scope.current_voltransfer_target = $scope.record_id;
+        egCore.hatch.setLocalItem('eg.cat.marked_volume_transfer_record',$scope.record_id);
+    };
+
+    $scope.markOverlay = function () {
+        $scope.current_overlay_target = $scope.record_id;
+        egCore.hatch.setLocalItem('eg.cat.marked_overlay_record',$scope.record_id);
+        ngToast.create(egCore.strings.MARK_OVERLAY_TARGET);
+    };
+
+    $scope.clearRecordMarks = function () {
+        $scope.current_overlay_target     = null;
+        $scope.current_voltransfer_target = null;
+        $scope.current_conjoined_target   = null;
+        $scope.current_hold_transfer_dest = null;
+        egCore.hatch.removeLocalItem('eg.cat.marked_volume_transfer_record');
+        egCore.hatch.removeLocalItem('eg.cat.marked_conjoined_record');
+        egCore.hatch.removeLocalItem('eg.cat.marked_overlay_record');
+        egCore.hatch.removeLocalItem('eg.circ.hold.title_transfer_target');
+    }
+
     $scope.stop_unload = false;
     $scope.$watch('stop_unload',
         function(newVal, oldVal) {
@@ -325,6 +412,20 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
     if ($scope.record_id)
         egCore.hatch.setLocalItem("eg.cat.last_record_retrieved", $scope.record_id);
 
+    $scope.refresh_record_callback = function (record_id) {
+        egCore.pcrud.retrieve('bre', record_id, {
+            flesh : 1,
+            flesh_fields : {
+                bre : ['simple_record','creator','editor']
+            }
+        }).then(function(rec) {
+            rec.owner(egCore.org.get(rec.owner()));
+            $scope.summary_pane_record = rec;
+        });
+
+        return record_id;
+    }
+
     // also set it when the iframe changes to a new record
     $scope.handle_page = function(url) {
 
@@ -338,7 +439,11 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
             $scope.record_id = match[1];
             egCore.hatch.setLocalItem("eg.cat.last_record_retrieved", $scope.record_id);
             $scope.holdings_record_id_changed($scope.record_id);
+            conjoinedSvc.fetch($scope.record_id).then(function(){
+                $scope.conjoinedGridDataProvider.refresh();
+            });
             init_parts_url();
+            $location.update_path('/cat/catalog/record/' + $scope.record_id);
         } else {
             delete $scope.record_id;
             $scope.from_route = false;
@@ -364,25 +469,316 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
     $scope.handlers = { }
 
     // ------------------------------------------------------------------
+    // Conjoined items
+
+    $scope.conjoinedGridControls = {};
+    $scope.conjoinedGridDataProvider = egGridDataProvider.instance({
+        get : function(offset, count) {
+            return this.arrayNotifier(conjoinedSvc.items, offset, count);
+        }
+    });
+
+    $scope.changeConjoinedType = function () {
+        var peers = egCore.idl.Clone($scope.conjoinedGridControls.selectedItems());
+        angular.forEach(peers, function (p) {
+            p.target_copy(p.target_copy().id());
+            p.peer_type(p.peer_type().id());
+        });
+
+        var conjoinedGridDataProviderRef = $scope.conjoinedGridDataProvider;
+
+        return $uibModal.open({
+            templateUrl: './cat/catalog/t_conjoined_selector',
+            animation: true,
+            controller:
+                   ['$scope','$uibModalInstance',
+            function($scope , $uibModalInstance) {
+                $scope.update = true;
+
+                $scope.peer_type = null;
+                $scope.peer_type_list = [];
+                conjoinedSvc.get_peer_types().then(function(list){
+                    $scope.peer_type_list = list;
+                });
+    
+                $scope.ok = function(type) {
+                    var promises = [];
+    
+                    angular.forEach(peers, function (p) {
+                        p.ischanged(1);
+                        p.peer_type(type);
+                        promises.push(egCore.pcrud.update(p));
+                    });
+    
+                    return $q.all(promises)
+                        .then(function(){$uibModalInstance.close()})
+                        .then(function(){return conjoinedSvc.fetch()})
+                        .then(function(){conjoinedGridDataProviderRef.refresh()});
+                }
+    
+                $scope.cancel = function($event) {
+                    $uibModalInstance.dismiss();
+                    $event.preventDefault();
+                }
+            }]
+        });
+        
+    }
+
+    $scope.refreshConjoined = function () {
+        conjoinedSvc.fetch($scope.record_id)
+        .then(function(){$scope.conjoinedGridDataProvider.refresh();});
+    }
+
+    $scope.deleteSelectedConjoined = function () {
+        var peers = $scope.conjoinedGridControls.selectedItems();
+
+        if (peers.length > 0) {
+            egConfirmDialog.open(
+                egCore.strings.CONFIRM_DELETE_PEERS,
+                egCore.strings.CONFIRM_DELETE_PEERS_MESSAGE,
+                {peers : peers.length}
+            ).result.then(function() {
+                angular.forEach(peers, function (p) {
+                    p.isdeleted(1);
+                });
+
+                egCore.pcrud.remove(peers).then(function() {
+                    return conjoinedSvc.fetch();
+                }).then(function() {
+                    $scope.conjoinedGridDataProvider.refresh();
+                });
+            });
+        }
+    }
+    if ($scope.record_id)
+        conjoinedSvc.fetch($scope.record_id);
+
+    // ------------------------------------------------------------------
     // Holdings
 
-    $scope.holdingsGridControls = {};
+    $scope.holdingsGridControls = {
+        activateItem : function (item) {
+            $scope.selectedHoldingsVolCopyEdit();
+        }
+    };
     $scope.holdingsGridDataProvider = egGridDataProvider.instance({
         get : function(offset, count) {
-            return this.arrayNotifier(holdingsSvc.copies, offset, count);
+            return this.arrayNotifier(holdingsSvcInst.copies, offset, count);
         }
     });
 
+    $scope.add_copies_to_bucket = function() {
+        var copy_list = gatherSelectedHoldingsIds();
+        if (copy_list.length == 0) return;
+
+        return $uibModal.open({
+            templateUrl: './cat/catalog/t_add_to_bucket',
+            animation: true,
+            size: 'md',
+            controller:
+                   ['$scope','$uibModalInstance',
+            function($scope , $uibModalInstance) {
+
+                $scope.bucket_id = 0;
+                $scope.newBucketName = '';
+                $scope.allBuckets = [];
+
+                egCore.net.request(
+                    'open-ils.actor',
+                    'open-ils.actor.container.retrieve_by_class.authoritative',
+                    egCore.auth.token(), egCore.auth.user().id(),
+                    'copy', 'staff_client'
+                ).then(function(buckets) { $scope.allBuckets = buckets; });
+
+                $scope.add_to_bucket = function() {
+                    var promises = [];
+                    angular.forEach(copy_list, function (cp) {
+                        var item = new egCore.idl.ccbi()
+                        item.bucket($scope.bucket_id);
+                        item.target_copy(cp);
+                        promises.push(
+                            egCore.net.request(
+                                'open-ils.actor',
+                                'open-ils.actor.container.item.create',
+                                egCore.auth.token(), 'copy', item
+                            )
+                        );
+
+                        return $q.all(promises).then(function() {
+                            $uibModalInstance.close();
+                        });
+                    });
+                }
+
+                $scope.add_to_new_bucket = function() {
+                    var bucket = new egCore.idl.ccb();
+                    bucket.owner(egCore.auth.user().id());
+                    bucket.name($scope.newBucketName);
+                    bucket.description('');
+                    bucket.btype('staff_client');
+
+                    return egCore.net.request(
+                        'open-ils.actor',
+                        'open-ils.actor.container.create',
+                        egCore.auth.token(), 'copy', bucket
+                    ).then(function(bucket) {
+                        $scope.bucket_id = bucket;
+                        $scope.add_to_bucket();
+                    });
+                }
+
+                $scope.cancel = function() {
+                    $uibModalInstance.dismiss();
+                }
+            }]
+        });
+    }
+
+    // TODO: refactor common code between cat/catalog/app.js and cat/item/app.js 
+
+    $scope.need_one_selected = function() {
+        var items = $scope.holdingsGridControls.selectedItems();
+        if (items.length == 1) return false;
+        return true;
+    };
+
+    $scope.make_copies_bookable = function() {
+
+        var copies_by_record = {};
+        var record_list = [];
+        angular.forEach(
+            $scope.holdingsGridControls.selectedItems(),
+            function (item) {
+                var record_id = item['call_number.record.id'];
+                if (typeof copies_by_record[ record_id ] == 'undefined') {
+                    copies_by_record[ record_id ] = [];
+                    record_list.push( record_id );
+                }
+                copies_by_record[ record_id ].push(item.id);
+            }
+        );
+
+        var promises = [];
+        var combined_results = [];
+        angular.forEach(record_list, function(record_id) {
+            promises.push(
+                egCore.net.request(
+                    'open-ils.booking',
+                    'open-ils.booking.resources.create_from_copies',
+                    egCore.auth.token(),
+                    copies_by_record[record_id]
+                ).then(function(results) {
+                    if (results && results['brsrc']) {
+                        combined_results = combined_results.concat(results['brsrc']);
+                    }
+                })
+            );
+        });
+
+        $q.all(promises).then(function() {
+            if (combined_results.length > 0) {
+                $uibModal.open({
+                    template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
+                    animation: true,
+                    size: 'md',
+                    controller:
+                           ['$scope','$location','egCore','$uibModalInstance',
+                    function($scope , $location , egCore , $uibModalInstance) {
+
+                        $scope.funcs = {
+                            ses : egCore.auth.token(),
+                            resultant_brsrc : combined_results.map(function(o) { return o[0]; })
+                        }
+
+                        var booking_path = '/eg/conify/global/booking/resource';
+
+                        $scope.booking_admin_url =
+                            $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
+                    }]
+                });
+            }
+        });
+    }
+
+    $scope.book_copies_now = function() {
+        var copies_by_record = {};
+        var record_list = [];
+        angular.forEach(
+            $scope.holdingsGridControls.selectedItems(),
+            function (item) {
+                var record_id = item['call_number.record.id'];
+                if (typeof copies_by_record[ record_id ] == 'undefined') {
+                    copies_by_record[ record_id ] = [];
+                    record_list.push( record_id );
+                }
+                copies_by_record[ record_id ].push(item.id);
+            }
+        );
+
+        var promises = [];
+        var combined_brt = [];
+        var combined_brsrc = [];
+        angular.forEach(record_list, function(record_id) {
+            promises.push(
+                egCore.net.request(
+                    'open-ils.booking',
+                    'open-ils.booking.resources.create_from_copies',
+                    egCore.auth.token(),
+                    copies_by_record[record_id]
+                ).then(function(results) {
+                    if (results && results['brt']) {
+                        combined_brt = combined_brt.concat(results['brt']);
+                    }
+                    if (results && results['brsrc']) {
+                        combined_brsrc = combined_brsrc.concat(results['brsrc']);
+                    }
+                })
+            );
+        });
+
+        $q.all(promises).then(function() {
+            if (combined_brt.length > 0 || combined_brsrc.length > 0) {
+                $uibModal.open({
+                    template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
+                    animation: true,
+                    size: 'md',
+                    controller:
+                           ['$scope','$location','egCore','$uibModalInstance',
+                    function($scope , $location , egCore , $uibModalInstance) {
+
+                        $scope.funcs = {
+                            ses : egCore.auth.token(),
+                            bresv_interface_opts : {
+                                booking_results : {
+                                     brt : combined_brt
+                                    ,brsrc : combined_brsrc
+                                }
+                            }
+                        }
+
+                        var booking_path = '/eg/booking/reservation';
+
+                        $scope.booking_admin_url =
+                            $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
+
+                    }]
+                });
+            }
+        });
+    }
+
+
     $scope.requestItems = function() {
         var copy_list = gatherSelectedHoldingsIds();
         if (copy_list.length == 0) return;
 
-        return $modal.open({
+        return $uibModal.open({
             templateUrl: './cat/catalog/t_request_items',
             animation: true,
             controller:
-                   ['$scope','$modalInstance',
-            function($scope , $modalInstance) {
+                   ['$scope','$uibModalInstance',
+            function($scope , $uibModalInstance) {
                 $scope.user = null;
                 $scope.first_user_fetch = true;
 
@@ -431,22 +827,85 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
                         egCore.auth.token(), args, h.copy_list
                     );
 
-                    $modalInstance.close();
+                    $uibModalInstance.close();
                 }
 
                 $scope.cancel = function($event) {
-                    $modalInstance.dismiss();
+                    $uibModalInstance.dismiss();
                     $event.preventDefault();
                 }
             }]
         });
     }
 
+    $scope.view_place_orders = function() {
+        if (!$scope.record_id) return;
+        var url = egCore.env.basePath + 'acq/legacy/lineitem/related/' + $scope.record_id + '?target=bib';
+        $timeout(function() { $window.open(url, '_blank') });
+    }
+
+    $scope.replaceBarcodes = function() {
+        var copy_list = gatherSelectedRawCopies();
+        if (copy_list.length == 0) return;
+
+        var holdingsGridDataProviderRef = $scope.holdingsGridDataProvider;
+
+        angular.forEach(copy_list, function (cp) {
+            $uibModal.open({
+                templateUrl: './cat/share/t_replace_barcode',
+                animation: true,
+                controller:
+                           ['$scope','$uibModalInstance',
+                    function($scope , $uibModalInstance) {
+                        $scope.isModal = true;
+                        $scope.focusBarcode = false;
+                        $scope.focusBarcode2 = true;
+                        $scope.barcode1 = cp.barcode();
+
+                        $scope.updateBarcode = function() {
+                            $scope.copyNotFound = false;
+                            $scope.updateOK = false;
+                
+                            egCore.pcrud.search('acp',
+                                {deleted : 'f', barcode : $scope.barcode1})
+                            .then(function(copy) {
+                
+                                if (!copy) {
+                                    $scope.focusBarcode = true;
+                                    $scope.copyNotFound = true;
+                                    return;
+                                }
+                
+                                $scope.copyId = copy.id();
+                                copy.barcode($scope.barcode2);
+                
+                                egCore.pcrud.update(copy).then(function(stat) {
+                                    $scope.updateOK = stat;
+                                    $scope.focusBarcode = true;
+                                    holdingsSvc.fetchAgain().then(function (){
+                                        holdingsGridDataProviderRef.refresh();
+                                    });
+                                });
+
+                            });
+                            $uibModalInstance.close();
+                        }
+
+                        $scope.cancel = function($event) {
+                            $uibModalInstance.dismiss();
+                            $event.preventDefault();
+                        }
+                    }
+                ]
+            });
+        });
+    }
+
     // refresh the list of holdings when the record_id is changed.
     $scope.holdings_record_id_changed = function(id) {
         if ($scope.record_id != id) $scope.record_id = id;
         console.log('record id changed to ' + id + ', loading new holdings');
-        holdingsSvc.fetch({
+        holdingsSvcInst.fetch({
             rid : $scope.record_id,
             org : $scope.holdings_ou,
             copy: $scope.holdings_show_copies,
@@ -461,7 +920,7 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
     $scope.holdings_ou = egCore.org.get(egCore.auth.user().ws_ou());
     $scope.holdings_ou_changed = function(org) {
         $scope.holdings_ou = org;
-        holdingsSvc.fetch({
+        holdingsSvcInst.fetch({
             rid : $scope.record_id,
             org : $scope.holdings_ou,
             copy: $scope.holdings_show_copies,
@@ -475,7 +934,7 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
     $scope.holdings_cb_changed = function(cb,newVal,norefresh) {
         $scope[cb] = newVal;
         egCore.hatch.setItem('cat.' + cb, newVal);
-        if (!norefresh) holdingsSvc.fetch({
+        if (!norefresh) holdingsSvcInst.fetch({
             rid : $scope.record_id,
             org : $scope.holdings_ou,
             copy: $scope.holdings_show_copies,
@@ -534,6 +993,18 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         return cp_list;
     }
 
+    function gatherSelectedEmptyVolumeIds () {
+        var cn_id_list = [];
+        angular.forEach(
+            $scope.holdingsGridControls.selectedItems(),
+            function (item) {
+                if (item.copy_count == 0)
+                    cn_id_list.push(item.call_number.id)
+            }
+        );
+        return cn_id_list;
+    }
+
     function gatherSelectedVolumeIds () {
         var cn_id_list = [];
         angular.forEach(
@@ -594,6 +1065,9 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
 
         if (cnList.length == 0) return;
 
+        var flags = {};
+        if (vols && copies) flags.force_delete_copies = 1;
+
         egConfirmDialog.open(
             egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES,
             egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES_MESSAGE,
@@ -602,9 +1076,11 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
             egCore.net.request(
                 'open-ils.cat',
                 'open-ils.cat.asset.volume.fleshed.batch.update.override',
-                egCore.auth.token(), cnList, 1, {}
+                egCore.auth.token(), cnList, 1, flags
             ).then(function(update_count) {
-                $scope.holdingsGridDataProvider.refresh();
+                holdingsSvcInst.fetchAgain().then(function() {
+                    $scope.holdingsGridDataProvider.refresh();
+                });
             });
         });
     }
@@ -619,14 +1095,24 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
                 raw.push( {callnumber : v} );
             });
         } else if (vols) {
-            angular.forEach(
-                $scope.holdingsGridControls.selectedItems(),
-                function (item) {
-                    raw.push({owner : item.owner_id});
-                }
-            );
+            if (typeof $scope.holdingsGridControls.selectedItems == "function" &&
+                $scope.holdingsGridControls.selectedItems().length > 0) {
+                angular.forEach($scope.holdingsGridControls.selectedItems(),
+                    function (item) {
+                        raw.push({
+                            owner : item.owner_id,
+                            label : item.call_number.label
+                        });
+                    });
+            } else {
+                raw.push({
+                    owner : egCore.auth.user().ws_ou()
+                });
+            }
         }
 
+        if (raw.length == 0) raw.push({});
+
         egCore.net.request(
             'open-ils.actor',
             'open-ils.actor.anon_cache.set_value',
@@ -655,6 +1141,9 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
             null, 'edit-these-copies', {
                 record_id: $scope.record_id,
                 copies: gatherSelectedHoldingsIds(),
+                raw: gatherSelectedEmptyVolumeIds().map(
+                    function(v){ return { callnumber : v } }
+                ),
                 hide_vols : hide_vols,
                 hide_copies : hide_copies
             }
@@ -682,14 +1171,41 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
                 'eg.cat.item_transfer_target',
                 $scope.holdingsGridControls.selectedItems()[0].call_number.id
             );
+            ngToast.create(egCore.strings.MARK_ITEM_TARGET);
         }
     }
 
     $scope.markLibAsVolTarget = function() {
+        return $uibModal.open({
+            templateUrl: './cat/catalog/t_choose_vol_target_lib',
+            animation: true,
+            controller:
+                   ['$scope','$uibModalInstance',
+            function($scope , $uibModalInstance) {
+
+                var orgId = egCore.hatch.getLocalItem('eg.cat.volume_transfer_target') || 1;
+                $scope.org = egCore.org.get(orgId);
+                $scope.cant_have_vols = function (id) { return !egCore.org.CanHaveVolumes(id); };
+                $scope.ok = function(org) {
+                    egCore.hatch.setLocalItem(
+                        'eg.cat.volume_transfer_target',
+                        org.id()
+                    );
+                    $uibModalInstance.close();
+                }
+                $scope.cancel = function($event) {
+                    $uibModalInstance.dismiss();
+                    $event.preventDefault();
+                }
+            }]
+        });
+    }
+    $scope.markLibFromSelectedAsVolTarget = function() {
         egCore.hatch.setLocalItem(
             'eg.cat.volume_transfer_target',
             $scope.holdingsGridControls.selectedItems()[0].owner_id
         );
+        ngToast.create(egCore.strings.MARK_VOL_TARGET);
     }
 
     $scope.selectedHoldingsItemStatusDetail = function (){
@@ -703,27 +1219,61 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         );
     }
 
-    $scope.transferVolumes = function (){
+    $scope.transferVolumesToRecord = function (){
+        var target_record = egCore.hatch.getLocalItem('eg.cat.marked_volume_transfer_record');
+        if (!target_record) return;
+        if ($scope.record_id == target_record) return;
+        var items = $scope.holdingsGridControls.selectedItems();
+        if (!items.length) return;
+
+        var vols_to_move   = {};
+        angular.forEach(items, function(item) {
+            if (!(item.call_number.owning_lib in vols_to_move)) {
+                vols_to_move[item.call_number.owning_lib] = new Array;
+            }
+            vols_to_move[item.call_number.owning_lib].push(item.call_number.id);
+        });
+
+        var promises = [];        
+        angular.forEach(vols_to_move, function(vols, owning_lib) {
+            promises.push(egCore.net.request(
+                'open-ils.cat',
+                'open-ils.cat.asset.volume.batch.transfer.override',
+                egCore.auth.token(), {
+                    docid   : target_record,
+                    lib     : owning_lib,
+                    volumes : vols
+                }
+            ));
+        });
+        $q.all(promises).then(function(success) {
+            if (success) {
+                ngToast.create(egCore.strings.VOLS_TRANSFERED);
+                holdingsSvcInst.fetchAgain().then(function() {
+                    $scope.holdingsGridDataProvider.refresh();
+                });
+            } else {
+                alert('Could not transfer volumes!');
+            }
+        });
+    }
+
+    function transferVolumes(new_record){
         var xfer_target = egCore.hatch.getLocalItem('eg.cat.volume_transfer_target');
 
         if (xfer_target) {
             egCore.net.request(
                 'open-ils.cat',
-                'open-ils.open-ils.cat.asset.volume.batch.transfer.override',
+                'open-ils.cat.asset.volume.batch.transfer.override',
                 egCore.auth.token(), {
-                    docid   : $scope.record_id,
+                    docid   : (new_record ? new_record : $scope.record_id),
                     lib     : xfer_target,
                     volumes : gatherSelectedVolumeIds()
                 }
             ).then(function(success) {
                 if (success) {
-                    holdingsSvc.fetch({
-                        rid : $scope.record_id,
-                        org : $scope.holdings_ou,
-                        copy: $scope.holdings_show_copies,
-                        vol : $scope.holdings_show_vols,
-                        empty: $scope.holdings_show_empty
-                    }).then(function() {
+                    ngToast.create(egCore.strings.VOLS_TRANSFERED);
+                    holdingsSvcInst.fetchAgain().then(function() {
                         $scope.holdingsGridDataProvider.refresh();
                     });
                 } else {
@@ -734,34 +1284,113 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         
     }
 
-    $scope.transferItems = function (){
-        var xfer_target = egCore.hatch.getLocalItem('eg.cat.item_transfer_target');
-        if (xfer_target) {
-            var copy_list = gatherSelectedRawCopies();
+    $scope.transferVolumesToLibrary = function() {
+        transferVolumes();
+    }
 
-            angular.forEach(copy_list, function (cp) {
-                cp.call_number(xfer_target);
-            });
+    $scope.transferVolumesToRecordAndLibrary = function() {
+        var target_record = egCore.hatch.getLocalItem('eg.cat.marked_volume_transfer_record');
+        if (!target_record) return;
+        transferVolumes(target_record);
+    }
 
-            egCore.pcrud.update(
-                copy_list
-            ).then(function(success) {
-                if (success) {
-                    holdingsSvc.fetch({
-                        rid : $scope.record_id,
-                        org : $scope.holdings_ou,
-                        copy: $scope.holdings_show_copies,
-                        vol : $scope.holdings_show_vols,
-                        empty: $scope.holdings_show_empty
-                    }).then(function() {
-                        $scope.holdingsGridDataProvider.refresh();
-                    });
+    // this "transfers" selected copies to a new owning library,
+    // auto-creating volumes and deleting unused volumes as required.
+    $scope.changeItemOwningLib = function() {
+        var xfer_target = egCore.hatch.getLocalItem('eg.cat.volume_transfer_target');
+        var items = $scope.holdingsGridControls.selectedItems();
+        if (!xfer_target || !items.length) {
+            return;
+        }
+        var vols_to_move   = {};
+        var copies_to_move = {};
+        angular.forEach(items, function(item) {
+            if (item.call_number.owning_lib != xfer_target) {
+                if (item.call_number.id in vols_to_move) {
+                    copies_to_move[item.call_number.id].push(item.id);
                 } else {
-                    alert('Could not transfer items!');
+                    vols_to_move[item.call_number.id] = item.call_number;
+                    copies_to_move[item.call_number.id] = new Array;
+                    copies_to_move[item.call_number.id].push(item.id);
                 }
+            }
+        });
+    
+        var promises = [];
+        angular.forEach(vols_to_move, function(vol) {
+            promises.push(egCore.net.request(
+                'open-ils.cat',
+                'open-ils.cat.call_number.find_or_create',
+                egCore.auth.token(),
+                vol.label,
+                vol.record,
+                xfer_target,
+                vol.prefix.id,
+                vol.suffix.id,
+                vol.label_class
+            ).then(function(resp) {
+                var evt = egCore.evt.parse(resp);
+                if (evt) return;
+                return egCore.net.request(
+                    'open-ils.cat',
+                    'open-ils.cat.transfer_copies_to_volume',
+                    egCore.auth.token(),
+                    resp.acn_id,
+                    copies_to_move[vol.id]
+                );
+            }));
+        });
+        $q.all(promises).then(function() {
+            ngToast.create(egCore.strings.ITEMS_TRANSFERED);
+            holdingsSvcInst.fetchAgain().then(function() {
+                $scope.holdingsGridDataProvider.refresh();
             });
+        });
+    }
+
+    $scope.transferItems = function (){
+        var xfer_target = egCore.hatch.getLocalItem('eg.cat.item_transfer_target');
+        var copy_ids = gatherSelectedHoldingsIds();
+        if (xfer_target && copy_ids.length > 0) {
+            egCore.net.request(
+                'open-ils.cat',
+                'open-ils.cat.transfer_copies_to_volume',
+                egCore.auth.token(),
+                xfer_target,
+                copy_ids
+            ).then(
+                function(resp) { // oncomplete
+                    var evt = egCore.evt.parse(resp);
+                    if (evt) {
+                        egConfirmDialog.open(
+                            egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_TITLE,
+                            egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_BODY,
+                            {'evt_desc': evt.desc}
+                        ).result.then(function() {
+                            egCore.net.request(
+                                'open-ils.cat',
+                                'open-ils.cat.transfer_copies_to_volume.override',
+                                egCore.auth.token(),
+                                xfer_target,
+                                copy_ids,
+                                { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
+                            ).then(function(resp) {
+                                holdingsSvcInst.fetchAgain().then(function() {
+                                    $scope.holdingsGridDataProvider.refresh();
+                                });
+                            });
+                        });
+                    } else {
+                        ngToast.create(egCore.strings.ITEMS_TRANSFERED);
+                        holdingsSvcInst.fetchAgain().then(function() {
+                            $scope.holdingsGridDataProvider.refresh();
+                        });
+                    }
+                },
+                null, // onerror
+                null // onprogress
+            )
         }
-        
     }
 
     $scope.selectedHoldingsItemStatusTgrEvt = function (){
@@ -788,13 +1417,7 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
 
     $scope.selectedHoldingsDamaged = function () {
         egCirc.mark_damaged(gatherSelectedHoldingsIds()).then(function() {
-            holdingsSvc.fetch({
-                rid : $scope.record_id,
-                org : $scope.holdings_ou,
-                copy: $scope.holdings_show_copies,
-                vol : $scope.holdings_show_vols,
-                empty: $scope.holdings_show_empty
-            }).then(function() {
+            holdingsSvcInst.fetchAgain().then(function() {
                 $scope.holdingsGridDataProvider.refresh();
             });
         });
@@ -802,18 +1425,57 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
 
     $scope.selectedHoldingsMissing = function () {
         egCirc.mark_missing(gatherSelectedHoldingsIds()).then(function() {
-            holdingsSvc.fetch({
-                rid : $scope.record_id,
-                org : $scope.holdings_ou,
-                copy: $scope.holdings_show_copies,
-                vol : $scope.holdings_show_vols,
-                empty: $scope.holdings_show_empty
-            }).then(function() {
+            holdingsSvcInst.fetchAgain().then(function() {
                 $scope.holdingsGridDataProvider.refresh();
             });
         });
     }
 
+    $scope.attach_to_peer_bib = function() {
+        var copy_list = gatherSelectedHoldingsIds();
+        if (copy_list.length == 0) return;
+
+        egCore.hatch.getItem('eg.cat.marked_conjoined_record').then(function(target_record) {
+            if (!target_record) return;
+
+            return $uibModal.open({
+                templateUrl: './cat/catalog/t_conjoined_selector',
+                animation: true,
+                controller:
+                       ['$scope','$uibModalInstance',
+                function($scope , $uibModalInstance) {
+                    $scope.update = false;
+
+                    $scope.peer_type = null;
+                    $scope.peer_type_list = [];
+                    conjoinedSvc.get_peer_types().then(function(list){
+                        $scope.peer_type_list = list;
+                    });
+    
+                    $scope.ok = function(type) {
+                        var promises = [];
+    
+                        angular.forEach(copy_list, function (cp) {
+                            var n = new egCore.idl.bpbcm();
+                            n.isnew(true);
+                            n.peer_record(target_record);
+                            n.target_copy(cp);
+                            n.peer_type(type);
+                            promises.push(egCore.pcrud.create(n));
+                        });
+    
+                        return $q.all(promises).then(function(){$uibModalInstance.close()});
+                    }
+    
+                    $scope.cancel = function($event) {
+                        $uibModalInstance.dismiss();
+                        $event.preventDefault();
+                    }
+                }]
+            });
+        });
+    }
+
 
     // ------------------------------------------------------------------
     // Holds 
@@ -826,6 +1488,7 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
     var hold_ids = []; // current list of holds
     function fetchHolds(offset, count) {
         var ids = hold_ids.slice(offset, offset + count);
+
         return egHolds.fetch_holds(ids).then(null, null,
             function(hold_data) { 
                 return hold_data;
@@ -838,6 +1501,9 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         var deferred = $q.defer();
         hold_ids = []; // no caching ATM
 
+        // open a determinate progress dialog, max value set below.
+        egProgressDialog.open({max : 1, value : 0});
+
         // fetch the IDs
         egCore.net.request(
             'open-ils.circ',
@@ -849,8 +1515,22 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
                 angular.forEach(hold_data, function(list, type) {
                     hold_ids = hold_ids.concat(list);
                 });
-                fetchHolds(offset, count).then(
-                    deferred.resolve, null, deferred.notify);
+
+                // Set the max value of the progress bar to the lesser of
+                // the total number of holds to fetch or the page size
+                // of the grid.
+                egProgressDialog.update(
+                    {max : Math.min(hold_ids.length, count)});
+
+                var holds_fetched = 0;
+                fetchHolds(offset, count)
+                .then(deferred.resolve, null, 
+                    function(hold_data) {
+                        holds_fetched++;
+                        deferred.notify(hold_data);
+                        egProgressDialog.increment();
+                    }
+                )['finally'](egProgressDialog.close);
             }
         );
 
@@ -896,9 +1576,13 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
         });
     }
 
+    $scope.current_hold_transfer_dest = egCore.hatch.getLocalItem ('eg.circ.hold.title_transfer_target');
+
     $scope.mark_hold_transfer_dest = function() {
+        $scope.current_hold_transfer_dest = $scope.record_id;
         egCore.hatch.setLocalItem(
             'eg.circ.hold.title_transfer_target', $scope.record_id);
+        ngToast.create(egCore.strings.HOLD_TRANSFER_DEST_MARKED);
     }
 
     // UI presents this option as "all holds"
@@ -924,6 +1608,29 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
             url = url.replace(/advanced/, '/record/' + $scope.record_id);
         }
 
+        // Jumping directly to the results page by passing a search
+        // query via the URL.  Copy all URL params to the iframe url.
+        if ($location.path().match(/catalog\/results/)) {
+            url = url.replace(/advanced/, '/results?');
+            var first = true;
+            angular.forEach($location.search(), function(val, key) {
+                if (!first) url += '&';
+                first = false;
+                url += encodeURIComponent(key) 
+                    + '=' + encodeURIComponent(val);
+            });
+        }
+
+        // if we're displaying the advanced search form, select
+        // whatever default pane the user has chosen via workstation
+        // preference
+        if (url.match(/\/opac\/advanced$/)) {
+            var adv_pane = egCore.hatch.getLocalItem('eg.search.adv_pane');
+            if (adv_pane) {
+                url += '?pane=' + encodeURIComponent(adv_pane);
+            }
+        }
+
         $scope.catalog_url = url;
     }
 
@@ -953,6 +1660,7 @@ function($scope , $routeParams , $location , $window , $q , egCore , egHolds , e
                 $scope.detail_hold_record_id = $scope.record_id; 
                 // refresh the holds grid
                 provider.refresh();
+
                 break;
         }
     }
@@ -997,7 +1705,7 @@ function($scope , $location) {
 .controller('VandelayCtrl',
        ['$scope','$location',
 function($scope , $location) {
-    $scope.vandelay_url = $location.absUrl().replace(/\/staff.*/, '/vandelay/vandelay');
+    $scope.vandelay_url = $location.absUrl().replace(/\/staff\/cat\/catalog\/vandelay/, '/vandelay/vandelay');
 }])
 
 .controller('ManageAuthoritiesCtrl',
@@ -1029,261 +1737,61 @@ function($scope , $location , $routeParams) {
     }
 })
 
-.factory('holdingsSvc', 
+.factory('conjoinedSvc', 
        ['egCore','$q',
 function(egCore , $q) {
 
     var service = {
-        ongoing : false,
-        copies : [], // record search results
+        items : [], // record search results
         index : 0, // search grid index
-        org : null,
         rid : null
     };
 
     service.flesh = {   
-        flesh : 2
+        flesh : 4
         flesh_fields : {
-            acp : ['status','location'],
-            acn : ['prefix','suffix','copies']
-        }
+            bpbcm : ['target_copy','peer_type'],
+            acp : ['call_number'],
+            acn : ['record'],
+            bre : ['simple_record']
+        },
+        // avoid fetching the MARC blob by specifying which
+        // fields on the bre to select.  More may be needed.
+        // note that fleshed fields are explicitly selected.
+        select : { bre : ['id'] },
+        order_by : { bpbcm : ['id'] },
     }
 
     // resolved with the last received copy
-    service.fetch = function(opts) {
-        if (service.ongoing) {
-            console.log('Skipping fetch, ongoing = true');
-            return $q.when();
-        }
-
-        var rid = opts.rid;
-        var org = opts.org;
-        var copy = opts.copy;
-        var vol = opts.vol;
-        var empty = opts.empty;
-
-        if (!rid) return $q.when();
-        if (!org) return $q.when();
+    service.fetch = function(rid) {
+        if (!rid && !service.rid) return $q.when();
 
-        service.ongoing = true;
-
-        service.rid = rid;
-        service.org = org;
-        service.copies = [];
+        if (rid) service.rid = rid;
+        service.items = [];
         service.index = 0;
 
-        var org_list = egCore.org.descendants(org.id(), true);
-        console.log('Holdings fetch with: rid='+rid+' org='+org_list+' copy='+copy+' vol='+vol+' empty='+empty);
-
         return egCore.pcrud.search(
-            'acn',
-            {record : rid, owning_lib : org_list, deleted : 'f'},
-            service.flesh
-        ).then(
-            function() { // finished
-                service.copies = service.copies.sort(
-                    function (a, b) {
-                        function compare_array (x, y, i) {
-                            if (x[i] && y[i]) { // both have values
-                                if (x[i] == y[i]) { // need to look deeper
-                                    return compare_array(x, y, ++i);
-                                }
-
-                                if (x[i] < y[i]) { // x is first
-                                    return -1;
-                                } else if (x[i] > y[i]) { // y is first
-                                    return 1;
-                                }
-
-                            } else { // no orgs to compare ...
-                                if (x[i]) return -1;
-                                if (y[i]) return 1;
-                            }
-                            return 0;
-                        }
-
-                        var owner_order = compare_array(a.owner_list, b.owner_list, 0);
-                        if (!owner_order) {
-                            // now compare on CN label
-                            if (a.call_number.label < b.call_number.label) return -1;
-                            if (a.call_number.label > b.call_number.label) return 1;
-
-                            // try copy number
-                            if (a.copy_number < b.copy_number) return -1;
-                            if (a.copy_number > b.copy_number) return 1;
-
-                            // finally, barcode
-                            if (a.barcode < b.barcode) return -1;
-                            if (a.barcode > b.barcode) return 1;
-                        }
-                        return owner_order;
-                    }
-                );
-
-                // create a label using just the unique part of the owner list
-                var index = 0;
-                var prev_owner_list;
-                angular.forEach(service.copies, function (cp) {
-                    if (!prev_owner_list) {
-                        cp.owner_label = cp.owner_list.join(' ... ');
-                    } else {
-                        var current_owner_list = cp.owner_list.slice();
-                        while (current_owner_list[1] && prev_owner_list[1] && current_owner_list[0] == prev_owner_list[0]) {
-                            current_owner_list.shift();
-                            prev_owner_list.shift();
-                        }
-                        cp.owner_label = current_owner_list.join(' ... ');
-                    }
-
-                    cp.index = index++;
-                    prev_owner_list = cp.owner_list.slice();
-                });
-
-                var new_list = service.copies;
-                if (!copy || !vol) { // collapse copy rows, supply a count instead
-
-                    index = 0;
-                    var cp_list = [];
-                    var prev_key;
-                    var current_blob = { copy_count : 0 };
-                    angular.forEach(new_list, function (cp) {
-                        if (!prev_key) {
-                            prev_key = cp.owner_list.join('') + cp.call_number.label;
-                            if (cp.barcode) current_blob.copy_count = 1;
-                            current_blob.index = index++;
-                            current_blob.id_list = cp.id_list;
-                            if (cp.raw) current_blob.raw = cp.raw;
-                            current_blob.call_number = cp.call_number;
-                            current_blob.owner_list = cp.owner_list;
-                            current_blob.owner_label = cp.owner_label;
-                            current_blob.owner_id = cp.owner_id;
-                        } else {
-                            var current_key = cp.owner_list.join('') + cp.call_number.label;
-                            if (prev_key == current_key) { // collapse into current_blob
-                                current_blob.copy_count++;
-                                current_blob.id_list = current_blob.id_list.concat(cp.id_list);
-                                current_blob.raw = current_blob.raw.concat(cp.raw);
-                            } else {
-                                current_blob.barcode = current_blob.copy_count;
-                                cp_list.push(current_blob);
-                                prev_key = current_key;
-                                current_blob = { copy_count : 0 };
-                                if (cp.barcode) current_blob.copy_count = 1;
-                                current_blob.index = index++;
-                                current_blob.id_list = cp.id_list;
-                                if (cp.raw) current_blob.raw = cp.raw;
-                                current_blob.owner_label = cp.owner_label;
-                                current_blob.owner_id = cp.owner_id;
-                                current_blob.call_number = cp.call_number;
-                                current_blob.owner_list = cp.owner_list;
-                            }
-                        }
-                    });
-
-                    current_blob.barcode = current_blob.copy_count;
-                    cp_list.push(current_blob);
-                    new_list = cp_list;
-
-                    if (!vol) { // do the same for vol rows
-
-                        index = 0;
-                        var cn_list = [];
-                        prev_key = '';
-                        current_blob = { copy_count : 0 };
-                        angular.forEach(cp_list, function (cp) {
-                            if (!prev_key) {
-                                prev_key = cp.owner_list.join('');
-                                current_blob.index = index++;
-                                current_blob.id_list = cp.id_list;
-                                if (cp.raw) current_blob.raw = cp.raw;
-                                current_blob.cn_count = 1;
-                                current_blob.copy_count = cp.copy_count;
-                                current_blob.owner_list = cp.owner_list;
-                                current_blob.owner_label = cp.owner_label;
-                                current_blob.owner_id = cp.owner_id;
-                            } else {
-                                var current_key = cp.owner_list.join('');
-                                if (prev_key == current_key) { // collapse into current_blob
-                                    current_blob.cn_count++;
-                                    current_blob.copy_count += cp.copy_count;
-                                    current_blob.id_list = current_blob.id_list.concat(cp.id_list);
-                                    if (cp.raw) current_blob.raw = current_blob.raw.concat(cp.raw);
-                                } else {
-                                    current_blob.barcode = current_blob.copy_count;
-                                    current_blob.call_number = { label : current_blob.cn_count };
-                                    cn_list.push(current_blob);
-                                    prev_key = current_key;
-                                    current_blob = { copy_count : 0 };
-                                    current_blob.index = index++;
-                                    current_blob.id_list = cp.id_list;
-                                    if (cp.raw) current_blob.raw = cp.raw;
-                                    current_blob.owner_label = cp.owner_label;
-                                    current_blob.owner_id = cp.owner_id;
-                                    current_blob.cn_count = 1;
-                                    current_blob.copy_count = cp.copy_count;
-                                    current_blob.owner_list = cp.owner_list;
-                                }
-                            }
-                        });
-    
-                        current_blob.barcode = current_blob.copy_count;
-                        current_blob.call_number = { label : current_blob.cn_count };
-                        cn_list.push(current_blob);
-                        new_list = cn_list;
-    
-                    }
-                }
-
-                service.copies = new_list;
-                service.ongoing = false;
-            },
-
-            null, // error
-
-            // notify reads the stream of copies, one at a time.
-            function(cn) {
-
-                var copies = cn.copies().filter(function(cp){ return cp.deleted() == 'f' });
-                cn.copies([]);
-
-                angular.forEach(copies, function (cp) {
-                    cp.call_number(cn);
-                });
-
-                var owner_id = cn.owning_lib();
-                var owner = egCore.org.get(owner_id);
-
-                var owner_name_list = [];
-                while (owner.parent_ou()) { // we're going to skip the top of the tree...
-                    owner_name_list.unshift(owner.name());
-                    owner = egCore.org.get(owner.parent_ou());
-                }
-
-                if (copies[0]) {
-                    var flat = [];
-                    angular.forEach(copies, function (cp) {
-                        var flat_cp = egCore.idl.toHash(cp);
-                        flat_cp.owner_id = owner_id;
-                        flat_cp.owner_list = owner_name_list;
-                        flat_cp.id_list = [flat_cp.id];
-                        flat_cp.raw = [cp];
-                        flat.push(flat_cp);
-                    });
+            'bpbcm',
+            {peer_record : service.rid},
+            service.flesh,
+            {atomic : true}
+        ).then( function(list) { // finished
+            service.items = list;
+            return service.items;
+        });
+    }
 
-                    service.copies = service.copies.concat(flat);
-                } else if (empty) {
-                    service.copies.push({
-                        owner_id   : owner_id,
-                        owner_list : owner_name_list,
-                        call_number: egCore.idl.toHash(cn),
-                        raw_call_number: cn
-                    });
-                }
+    // returns a promise resolved with the list of peer bib types
+    service.get_peer_types = function() {
+        if (egCore.env.bpt)
+            return $q.when(egCore.env.bpt.list);
 
-                return cn;
-            }
-        );
-    }
+        return egCore.pcrud.retrieveAll('bpt', null, {atomic : true})
+        .then(function(list) {
+            egCore.env.absorbList(list, 'bpt');
+            return list;
+        });
+    };
 
     return service;
 }])