]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/cat/catalog/app.js
LP#1792188: Record -> View Holds fails to refresh on Next/Prev record
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / cat / catalog / app.js
1 /**
2  * TPAC Frame App
3  *
4  * currently, this app doesn't use routes for each sub-ui, because 
5  * reloading the catalog each time is sloooow.  better so far to 
6  * swap out divs w/ ng-if / ng-show / ng-hide as needed.
7  *
8  */
9
10 angular.module('egCatalogApp', ['ui.bootstrap','ngRoute','ngLocationUpdate','egCoreMod','egGridMod', 'egMarcMod', 'egUserMod', 'egHoldingsMod', 'ngToast','egPatronSearchMod',
11 'egSerialsMod','egSerialsAppDep'])
12
13 .config(['ngToastProvider', function(ngToastProvider) {
14   ngToastProvider.configure({
15     verticalPosition: 'bottom',
16     animation: 'fade'
17   });
18 }])
19
20 .config(function($routeProvider, $locationProvider, $compileProvider) {
21     $locationProvider.html5Mode(true);
22     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|mailto|blob):/); // grid export
23         
24     var resolver = {delay : ['egCore','egStartup','egUser', function(egCore, egStartup, egUser) {
25         egCore.env.classLoaders.aous = function() {
26             return egCore.org.settings([
27                 'cat.marc_control_number_identifier'
28             ]).then(function(settings) {
29                 // local settings are cached within egOrg.  Caching them
30                 // again in egEnv just simplifies the syntax for access.
31                 egCore.env.aous = settings;
32             });
33         }
34         egCore.env.loadClasses.push('aous');
35         return egStartup.go()
36     }]};
37
38     $routeProvider.when('/cat/catalog/index', {
39         templateUrl: './cat/catalog/t_catalog',
40         controller: 'CatalogCtrl',
41         resolve : resolver
42     });
43
44     // Jump directly to the results page.  Any URL parameter 
45     // supported by the embedded catalog is supported here.
46     $routeProvider.when('/cat/catalog/results', {
47         templateUrl: './cat/catalog/t_catalog',
48         controller: 'CatalogCtrl',
49         resolve : resolver
50     });
51
52     $routeProvider.when('/cat/catalog/retrieve_by_id', {
53         templateUrl: './cat/catalog/t_retrieve_by_id',
54         controller: 'CatalogRecordRetrieve',
55         resolve : resolver
56     });
57
58     $routeProvider.when('/cat/catalog/retrieve_by_tcn', {
59         templateUrl: './cat/catalog/t_retrieve_by_tcn',
60         controller: 'CatalogRecordRetrieve',
61         resolve : resolver
62     });
63
64     $routeProvider.when('/cat/catalog/retrieve_by_authority_id', {
65         templateUrl: './cat/catalog/t_retrieve_by_authority_id',
66         controller: 'CatalogRecordRetrieve',
67         resolve : resolver
68     });
69
70     $routeProvider.when('/cat/catalog/new_bib', {
71         templateUrl: './cat/catalog/t_new_bib',
72         controller: 'NewBibCtrl',
73         resolve : resolver
74     });
75
76     // create some catalog page-specific mappings
77     $routeProvider.when('/cat/catalog/record/:record_id', {
78         templateUrl: './cat/catalog/t_catalog',
79         controller: 'CatalogCtrl',
80         resolve : resolver
81     });
82
83     // create some catalog page-specific mappings
84     $routeProvider.when('/cat/catalog/record/:record_id/:record_tab', {
85         templateUrl: './cat/catalog/t_catalog',
86         controller: 'CatalogCtrl',
87         resolve : resolver
88     });
89
90     $routeProvider.when('/cat/catalog/batchEdit', {
91         templateUrl: './cat/catalog/t_batchedit',
92         controller: 'BatchEditCtrl',
93         resolve : resolver
94     });
95
96     $routeProvider.when('/cat/catalog/batchEdit/:container_type/:container_id', {
97         templateUrl: './cat/catalog/t_batchedit',
98         controller: 'BatchEditCtrl',
99         resolve : resolver
100     });
101
102     $routeProvider.when('/cat/catalog/vandelay', {
103         templateUrl: './cat/catalog/t_vandelay',
104         controller: 'VandelayCtrl',
105         resolve : resolver
106     });
107
108     $routeProvider.when('/cat/catalog/verifyURLs', {
109         templateUrl: './cat/catalog/t_verifyurls',
110         controller: 'URLVerifyCtrl',
111         resolve : resolver
112     });
113
114     $routeProvider.when('/cat/catalog/manageAuthorities', {
115         templateUrl: './cat/catalog/t_manageauthorities',
116         controller: 'ManageAuthoritiesCtrl',
117         resolve : resolver
118     });
119
120     $routeProvider.when('/cat/catalog/authority/:authority_id/marc_edit', {
121         templateUrl: './cat/catalog/t_authority',
122         controller: 'AuthorityCtrl',
123         resolve : resolver
124     });
125
126     $routeProvider.otherwise({redirectTo : '/cat/catalog/index'});
127 })
128
129
130 /**
131  * */
132 .controller('CatalogRecordRetrieve',
133        ['$scope','$routeParams','$location','$q','egCore',
134 function($scope , $routeParams , $location , $q , egCore ) {
135
136     $scope.focusMe = true;
137
138     // jump to the patron checkout UI
139     function loadRecord(record_id) {
140         $location
141         .path('/cat/catalog/record/' + record_id);
142     }
143
144     function loadAuthorityRecord(record_id) {
145         $location
146         .path('/cat/catalog/authority/' + record_id + '/marc_edit');
147     }
148
149     $scope.submitId = function(args) {
150         $scope.recordNotFound = null;
151         if (!args.record_id) return;
152
153         // blur so next time it's set to true it will re-apply select()
154         $scope.selectMe = false;
155
156         return loadRecord(args.record_id);
157     }
158
159     $scope.submitAuthorityId = function(args) {
160         if (!args.record_id) return;
161
162         // blur so next time it's set to true it will re-apply select()
163         $scope.selectMe = false;
164
165         return loadAuthorityRecord(args.record_id);
166     }
167
168     $scope.submitTCN = function(args) {
169         $scope.recordNotFound = null;
170         $scope.moreRecordsFound = null;
171         if (!args.record_tcn) return;
172
173         // blur so next time it's set to true it will re-apply select()
174         $scope.selectMe = false;
175
176         // lookup TCN
177         egCore.net.request(
178             'open-ils.search',
179             'open-ils.search.biblio.tcn',
180             args.record_tcn)
181
182         .then(function(resp) { // get_barcodes
183
184             if (evt = egCore.evt.parse(resp)) {
185                 alert(evt); // FIXME
186                 return;
187             }
188
189             if (!resp.count) {
190                 $scope.recordNotFound = args.record_tcn;
191                 $scope.selectMe = true;
192                 return;
193             }
194
195             if (resp.count > 1) {
196                 $scope.moreRecordsFound = args.record_tcn;
197                 $scope.selectMe = true;
198                 return;
199             }
200
201             var record_id = resp.ids[0];
202             return loadRecord(record_id);
203         });
204     }
205
206 }])
207
208 .controller('NewBibCtrl',
209        ['$scope','$routeParams','$location','$window','$q','egCore',
210         'egGridDataProvider','egHoldGridActions','$timeout','holdingsSvc',
211 function($scope , $routeParams , $location , $window , $q , egCore) {
212
213     $scope.have_template = false;
214     $scope.marc_template = '';
215     $scope.stop_unload = false;
216     $scope.template_list = [];
217     $scope.template_name = '';
218     $scope.new_bib_id = 0;
219
220     egCore.net.request(
221         'open-ils.cat',
222         'open-ils.cat.marc_template.types.retrieve'
223     ).then(function(resp) {
224         angular.forEach(resp, function(name) {
225             $scope.template_list.push(name);
226         });
227         $scope.template_list.sort();
228     });
229     $scope.template_name = egCore.hatch.getSessionItem('eg.cat.last_bib_marc_template');
230     if (!$scope.template_name) {
231         egCore.hatch.getItem('cat.default_bib_marc_template').then(function(template) {
232             $scope.template_name = template;
233         });
234     }
235
236     $scope.loadTemplate = function() {
237         if ($scope.template_name) {
238             egCore.net.request(
239                 'open-ils.cat',
240                 'open-ils.cat.biblio.marc_template.retrieve',
241                 $scope.template_name
242             ).then(function(template) {
243                 $scope.marc_template = template;
244                 $scope.have_template = true;
245                 egCore.hatch.setSessionItem('eg.cat.last_bib_marc_template', $scope.template_name);
246             });
247         }
248     }
249
250     $scope.setDefaultTemplate = function() {
251         var hatch_key = "cat.default_bib_marc_template";
252         if ($scope.template_name) {
253             egCore.hatch.setItem(hatch_key, $scope.template_name);
254         } else {
255             egCore.hatch.removeItem(hatch_key);
256         }
257     }
258
259     $scope.$watch('new_bib_id', function(newVal, oldVal) {
260         if (newVal) {
261             $location.path('/cat/catalog/record/' + $scope.new_bib_id);
262         }
263     });
264     
265
266 }])
267 .controller('CatalogCtrl',
268        ['$scope','$routeParams','$location','$window','$q','egCore','egHolds','egCirc','egConfirmDialog','ngToast',
269         'egGridDataProvider','egHoldGridActions','egProgressDialog','$timeout','$uibModal','holdingsSvc','egUser','conjoinedSvc',
270         '$cookies','egSerialsCoreSvc',
271 function($scope , $routeParams , $location , $window , $q , egCore , egHolds , egCirc , egConfirmDialog , ngToast ,
272          egGridDataProvider , egHoldGridActions , egProgressDialog , $timeout , $uibModal , holdingsSvc , egUser , conjoinedSvc,
273          $cookies , egSerialsCoreSvc
274 ) {
275
276     var holdingsSvcInst = new holdingsSvc();
277
278     // set record ID on page load if available...
279     $scope.record_id = $routeParams.record_id;
280     $scope.summary_pane_record;
281
282     if ($scope.record_id) {
283         // TODO: Apply tab-specific title contexts
284         egCore.strings.setPageTitle(
285             egCore.strings.PAGE_TITLE_BIB_DETAIL,
286             egCore.strings.PAGE_TITLE_CATALOG_CONTEXT,
287             {record_id : $scope.record_id}
288         );
289     } else {
290         // Default to title = Catalog
291         egCore.strings.setPageTitle(
292             egCore.strings.PAGE_TITLE_CATALOG_CONTEXT);
293     }
294
295     if ($routeParams.record_id) $scope.from_route = true;
296     else $scope.from_route = false;
297
298     // set search and preferred library cookies
299     egCore.hatch.getItem('eg.search.search_lib').then(function(val) {
300         $cookies.put('eg_search_lib', val, { path : '/' });
301     });
302     egCore.hatch.getItem('eg.search.pref_lib').then(function(val) {
303         $cookies.put('eg_pref_lib', val, { path : '/' });
304     });
305
306     // will hold a ref to the opac iframe
307     $scope.opac_iframe = null;
308     $scope.parts_iframe = null;
309
310     $scope.search_result_index = 1;
311     $scope.search_result_hit_count = 1;
312
313     $scope.$watch(
314         'opac_iframe.dom.contentWindow.search_result_index',
315         function (n,o) {
316             if (!isNaN(parseInt(n)))
317                 $scope.search_result_index = n + 1;
318         }
319     );
320
321     $scope.$watch(
322         'opac_iframe.dom.contentWindow.search_result_hit_count',
323         function (n,o) {
324             if (!isNaN(parseInt(n)))
325                 $scope.search_result_hit_count = n;
326         }
327     );
328
329     $scope.in_opac_call = false;
330     $scope.opac_call = function (opac_frame_function, force_opac_tab) {
331         if ($scope.opac_iframe) {
332             if (force_opac_tab) $scope.record_tab = 'catalog';
333             $scope.in_opac_call = true;
334             $scope.opac_iframe.dom.contentWindow[opac_frame_function]();
335             if (opac_frame_function == 'rdetailBackToResults') {
336                 $location.update_path('/cat/catalog/index');
337             }
338         }
339     }
340
341     $scope.add_cart_to_record_bucket = function() {
342         var cartkey = $cookies.get('cartcache');
343         if (!cartkey) return;
344         egCore.net.request(
345             'open-ils.actor',
346             'open-ils.actor.anon_cache.get_value',
347             cartkey,
348             'mylist'
349         ).then(function(list) {
350             list = list.map(function(x) {
351                 return parseInt(x);
352             });
353             $scope.add_to_record_bucket(list);
354         });
355     }
356
357     $scope.add_to_record_bucket = function(recs) {
358         if (!angular.isArray(recs)) {
359             recs = [ $scope.record_id ];
360         }
361         return $uibModal.open({
362             templateUrl: './cat/catalog/t_add_to_bucket',
363             backdrop: 'static',
364             animation: true,
365             size: 'md',
366             controller:
367                    ['$scope','$uibModalInstance',
368             function($scope , $uibModalInstance) {
369
370                 $scope.bucket_id = 0;
371                 $scope.newBucketName = '';
372                 $scope.allBuckets = [];
373                 egCore.net.request(
374                     'open-ils.actor',
375                     'open-ils.actor.container.retrieve_by_class.authoritative',
376                     egCore.auth.token(), egCore.auth.user().id(),
377                     'biblio', 'staff_client'
378                 ).then(function(buckets) { $scope.allBuckets = buckets; });
379
380                 $scope.add_to_bucket = function() {
381                     var promises = [];
382                     angular.forEach(recs, function(recId) {
383                         var item = new egCore.idl.cbrebi();
384                         item.bucket($scope.bucket_id);
385                         item.target_biblio_record_entry(recId);
386                         promises.push(egCore.net.request(
387                             'open-ils.actor',
388                             'open-ils.actor.container.item.create',
389                             egCore.auth.token(), 'biblio', item
390                         ));
391                     });
392                     $q.all(promises).then(function(resp) {
393                         $uibModalInstance.close();
394                     });
395                 }
396
397                 $scope.add_to_new_bucket = function() {
398                     var bucket = new egCore.idl.cbreb();
399                     bucket.owner(egCore.auth.user().id());
400                     bucket.name($scope.newBucketName);
401                     bucket.description('');
402                     bucket.btype('staff_client');
403
404                     egCore.net.request(
405                         'open-ils.actor',
406                         'open-ils.actor.container.create',
407                         egCore.auth.token(), 'biblio', bucket
408                     ).then(function(bucket) {
409                         $scope.bucket_id = bucket;
410                         $scope.add_to_bucket();
411                     });
412                 }
413
414                 $scope.cancel = function() {
415                     $uibModalInstance.dismiss();
416                 }
417             }]
418         });
419     }
420
421     $scope.current_overlay_target     = egCore.hatch.getLocalItem('eg.cat.marked_overlay_record');
422     $scope.current_transfer_target    = egCore.hatch.getLocalItem('eg.cat.transfer_target_record');
423     $scope.current_conjoined_target   = egCore.hatch.getLocalItem('eg.cat.marked_conjoined_record');
424
425     $scope.quickReceive = function () {
426         var list = [];
427         var next_per_stream = {};
428
429         var recId = $scope.record_id;
430         return $uibModal.open({
431             templateUrl: './share/t_subscription_select_dialog',
432             backdrop: 'static',
433             controller: ['$scope', '$uibModalInstance',
434                 function($scope, $uibModalInstance) {
435
436                     $scope.focus = true;
437                     $scope.rememberMe = 'eg.serials.quickreceive.last_org';
438                     $scope.record_id = recId;
439                     $scope.ssubId = null;
440
441                     $scope.ok = function() { $uibModalInstance.close($scope.ssubId) }
442                     $scope.cancel = function() { $uibModalInstance.dismiss(); }
443                 }
444             ]
445         }).result.then(function(ssubId) {
446             if (ssubId) {
447                 var promises = [];
448                 promises.push(egSerialsCoreSvc.fetchItemsForSub(ssubId,{status:'Expected'}).then(function(){
449                     angular.forEach(egSerialsCoreSvc.itemTree, function (item) {
450                         if (next_per_stream[item.stream().id()]) return;
451                         if (item.status() == 'Expected') {
452                             next_per_stream[item.stream().id()] = item;
453                             list.push(egCore.idl.Clone(item));
454                         }
455                     });
456                 }));
457
458                 return $q.all(promises).then(function() {
459
460                     if (!list.length) {
461                         ngToast.warning(egCore.strings.SERIALS_NO_ITEMS);
462                         return $q.reject();
463                     }
464
465                     return egSerialsCoreSvc.process_items(
466                         'receive',
467                         $scope.record_id,
468                         list,
469                         true, // barcode
470                         false,// bind
471                         false, // print by default
472                         function() { $scope.holdings_record_id_changed($scope.record_id) }
473                     );
474                 });
475             } else {
476                 ngToast.warning(egCore.strings.SERIALS_NO_SUBS);
477                 return $q.reject();
478             }
479         });
480     }
481
482     $scope.markConjoined = function () {
483         $scope.current_conjoined_target = $scope.record_id;
484         egCore.hatch.setLocalItem('eg.cat.marked_conjoined_record',$scope.record_id);
485         ngToast.create(egCore.strings.MARK_CONJ_TARGET);
486     };
487
488     $scope.markHoldingsTransfer = function () {
489         $scope.current_transfer_target = $scope.record_id;
490         egCore.hatch.setLocalItem('eg.cat.transfer_target_record',$scope.record_id);
491         egCore.hatch.removeLocalItem('eg.cat.transfer_target_lib');
492         egCore.hatch.removeLocalItem('eg.cat.transfer_target_vol');
493         ngToast.create(egCore.strings.MARK_HOLDINGS_TARGET);
494     };
495
496     $scope.markOverlay = function () {
497         $scope.current_overlay_target = $scope.record_id;
498         egCore.hatch.setLocalItem('eg.cat.marked_overlay_record',$scope.record_id);
499         ngToast.create(egCore.strings.MARK_OVERLAY_TARGET);
500     };
501
502     $scope.clearRecordMarks = function () {
503         $scope.current_overlay_target     = null;
504         $scope.current_transfer_target    = null;
505         $scope.current_conjoined_target   = null;
506         $scope.current_hold_transfer_dest = null;
507         egCore.hatch.removeLocalItem('eg.cat.transfer_target_record');
508         egCore.hatch.removeLocalItem('eg.cat.marked_conjoined_record');
509         egCore.hatch.removeLocalItem('eg.cat.marked_overlay_record');
510         egCore.hatch.removeLocalItem('eg.circ.hold.title_transfer_target');
511     }
512
513     $scope.stop_unload = false;
514     $scope.$watch('stop_unload',
515         function(newVal, oldVal) {
516             if (newVal && newVal != oldVal && $scope.opac_iframe) {
517                 $($scope.opac_iframe.dom.contentWindow).on('beforeunload', function(){
518                     return 'There is unsaved data in this record.'
519                 });
520             } else {
521                 if ($scope.opac_iframe)
522                     $($scope.opac_iframe.dom.contentWindow).off('beforeunload');
523             }
524         }
525     );
526
527     // Set the "last bib" cookie, if we have that
528     if ($scope.record_id)
529         egCore.hatch.setLocalItem("eg.cat.last_record_retrieved", $scope.record_id);
530
531     $scope.refresh_record_callback = function (record_id) {
532         egCore.pcrud.retrieve('bre', record_id, {
533             flesh : 1,
534             flesh_fields : {
535                 bre : ['simple_record','creator','editor']
536             }
537         }).then(function(rec) {
538             rec.owner(egCore.org.get(rec.owner()));
539             $scope.summary_pane_record = rec;
540         });
541
542         return record_id;
543     }
544
545     patron_search_dialog = function() {
546         return $uibModal.open({
547             templateUrl: './share/t_patron_selector',
548             backdrop: 'static',
549             size: 'lg',
550             animation: true,
551             controller:
552                    ['$scope','$uibModalInstance','$controller',
553             function($scope , $uibModalInstance , $controller) {
554                 angular.extend(this, $controller('BasePatronSearchCtrl', {$scope : $scope}));
555                 $scope.clearForm();
556                 $scope.need_one_selected = function() {
557                     var items = $scope.gridControls.selectedItems();
558                     return (items.length == 1) ? false : true
559                 }
560                 $scope.ok = function() {
561                     var items = $scope.gridControls.selectedItems();
562                     if (items.length == 1) {
563                         $uibModalInstance.close(items[0].card().barcode());
564                     } else {
565                         $uibModalInstance.close()
566                     }
567                 }
568                 $scope.cancel = function($event) {
569                     $uibModalInstance.dismiss();
570                     $event.preventDefault();
571                 }
572             }]
573         });
574     }
575
576     // also set it when the iframe changes to a new record
577     $scope.handle_page = function(url) {
578
579         if (!url || url == 'about:blank') {
580             // nothing loaded.  If we already have a record ID, leave it.
581             return;
582         }
583
584         var prev_record_id = $scope.record_id;
585         var match = url.match(/\/+opac\/+record\/+(\d+)/);
586         if (match) {
587             $scope.record_id = match[1];
588             egCore.hatch.setLocalItem("eg.cat.last_record_retrieved", $scope.record_id);
589             $scope.holdings_record_id_changed($scope.record_id);
590             conjoinedSvc.fetch($scope.record_id).then(function(){
591                 $scope.conjoinedGridDataProvider.refresh();
592             });
593             init_parts_url();
594             $scope.grid_actions.refresh();
595             $location.update_path('/cat/catalog/record/' + $scope.record_id);
596             // update_path() bypasses the controller for path 
597             // /cat/catalog/record/:record_id. Manually set title here too.
598             egCore.strings.setPageTitle(
599                 egCore.strings.PAGE_TITLE_BIB_DETAIL,
600                 egCore.strings.PAGE_TITLE_CATALOG_CONTEXT,
601                 {record_id : $scope.record_id}
602             );
603         } else {
604             delete $scope.record_id;
605             $scope.from_route = false;
606         }
607
608         // child scope is executing this function, so our digest doesn't fire ... thus,
609         $scope.$apply();
610
611         // don't change tabs if we are using the OPAC nav buttons,
612         // or we didn't change records on the OPAC load
613         if (!$scope.in_opac_call && ($scope.record_id != prev_record_id)) {
614             if ($scope.record_id) {
615                 $scope.default_tab = egCore.hatch.getLocalItem( 'eg.cat.default_record_tab' );
616                 tab = $routeParams.record_tab || $scope.default_tab || 'catalog';
617             } else {
618                 tab = $routeParams.record_tab || 'catalog';
619             }
620             $scope.set_record_tab(tab);
621         } else {
622             $scope.in_opac_call = false;
623         }
624
625         if ($scope.opac_iframe && $location.path().match(/cat\/catalog/)) {
626             var doc = $scope.opac_iframe.dom.contentWindow.document;
627             $(doc).find('#hold_usr_search').show();
628             $(doc).find('#hold_usr_search').on('click', function() {
629                 patron_search_dialog().result.then(function(barc) {
630                     $(doc).find('#hold_usr_input').val(barc);
631                     $(doc).find('#hold_usr_input').change();
632                 });
633             });
634             $(doc).find('#select_basket_action').on('change', function() {
635                 if (this.options[this.selectedIndex].value && this.options[this.selectedIndex].value == "add_cart_to_bucket") {
636                     $scope.add_cart_to_record_bucket();
637                 }
638             });
639         }
640
641     }
642
643     // xulG catalog handlers
644     $scope.handlers = { }
645
646     // ------------------------------------------------------------------
647     // Conjoined items
648
649     $scope.conjoinedGridControls = {};
650     $scope.conjoinedGridDataProvider = egGridDataProvider.instance({
651         get : function(offset, count) {
652             return this.arrayNotifier(conjoinedSvc.items, offset, count);
653         }
654     });
655
656     $scope.changeConjoinedType = function () {
657         var peers = egCore.idl.Clone($scope.conjoinedGridControls.selectedItems());
658         angular.forEach(peers, function (p) {
659             p.target_copy(p.target_copy().id());
660             p.peer_type(p.peer_type().id());
661         });
662
663         var conjoinedGridDataProviderRef = $scope.conjoinedGridDataProvider;
664
665         return $uibModal.open({
666             templateUrl: './cat/catalog/t_conjoined_selector',
667             backdrop: 'static',
668             animation: true,
669             controller:
670                    ['$scope','$uibModalInstance',
671             function($scope , $uibModalInstance) {
672                 $scope.update = true;
673
674                 $scope.peer_type = null;
675                 $scope.peer_type_list = [];
676                 conjoinedSvc.get_peer_types().then(function(list){
677                     $scope.peer_type_list = list;
678                 });
679     
680                 $scope.ok = function(type) {
681                     var promises = [];
682     
683                     angular.forEach(peers, function (p) {
684                         p.ischanged(1);
685                         p.peer_type(type);
686                         promises.push(egCore.pcrud.update(p));
687                     });
688     
689                     return $q.all(promises)
690                         .then(function(){$uibModalInstance.close()})
691                         .then(function(){return conjoinedSvc.fetch()})
692                         .then(function(){conjoinedGridDataProviderRef.refresh()});
693                 }
694     
695                 $scope.cancel = function($event) {
696                     $uibModalInstance.dismiss();
697                     $event.preventDefault();
698                 }
699             }]
700         });
701         
702     }
703
704     $scope.refreshConjoined = function () {
705         conjoinedSvc.fetch($scope.record_id)
706         .then(function(){$scope.conjoinedGridDataProvider.refresh();});
707     }
708
709     $scope.deleteSelectedConjoined = function () {
710         var peers = $scope.conjoinedGridControls.selectedItems();
711
712         if (peers.length > 0) {
713             egConfirmDialog.open(
714                 egCore.strings.CONFIRM_DELETE_PEERS,
715                 egCore.strings.CONFIRM_DELETE_PEERS_MESSAGE,
716                 {peers : peers.length}
717             ).result.then(function() {
718                 angular.forEach(peers, function (p) {
719                     p.isdeleted(1);
720                 });
721
722                 egCore.pcrud.remove(peers).then(function() {
723                     return conjoinedSvc.fetch();
724                 }).then(function() {
725                     $scope.conjoinedGridDataProvider.refresh();
726                 });
727             });
728         }
729     }
730     if ($scope.record_id)
731         conjoinedSvc.fetch($scope.record_id);
732
733     // ------------------------------------------------------------------
734     // Holdings
735
736     $scope.holdingsGridControls = {
737         activateItem : function (item) {
738             $scope.selectedHoldingsVolCopyEdit();
739         }
740     };
741     $scope.holdingsGridDataProvider = egGridDataProvider.instance({
742         get : function(offset, count) {
743             return this.arrayNotifier(holdingsSvcInst.copies, offset, count);
744         }
745     });
746
747     $scope.add_copies_to_bucket = function() {
748         var copy_list = gatherSelectedHoldingsIds();
749         if (copy_list.length == 0) return;
750
751         return $uibModal.open({
752             templateUrl: './cat/catalog/t_add_to_bucket',
753             backdrop: 'static',
754             animation: true,
755             size: 'md',
756             controller:
757                    ['$scope','$uibModalInstance',
758             function($scope , $uibModalInstance) {
759
760                 $scope.bucket_id = 0;
761                 $scope.newBucketName = '';
762                 $scope.allBuckets = [];
763
764                 egCore.net.request(
765                     'open-ils.actor',
766                     'open-ils.actor.container.retrieve_by_class.authoritative',
767                     egCore.auth.token(), egCore.auth.user().id(),
768                     'copy', 'staff_client'
769                 ).then(function(buckets) { $scope.allBuckets = buckets; });
770
771                 $scope.add_to_bucket = function() {
772                     var promises = [];
773                     angular.forEach(copy_list, function (cp) {
774                         var item = new egCore.idl.ccbi()
775                         item.bucket($scope.bucket_id);
776                         item.target_copy(cp);
777                         promises.push(
778                             egCore.net.request(
779                                 'open-ils.actor',
780                                 'open-ils.actor.container.item.create',
781                                 egCore.auth.token(), 'copy', item
782                             )
783                         );
784
785                         return $q.all(promises).then(function() {
786                             $uibModalInstance.close();
787                         });
788                     });
789                 }
790
791                 $scope.add_to_new_bucket = function() {
792                     var bucket = new egCore.idl.ccb();
793                     bucket.owner(egCore.auth.user().id());
794                     bucket.name($scope.newBucketName);
795                     bucket.description('');
796                     bucket.btype('staff_client');
797
798                     return egCore.net.request(
799                         'open-ils.actor',
800                         'open-ils.actor.container.create',
801                         egCore.auth.token(), 'copy', bucket
802                     ).then(function(bucket) {
803                         $scope.bucket_id = bucket;
804                         $scope.add_to_bucket();
805                     });
806                 }
807
808                 $scope.cancel = function() {
809                     $uibModalInstance.dismiss();
810                 }
811             }]
812         });
813     }
814
815     // TODO: refactor common code between cat/catalog/app.js and cat/item/app.js 
816
817     $scope.need_one_selected = function() {
818         var items = $scope.holdingsGridControls.selectedItems();
819         if (items.length == 1) return false;
820         return true;
821     };
822
823     $scope.make_copies_bookable = function() {
824
825         var copies_by_record = {};
826         var record_list = [];
827         angular.forEach(
828             $scope.holdingsGridControls.selectedItems(),
829             function (item) {
830                 var record_id = item['call_number.record.id'];
831                 if (typeof copies_by_record[ record_id ] == 'undefined') {
832                     copies_by_record[ record_id ] = [];
833                     record_list.push( record_id );
834                 }
835                 copies_by_record[ record_id ].push(item.id);
836             }
837         );
838
839         var promises = [];
840         var combined_results = [];
841         angular.forEach(record_list, function(record_id) {
842             promises.push(
843                 egCore.net.request(
844                     'open-ils.booking',
845                     'open-ils.booking.resources.create_from_copies',
846                     egCore.auth.token(),
847                     copies_by_record[record_id]
848                 ).then(function(results) {
849                     if (results && results['brsrc']) {
850                         combined_results = combined_results.concat(results['brsrc']);
851                     }
852                 })
853             );
854         });
855
856         $q.all(promises).then(function() {
857             if (combined_results.length > 0) {
858                 $uibModal.open({
859                     template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
860                     backdrop: 'static',
861                     animation: true,
862                     size: 'md',
863                     controller:
864                            ['$scope','$location','egCore','$uibModalInstance',
865                     function($scope , $location , egCore , $uibModalInstance) {
866
867                         $scope.funcs = {
868                             ses : egCore.auth.token(),
869                             resultant_brsrc : combined_results.map(function(o) { return o[0]; })
870                         }
871
872                         var booking_path = '/eg/conify/global/booking/resource';
873
874                         $scope.booking_admin_url =
875                             $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
876                     }]
877                 });
878             }
879         });
880     }
881
882     $scope.book_copies_now = function() {
883         var copies_by_record = {};
884         var record_list = [];
885         angular.forEach(
886             $scope.holdingsGridControls.selectedItems(),
887             function (item) {
888                 var record_id = item['call_number.record.id'];
889                 if (typeof copies_by_record[ record_id ] == 'undefined') {
890                     copies_by_record[ record_id ] = [];
891                     record_list.push( record_id );
892                 }
893                 copies_by_record[ record_id ].push(item.id);
894             }
895         );
896
897         var promises = [];
898         var combined_brt = [];
899         var combined_brsrc = [];
900         angular.forEach(record_list, function(record_id) {
901             promises.push(
902                 egCore.net.request(
903                     'open-ils.booking',
904                     'open-ils.booking.resources.create_from_copies',
905                     egCore.auth.token(),
906                     copies_by_record[record_id]
907                 ).then(function(results) {
908                     if (results && results['brt']) {
909                         combined_brt = combined_brt.concat(results['brt']);
910                     }
911                     if (results && results['brsrc']) {
912                         combined_brsrc = combined_brsrc.concat(results['brsrc']);
913                     }
914                 })
915             );
916         });
917
918         $q.all(promises).then(function() {
919             if (combined_brt.length > 0 || combined_brsrc.length > 0) {
920                 $uibModal.open({
921                     template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
922                     backdrop: 'static',
923                     animation: true,
924                     size: 'md',
925                     controller:
926                            ['$scope','$location','egCore','$uibModalInstance',
927                     function($scope , $location , egCore , $uibModalInstance) {
928
929                         $scope.funcs = {
930                             ses : egCore.auth.token(),
931                             bresv_interface_opts : {
932                                 booking_results : {
933                                      brt : combined_brt
934                                     ,brsrc : combined_brsrc
935                                 }
936                             }
937                         }
938
939                         var booking_path = '/eg/booking/reservation';
940
941                         $scope.booking_admin_url =
942                             $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
943
944                     }]
945                 });
946             }
947         });
948     }
949
950
951     $scope.requestItems = function() {
952         var copy_list = gatherSelectedHoldingsIds();
953         if (copy_list.length == 0) return;
954
955         return $uibModal.open({
956             templateUrl: './cat/catalog/t_request_items',
957             animation: true,
958             controller:
959                    ['$scope','$uibModalInstance',
960             function($scope , $uibModalInstance) {
961                 $scope.user = null;
962                 $scope.first_user_fetch = true;
963
964                 $scope.hold_data = {
965                     hold_type : 'C',
966                     copy_list : copy_list,
967                     pickup_lib: egCore.org.get(egCore.auth.user().ws_ou()),
968                     user      : egCore.auth.user().id()
969                 };
970
971                 egUser.get( $scope.hold_data.user ).then(function(u) {
972                     $scope.user = u;
973                     $scope.barcode = u.card().barcode();
974                     $scope.user_name = egUser.format_name(u);
975                     $scope.hold_data.user = u.id();
976                 });
977
978                 $scope.user_name = '';
979                 $scope.barcode = '';
980                 $scope.$watch('barcode', function (n) {
981                     if (!$scope.first_user_fetch) {
982                         egUser.getByBarcode(n).then(function(u) {
983                             $scope.user = u;
984                             $scope.user_name = egUser.format_name(u);
985                             $scope.hold_data.user = u.id();
986                         }, function() {
987                             $scope.user = null;
988                             $scope.user_name = '';
989                             delete $scope.hold_data.user;
990                         });
991                     }
992                     $scope.first_user_fetch = false;
993                 });
994
995                 $scope.ok = function(h) {
996                     var args = {
997                         patronid  : h.user,
998                         hold_type : h.hold_type,
999                         pickup_lib: h.pickup_lib.id(),
1000                         depth     : 0
1001                     };
1002
1003                     egCore.net.request(
1004                         'open-ils.circ',
1005                         'open-ils.circ.holds.test_and_create.batch.override',
1006                         egCore.auth.token(), args, h.copy_list
1007                     );
1008
1009                     $uibModalInstance.close();
1010                 }
1011
1012                 $scope.cancel = function($event) {
1013                     $uibModalInstance.dismiss();
1014                     $event.preventDefault();
1015                 }
1016             }]
1017         });
1018     }
1019
1020     $scope.view_place_orders = function() {
1021         if (!$scope.record_id) return;
1022         var url = egCore.env.basePath + 'acq/legacy/lineitem/related/' + $scope.record_id + '?target=bib';
1023         $timeout(function() { $window.open(url, '_blank') });
1024     }
1025
1026     $scope.replaceBarcodes = function() {
1027         var copy_list = gatherSelectedRawCopies();
1028         if (copy_list.length == 0) return;
1029
1030         var holdingsGridDataProviderRef = $scope.holdingsGridDataProvider;
1031
1032         angular.forEach(copy_list, function (cp) {
1033             $uibModal.open({
1034                 templateUrl: './cat/share/t_replace_barcode',
1035                 backdrop: 'static',
1036                 animation: true,
1037                 controller:
1038                            ['$scope','$uibModalInstance',
1039                     function($scope , $uibModalInstance) {
1040                         $scope.isModal = true;
1041                         $scope.focusBarcode = false;
1042                         $scope.focusBarcode2 = true;
1043                         $scope.barcode1 = cp.barcode();
1044
1045                         $scope.updateBarcode = function() {
1046                             $scope.copyNotFound = false;
1047                             $scope.updateOK = false;
1048                 
1049                             egCore.pcrud.search('acp',
1050                                 {deleted : 'f', barcode : $scope.barcode1})
1051                             .then(function(copy) {
1052                 
1053                                 if (!copy) {
1054                                     $scope.focusBarcode = true;
1055                                     $scope.copyNotFound = true;
1056                                     return;
1057                                 }
1058                 
1059                                 $scope.copyId = copy.id();
1060                                 copy.barcode($scope.barcode2);
1061                 
1062                                 egCore.pcrud.update(copy).then(function(stat) {
1063                                     $scope.updateOK = stat;
1064                                     $scope.focusBarcode = true;
1065                                     holdingsSvc.fetchAgain().then(function (){
1066                                         holdingsGridDataProviderRef.refresh();
1067                                     });
1068                                 });
1069
1070                             });
1071                             $uibModalInstance.close();
1072                         }
1073
1074                         $scope.cancel = function($event) {
1075                             $uibModalInstance.dismiss();
1076                             $event.preventDefault();
1077                         }
1078                     }
1079                 ]
1080             });
1081         });
1082     }
1083
1084     var holdings_bChannel = null;
1085     // subscribe to BroadcastChannel for any child VolCopy tabs
1086     // refresh grid if needed to show new updates
1087     // if ($scope.record_tab === 'holdings'){
1088     $scope.$watch('record_tab', function(n){
1089     
1090         if (n === 'holdings'){
1091             if (typeof BroadcastChannel != 'undefined') {
1092                 // we're in holdings tab, connect 2 bChannel
1093                 holdings_bChannel = new BroadcastChannel('eg.holdings.update');
1094                 holdings_bChannel.onmessage = function(e){
1095                     if (e.data
1096                         && e.data.records
1097                         && e.data.records.length
1098                         && e.data.records.includes(Number($scope.record_id))
1099                     ){ // it's for us, refresh grid!
1100                         console.log("Got broadcast from channel eg.holdings.update for records " + e.data.records);
1101                         $scope.holdings_record_id_changed($scope.record_id);
1102                     }
1103                 }
1104             };
1105
1106         } else if (holdings_bChannel){ // we're leaving holding tab, close bChannel
1107             holdings_bChannel.close();
1108         }
1109     
1110     });
1111
1112     // refresh the list of holdings when the record_id is changed.
1113     $scope.holdings_record_id_changed = function(id) {
1114         if ($scope.record_id != id) $scope.record_id = id;
1115         console.log('record id changed to ' + id + ', loading new holdings');
1116         holdingsSvcInst.fetch({
1117             rid : $scope.record_id,
1118             org : $scope.holdings_ou,
1119             copy: $scope.holdings_show_vols ? $scope.holdings_show_copies : false,
1120             vol : $scope.holdings_show_vols,
1121             empty: $scope.holdings_show_empty,
1122             empty_org: $scope.holdings_show_empty_org
1123         }).then(function() {
1124             $scope.holdingsGridDataProvider.refresh();
1125         });
1126     }
1127
1128     // refresh the list of holdings when the filter lib is changed.
1129     $scope.holdings_ou = egCore.org.get(egCore.auth.user().ws_ou());
1130     $scope.holdings_ou_changed = function(org) {
1131         $scope.holdings_ou = org;
1132         holdingsSvcInst.fetch({
1133             rid : $scope.record_id,
1134             org : $scope.holdings_ou,
1135             copy: $scope.holdings_show_vols ? $scope.holdings_show_copies : false,
1136             vol : $scope.holdings_show_vols,
1137             empty: $scope.holdings_show_empty,
1138             empty_org: $scope.holdings_show_empty_org
1139         }).then(function() {
1140             $scope.holdingsGridDataProvider.refresh();
1141         });
1142     }
1143
1144     $scope.holdings_cb_changed = function(cb,newVal,norefresh) {
1145         $scope[cb] = newVal;
1146         var x = $scope.holdings_show_vols ? $scope.holdings_show_copies : false;
1147         $('#holdings_show_copies').prop('checked', x);
1148         egCore.hatch.setItem('cat.' + cb, newVal);
1149         if (!norefresh) holdingsSvcInst.fetch({
1150             rid : $scope.record_id,
1151             org : $scope.holdings_ou,
1152             copy: $scope.holdings_show_vols ? $scope.holdings_show_copies : false,
1153             vol : $scope.holdings_show_vols,
1154             empty: $scope.holdings_show_empty,
1155             empty_org: $scope.holdings_show_empty_org
1156         }).then(function() {
1157             $scope.holdingsGridDataProvider.refresh();
1158         });
1159     }
1160
1161     egCore.hatch.getItem('cat.holdings_show_vols').then(function(x){
1162         if (typeof x ==  'undefined') x = true;
1163         $scope.holdings_cb_changed('holdings_show_vols',x,true);
1164         $('#holdings_show_vols').prop('checked', x);
1165     }).then(function(){
1166         egCore.hatch.getItem('cat.holdings_show_copies').then(function(x){
1167             if (typeof x ==  'undefined') x = true;
1168             $scope.holdings_cb_changed('holdings_show_copies',x,true);
1169             x = $scope.holdings_show_vols ? x : false;
1170             $('#holdings_show_copies').prop('checked', x);
1171         }).then(function(){
1172             egCore.hatch.getItem('cat.holdings_show_empty').then(function(x){
1173                 if (typeof x ==  'undefined') x = true;
1174                 $scope.holdings_cb_changed('holdings_show_empty',x);
1175                 $('#holdings_show_empty').prop('checked', x);
1176             }).then(function(){
1177                 egCore.hatch.getItem('cat.holdings_show_empty_org').then(function(x){
1178                     if (typeof x ==  'undefined') x = true;
1179                     $scope.holdings_cb_changed('holdings_show_empty_org',x);
1180                     $('#holdings_show_empty_org').prop('checked', x);
1181                 })
1182             })
1183         })
1184     });
1185
1186     $scope.vols_not_shown = function () {
1187         return !$scope.holdings_show_vols;
1188     }
1189
1190     $scope.copies_not_shown = function () {
1191         return !$scope.holdings_show_copies;
1192     }
1193
1194     $scope.empty_org_not_shown = function () {
1195         return !$scope.holdings_show_empty_org;
1196     }
1197
1198     $scope.holdings_checkbox_handler = function (item) {
1199         $scope.holdings_cb_changed(item.checkbox,item.checked);
1200     }
1201
1202     function gatherSelectedHoldingsIds () {
1203         var cp_id_list = [];
1204         angular.forEach(
1205             $scope.holdingsGridControls.selectedItems(),
1206             function (item) { cp_id_list = cp_id_list.concat(item.id_list) }
1207         );
1208         return cp_id_list;
1209     }
1210
1211     function gatherSelectedRawCopies () {
1212         var cp_list = [];
1213         angular.forEach(
1214             $scope.holdingsGridControls.selectedItems(),
1215             function (item) { if (item.raw) cp_list = cp_list.concat(item.raw) }
1216         );
1217         return cp_list;
1218     }
1219
1220     function gatherSelectedEmptyVolumeIds () {
1221         var cn_id_list = [];
1222         angular.forEach(
1223             $scope.holdingsGridControls.selectedItems(),
1224             function (item) {
1225                 if (item.copy_count == 0 || (!item.id && item.call_number))
1226                     // we are in a compressed row with no copies, or we are in a single
1227                     // call number row with no copy (testing for presence of 'id')
1228                     // In either case, the call number is 'empty'
1229                     cn_id_list.push(item.call_number.id)
1230             }
1231         );
1232         return cn_id_list;
1233     }
1234
1235     function gatherSelectedVolumeIds () {
1236         var cn_id_list = [];
1237         angular.forEach(
1238             $scope.holdingsGridControls.selectedItems(),
1239             function (item) {
1240                 if (cn_id_list.indexOf(item.call_number.id) == -1)
1241                     cn_id_list.push(item.call_number.id)
1242             }
1243         );
1244         return cn_id_list;
1245     }
1246
1247     $scope.selectedHoldingsDelete = function (vols, copies) {
1248
1249         var cnHash = {};
1250         var perCnCopies = {};
1251
1252         var cn_count = 0;
1253         var cp_count = 0;
1254
1255         angular.forEach(
1256             $scope.holdingsGridControls.selectedItems(),
1257             function (item) {
1258                 if (vols && item.raw_call_number) {
1259                     cnHash[item.call_number.id] = egCore.idl.Clone(item.raw_call_number);
1260                     cnHash[item.call_number.id].isdeleted(1);
1261                     cn_count++;
1262                 } else if (copies) {
1263                     angular.forEach(egCore.idl.Clone(item.raw), function (cp) {
1264                         cp.isdeleted(1);
1265                         cp_count++;
1266                         var cn_id = cp.call_number().id();
1267                         if (!cnHash[cn_id]) {
1268                             cnHash[cn_id] = cp.call_number();
1269                             perCnCopies[cn_id] = [cp];
1270                         } else {
1271                             perCnCopies[cn_id].push(cp);
1272                         }
1273                         cp.call_number(cn_id); // prevent loops in JSON-ification
1274                     });
1275
1276                 }
1277             }
1278         );
1279
1280         angular.forEach(perCnCopies, function (v, k) {
1281             if (vols) {
1282                 cnHash[k].isdeleted(1);
1283                 cn_count++;
1284             }
1285             cnHash[k].copies(v);
1286         });
1287
1288         cnList = [];
1289         angular.forEach(cnHash, function (v, k) {
1290             cnList.push(v);
1291         });
1292
1293         if (cnList.length == 0) return;
1294
1295         var flags = {};
1296         if (vols && copies) flags.force_delete_copies = 1;
1297
1298         egConfirmDialog.open(
1299             egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES,
1300             egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES_MESSAGE,
1301             {copies : cp_count, volumes : cn_count}
1302         ).result.then(function() {
1303             egCore.net.request(
1304                 'open-ils.cat',
1305                 'open-ils.cat.asset.volume.fleshed.batch.update.override',
1306                 egCore.auth.token(), cnList, 1, flags
1307             ).then(function(update_count) {
1308                 holdingsSvcInst.fetchAgain().then(function() {
1309                     $scope.holdingsGridDataProvider.refresh();
1310                 });
1311             });
1312         });
1313     }
1314     $scope.selectedHoldingsCopyDelete = function () { $scope.selectedHoldingsDelete(false,true) }
1315     $scope.selectedHoldingsVolCopyDelete = function () { $scope.selectedHoldingsDelete(true,true) }
1316     $scope.selectedHoldingsEmptyVolCopyDelete = function () { $scope.selectedHoldingsDelete(true,false) }
1317
1318     spawnHoldingsAdd = function (add_vols,add_copies){
1319         var raw = [];
1320         if (!add_vols && add_copies) { // just a copy on existing volumes
1321             angular.forEach(gatherSelectedVolumeIds(), function (v) {
1322                 raw.push( {callnumber : v} );
1323             });
1324         } else if (add_vols) {
1325             if (typeof $scope.holdingsGridControls.selectedItems == "function" &&
1326                 $scope.holdingsGridControls.selectedItems().length > 0) {
1327                 angular.forEach($scope.holdingsGridControls.selectedItems(),
1328                     function (item) {
1329                         raw.push({
1330                             owner : item.owner_id,
1331                             label : ((item.call_number) ? item.call_number.label : null)
1332                         });
1333                     });
1334             } else {
1335                 raw.push({
1336                     owner : egCore.auth.user().ws_ou()
1337                 });
1338             }
1339         }
1340
1341         if (raw.length == 0) raw.push({});
1342
1343         egCore.net.request(
1344             'open-ils.actor',
1345             'open-ils.actor.anon_cache.set_value',
1346             null, 'edit-these-copies', {
1347                 record_id: $scope.record_id,
1348                 raw: raw,
1349                 hide_vols : false,
1350                 hide_copies : !add_copies
1351             }
1352         ).then(function(key) {
1353             if (key) {
1354                 var url = egCore.env.basePath + 'cat/volcopy/' + key;
1355                 $timeout(function() { $window.open(url, '_blank') });
1356             } else {
1357                 alert('Could not create anonymous cache key!');
1358             }
1359         });
1360     }
1361     $scope.selectedHoldingsVolCopyAdd = function () { spawnHoldingsAdd(true,true) }
1362     $scope.selectedHoldingsCopyAdd = function () { spawnHoldingsAdd(false,true) }
1363     $scope.selectedHoldingsVolAdd = function () { spawnHoldingsAdd(true,false) }
1364
1365     spawnHoldingsEdit = function (hide_vols,hide_copies){
1366         egCore.net.request(
1367             'open-ils.actor',
1368             'open-ils.actor.anon_cache.set_value',
1369             null, 'edit-these-copies', {
1370                 record_id: $scope.record_id,
1371                 copies: gatherSelectedHoldingsIds(),
1372                 raw: gatherSelectedEmptyVolumeIds().map(
1373                     function(v){ return { callnumber : v } }
1374                 ),
1375                 hide_vols : hide_vols,
1376                 hide_copies : hide_copies
1377             }
1378         ).then(function(key) {
1379             if (key) {
1380                 var url = egCore.env.basePath + 'cat/volcopy/' + key;
1381                 $timeout(function() { $window.open(url, '_blank') });
1382             } else {
1383                 alert('Could not create anonymous cache key!');
1384             }
1385         });
1386     }
1387     $scope.selectedHoldingsVolCopyEdit = function () { spawnHoldingsEdit(false,false) }
1388     $scope.selectedHoldingsVolEdit = function () { spawnHoldingsEdit(false,true) }
1389     $scope.selectedHoldingsCopyEdit = function () { spawnHoldingsEdit(true,false) }
1390
1391     $scope.selectedHoldingsItemStatus = function (){
1392         var url = egCore.env.basePath + 'cat/item/search/' + gatherSelectedHoldingsIds().join(',')
1393         $timeout(function() { $window.open(url, '_blank') });
1394     }
1395
1396     $scope.markFromSelectedAsHoldingsTarget = function() {
1397         egCore.hatch.setLocalItem(
1398             'eg.cat.transfer_target_lib',
1399             $scope.holdingsGridControls.selectedItems()[0].owner_id
1400         );
1401         egCore.hatch.setLocalItem(
1402             'eg.cat.transfer_target_record',
1403             $scope.record_id
1404         );
1405         if ($scope.holdingsGridControls.selectedItems()[0].call_number.id) { // cn.id missing when vols are collapsed, or we are on an empty lib
1406             egCore.hatch.setLocalItem(
1407                 'eg.cat.transfer_target_vol',
1408                 $scope.holdingsGridControls.selectedItems()[0].call_number.id
1409             );
1410         } else {
1411             // clear out the stale value if we're on a lib-only
1412             // or vol-collapsed row
1413             egCore.hatch.removeLocalItem('eg.cat.transfer_target_vol');
1414         }
1415         ngToast.create(egCore.strings.MARK_HOLDINGS_TARGET);
1416     }
1417
1418     $scope.selectedHoldingsItemStatusDetail = function (){
1419         angular.forEach(
1420             gatherSelectedHoldingsIds(),
1421             function (cid) {
1422                 var url = egCore.env.basePath +
1423                           'cat/item/' + cid;
1424                 $timeout(function() { $window.open(url, '_blank') });
1425             }
1426         );
1427     }
1428
1429     $scope.transferVolumes = function (){
1430         var target_record = egCore.hatch.getLocalItem('eg.cat.transfer_target_record');
1431         var target_lib = egCore.hatch.getLocalItem('eg.cat.transfer_target_lib');
1432         if (!target_lib
1433             && (!target_record || ($scope.record_id == target_record) )
1434         ) return;
1435
1436         var vols_to_move = {};
1437         if (target_lib) {
1438             // we're moving volumes to a different library
1439             var vol_ids = gatherSelectedVolumeIds();
1440             if (vol_ids.length) {
1441                 vols_to_move[target_lib] = vol_ids;
1442
1443                 // if we're *only* switching libs,
1444                 // grab the current record as the target
1445                 target_record = target_record || $scope.record_id;
1446             }
1447         } else {
1448             // we're moving volumes to the same library they exist in
1449             // currently, but on a different record
1450             var items = $scope.holdingsGridControls.selectedItems();
1451             angular.forEach(items, function(item) {
1452                 if (!(item.call_number.owning_lib in vols_to_move)) {
1453                     vols_to_move[item.call_number.owning_lib] = new Array;
1454                 }
1455                 vols_to_move[item.call_number.owning_lib].push(item.call_number.id);
1456             });
1457         }
1458
1459         var promises = [];        
1460         angular.forEach(vols_to_move, function(vols, owning_lib) {
1461             promises.push(egCore.net.request(
1462                 'open-ils.cat',
1463                 'open-ils.cat.asset.volume.batch.transfer.override',
1464                 egCore.auth.token(), {
1465                     docid   : target_record,
1466                     lib     : owning_lib,
1467                     volumes : vols
1468                 }
1469             ));
1470         });
1471         $q.all(promises).then(function(success) {
1472             if (success) {
1473                 ngToast.create(egCore.strings.VOLS_TRANSFERED);
1474                 holdingsSvcInst.fetchAgain().then(function() {
1475                     $scope.holdingsGridDataProvider.refresh();
1476                 });
1477             } else {
1478                 alert('Could not transfer volumes!');
1479             }
1480         });
1481     }
1482
1483     // this "transfers" selected copies to a new owning library,
1484     // auto-creating volumes as required
1485     $scope.transferItemsAutoFill = function() {
1486         var target_record = egCore.hatch.getLocalItem('eg.cat.transfer_target_record');
1487         var target_lib = egCore.hatch.getLocalItem('eg.cat.transfer_target_lib');
1488         if (!target_lib
1489             && (!target_record || ($scope.record_id == target_record) )
1490         ) return;
1491
1492         var items = $scope.holdingsGridControls.selectedItems();
1493         if (!items.length) {
1494             return;
1495         }
1496
1497         var vols_to_move   = {};
1498         var copies_to_move = {};
1499         angular.forEach(items, function(item) {
1500             var needs_move = false;
1501             if (target_lib
1502                 && (item.call_number.owning_lib != target_lib)) {
1503                     item.call_number.owning_lib = target_lib;
1504                     needs_move = true;
1505             }
1506             if (target_record
1507                 && (item.call_number.record != target_record)) {
1508                     item.call_number.record = target_record;
1509                     needs_move = true;
1510             }
1511             if (needs_move) {
1512                 if (item.call_number.id in vols_to_move) {
1513                     copies_to_move[item.call_number.id].push(item.id);
1514                 } else {
1515                     vols_to_move[item.call_number.id] = item.call_number;
1516                     copies_to_move[item.call_number.id] = new Array;
1517                     copies_to_move[item.call_number.id].push(item.id);
1518                 }
1519             }
1520         });
1521
1522         var promises = [];
1523         angular.forEach(vols_to_move, function(vol) {
1524             promises.push(egCore.net.request(
1525                 'open-ils.cat',
1526                 'open-ils.cat.call_number.find_or_create',
1527                 egCore.auth.token(),
1528                 vol.label,
1529                 vol.record, // may be new
1530                 vol.owning_lib, // may be new
1531                 vol.prefix.id,
1532                 vol.suffix.id,
1533                 vol.label_class
1534             ).then(function(resp) {
1535                 var evt = egCore.evt.parse(resp);
1536                 if (evt) return;
1537                 return egCore.net.request(
1538                     'open-ils.cat',
1539                     'open-ils.cat.transfer_copies_to_volume',
1540                     egCore.auth.token(),
1541                     resp.acn_id,
1542                     copies_to_move[vol.id]
1543                 );
1544             }));
1545         });
1546         $q.all(promises).then(function() {
1547             ngToast.create(egCore.strings.ITEMS_TRANSFERED);
1548             holdingsSvcInst.fetchAgain().then(function() {
1549                 $scope.holdingsGridDataProvider.refresh();
1550             });
1551         });
1552     }
1553
1554     $scope.gridCellHandlers = {};
1555     $scope.gridCellHandlers.copyAlertsEdit = function(id) {
1556         egCirc.manage_copy_alerts([id]).then(function() {
1557             // update grid items?
1558         });
1559     };
1560
1561     $scope.transferItems = function (){
1562         var xfer_target = egCore.hatch.getLocalItem('eg.cat.transfer_target_vol');
1563
1564         if (!xfer_target) {
1565             // we have no specific volume, let's try to fill in the
1566             // blanks instead
1567             return $scope.transferItemsAutoFill();
1568         }
1569
1570         var copy_ids = gatherSelectedHoldingsIds();
1571         if (copy_ids.length > 0) {
1572             egCore.net.request(
1573                 'open-ils.cat',
1574                 'open-ils.cat.transfer_copies_to_volume',
1575                 egCore.auth.token(),
1576                 xfer_target,
1577                 copy_ids
1578             ).then(
1579                 function(resp) { // oncomplete
1580                     var evt = egCore.evt.parse(resp);
1581                     if (evt) {
1582                         egConfirmDialog.open(
1583                             egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_TITLE,
1584                             egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_BODY,
1585                             {'evt_desc': evt.desc}
1586                         ).result.then(function() {
1587                             egCore.net.request(
1588                                 'open-ils.cat',
1589                                 'open-ils.cat.transfer_copies_to_volume.override',
1590                                 egCore.auth.token(),
1591                                 xfer_target,
1592                                 copy_ids,
1593                                 { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
1594                             ).then(function(resp) {
1595                                 holdingsSvcInst.fetchAgain().then(function() {
1596                                     $scope.holdingsGridDataProvider.refresh();
1597                                 });
1598                             });
1599                         });
1600                     } else {
1601                         ngToast.create(egCore.strings.ITEMS_TRANSFERED);
1602                         holdingsSvcInst.fetchAgain().then(function() {
1603                             $scope.holdingsGridDataProvider.refresh();
1604                         });
1605                     }
1606                 },
1607                 null, // onerror
1608                 null // onprogress
1609             )
1610         }
1611     }
1612
1613     $scope.selectedHoldingsItemStatusTgrEvt = function (){
1614         angular.forEach(
1615             gatherSelectedHoldingsIds(),
1616             function (cid) {
1617                 var url = egCore.env.basePath +
1618                           'cat/item/' + cid + '/triggered_events';
1619                 $timeout(function() { $window.open(url, '_blank') });
1620             }
1621         );
1622     }
1623
1624     $scope.selectedHoldingsItemStatusHolds = function (){
1625         angular.forEach(
1626             gatherSelectedHoldingsIds(),
1627             function (cid) {
1628                 var url = egCore.env.basePath +
1629                           'cat/item/' + cid + '/holds';
1630                 $timeout(function() { $window.open(url, '_blank') });
1631             }
1632         );
1633     }
1634
1635     $scope.selectedHoldingsPrintLabels = function() {
1636         egCore.net.request(
1637             'open-ils.actor',
1638             'open-ils.actor.anon_cache.set_value',
1639             null, 'print-labels-these-copies', {
1640                 copies : gatherSelectedHoldingsIds()
1641             }
1642         ).then(function(key) {
1643             if (key) {
1644                 var url = egCore.env.basePath + 'cat/printlabels/' + key;
1645                 $timeout(function() { $window.open(url, '_blank') });
1646             } else {
1647                 alert('Could not create anonymous cache key!');
1648             }
1649         });
1650     }
1651
1652     $scope.selectedHoldingsDamaged = function () {
1653         var copy_list = gatherSelectedRawCopies();
1654         if (copy_list.length == 0) return;
1655
1656         angular.forEach(copy_list, function(cp) {
1657             egCirc.mark_damaged({
1658                 id: cp.id(),
1659                 barcode: cp.barcode(),
1660                 circ_lib: cp.circ_lib().id()
1661             }).then(function() {
1662                 holdingsSvcInst.fetchAgain().then(function() {
1663                     $scope.holdingsGridDataProvider.refresh();
1664                 });
1665             });
1666         });
1667     }
1668
1669     $scope.selectedHoldingsMissing = function () {
1670         egCirc.mark_missing(gatherSelectedHoldingsIds()).then(function() {
1671             holdingsSvcInst.fetchAgain().then(function() {
1672                 $scope.holdingsGridDataProvider.refresh();
1673             });
1674         });
1675     }
1676
1677     $scope.selectedHoldingsCopyAlertsAdd = function() {
1678         egCirc.add_copy_alerts(gatherSelectedHoldingsIds()).then(function() {
1679             // no need to refresh grid
1680         });
1681     }
1682     $scope.selectedHoldingsCopyAlertsManage = function() {
1683         egCirc.manage_copy_alerts(gatherSelectedHoldingsIds()).then(function() {
1684             // no need to refresh grid
1685         });
1686     }
1687
1688     $scope.attach_to_peer_bib = function() {
1689         var copy_list = gatherSelectedHoldingsIds();
1690         if (copy_list.length == 0) return;
1691
1692         egCore.hatch.getItem('eg.cat.marked_conjoined_record').then(function(target_record) {
1693             if (!target_record) return;
1694
1695             return $uibModal.open({
1696                 templateUrl: './cat/catalog/t_conjoined_selector',
1697                 backdrop: 'static',
1698                 animation: true,
1699                 controller:
1700                        ['$scope','$uibModalInstance',
1701                 function($scope , $uibModalInstance) {
1702                     $scope.update = false;
1703
1704                     $scope.peer_type = null;
1705                     $scope.peer_type_list = [];
1706                     conjoinedSvc.get_peer_types().then(function(list){
1707                         $scope.peer_type_list = list;
1708                     });
1709     
1710                     $scope.ok = function(type) {
1711                         var promises = [];
1712     
1713                         angular.forEach(copy_list, function (cp) {
1714                             var n = new egCore.idl.bpbcm();
1715                             n.isnew(true);
1716                             n.peer_record(target_record);
1717                             n.target_copy(cp);
1718                             n.peer_type(type);
1719                             promises.push(egCore.pcrud.create(n));
1720                         });
1721     
1722                         return $q.all(promises).then(function(){$uibModalInstance.close()});
1723                     }
1724     
1725                     $scope.cancel = function($event) {
1726                         $uibModalInstance.dismiss();
1727                         $event.preventDefault();
1728                     }
1729                 }]
1730             });
1731         });
1732     }
1733
1734
1735     // ------------------------------------------------------------------
1736     // Holds 
1737     var provider = egGridDataProvider.instance({});
1738     var holds = []; // current list of holds
1739     var hold_count = 0;
1740
1741     $scope.hold_grid_data_provider = provider;
1742     $scope.grid_actions = egHoldGridActions;
1743     $scope.grid_actions.refresh = function () { holds = []; hold_count = 0; provider.refresh() };
1744     $scope.hold_grid_controls = {};
1745
1746     provider.get = function(offset, count) {
1747         if ($scope.record_tab != 'holds') return $q.when();
1748
1749         // see if we have the requested range cached
1750         if (holds[offset]) {
1751             return provider.arrayNotifier(holds, offset, count);
1752         }
1753
1754         hold_count = 0;
1755         holds = [];
1756         var restrictions = {
1757                 is_staff_request : 'true',
1758                 fulfillment_time : null,
1759                 cancel_time      : null,
1760                 record_id        : $scope.record_id,
1761                 pickup_lib       : egCore.org.descendants($scope.pickup_ou.id(), true)
1762         };
1763
1764         var order_by = [{ request_time : null }];
1765         if (provider.sort && provider.sort.length) {
1766             order_by = [];
1767             angular.forEach(provider.sort, function (c) {
1768                 if (!angular.isObject(c)) {
1769                     if (c.match(/^hold\./)) {
1770                         var i = c.replace('hold.','');
1771                         var ob = {};
1772                         ob[i] = null;
1773                         order_by.push(ob);
1774                     }
1775                 } else {
1776                     var i = Object.keys(c)[0];
1777                     var direction = c[i];
1778                     if (i.match(/^hold\./)) {
1779                         i = i.replace('hold.','');
1780                         var ob = {}
1781                         ob[i] = {dir:direction};
1782                         order_by.push(ob);
1783                     }
1784                 }
1785             });
1786         }
1787
1788         egProgressDialog.open({max : 1, value : 0});
1789         var first = true;
1790         return egHolds.fetch_wide_holds(
1791             restrictions,
1792             order_by
1793         ).then(function () {
1794                 return provider.arrayNotifier(holds, offset, count);
1795             },
1796             null,
1797             function(hold_data) {
1798                 if (first) {
1799                     hold_count = hold_data;
1800                     first = false;
1801                     egProgressDialog.update({max:hold_count});
1802                 } else {
1803                     egProgressDialog.increment();
1804                     var new_item = { id : hold_data.id, hold : hold_data };
1805                     new_item.status_string =
1806                         egCore.strings['HOLD_STATUS_' + hold_data.hold_status]
1807                         || hold_data.hold_status;
1808
1809                     holds.push(new_item);
1810                 }
1811             }
1812         ).finally(egProgressDialog.close);
1813
1814     }
1815
1816     $scope.detail_view = function(action, user_data, items) {
1817         if (h = items[0]) {
1818             $scope.detail_hold_id = h.hold.id;
1819         }
1820     }
1821
1822     $scope.list_view = function(items) {
1823          $scope.detail_hold_id = null;
1824     }
1825
1826     // refresh the list of record holds when the pickup lib is changed.
1827     $scope.pickup_ou = egCore.org.get(egCore.auth.user().ws_ou());
1828     $scope.pickup_ou_changed = function(org) {
1829         $scope.pickup_ou = org;
1830         holds = []
1831         hold_count = 0;
1832         provider.refresh();
1833     }
1834
1835     function map_prefix_to_subhash (h,pf) {
1836         var newhash = {};
1837         angular.forEach(Object.keys(h), function (k) {
1838             if (k.startsWith(pf)) {
1839                 var nk = k.substr(pf.length);
1840                 newhash[nk] = h[k];
1841             }
1842         });
1843         return newhash;
1844     }
1845
1846     $scope.print_holds = function() {
1847         var pholds = [];
1848         angular.forEach(holds, function(item) {
1849             pholds.push({
1850                 hold : item.hold,
1851                 status_string : item.status_string,
1852                 patron_first : item.hold.usr_first_given_name,
1853                 patron_last : item.hold.usr_family_name,
1854                 patron_alias : item.hold.usr_alias,
1855                 patron_barcode : item.hold.ucard_barcode,
1856                 copy : map_prefix_to_subhash(item.hold,'cp_'),
1857                 volume : map_prefix_to_subhash(item.hold,'cn_'),
1858                 title : item.hold.title,
1859                 author : item.hold.author
1860             });
1861         });
1862
1863         egCore.print.print({
1864             context : 'receipt', 
1865             template : 'holds_for_bib', 
1866             scope : {holds : pholds}
1867         });
1868     }
1869
1870     $scope.current_hold_transfer_dest = egCore.hatch.getLocalItem ('eg.circ.hold.title_transfer_target');
1871
1872     $scope.mark_hold_transfer_dest = function() {
1873         $scope.current_hold_transfer_dest = $scope.record_id;
1874         egCore.hatch.setLocalItem(
1875             'eg.circ.hold.title_transfer_target', $scope.record_id);
1876         ngToast.create(egCore.strings.HOLD_TRANSFER_DEST_MARKED);
1877     }
1878
1879     // UI presents this option as "all holds"
1880     $scope.transfer_holds_to_marked = function() {
1881         var hold_ids = $scope.hold_grid_controls.allItems().map(
1882             function(hold_data) {return hold_data.hold.id});
1883         egHolds.transfer_to_marked_title(hold_ids);
1884     }
1885
1886     // ------------------------------------------------------------------
1887     // Initialize the selected tab
1888
1889     // we explicitly initialize catalog_url because otherwise Firefox
1890     // ends up setting it to $BASE_URL/{{url}}, which then messes
1891     // things up. See LP#1708951
1892     $scope.catalog_url = '';
1893
1894     function init_cat_url() {
1895         // Set the initial catalog URL.  This only happens once.
1896         // The URL is otherwise generated through user navigation.
1897         if ($scope.catalog_url) return;
1898
1899         var url = $location.absUrl().replace(/\/staff.*/, '/opac/advanced');
1900
1901         // A record ID in the path indicates a request for the record-
1902         // specific page.
1903         if ($routeParams.record_id) {
1904             url = url.replace(/advanced/, '/record/' + $scope.record_id);
1905         }
1906
1907         // Jumping directly to the results page by passing a search
1908         // query via the URL.  Copy all URL params to the iframe url.
1909         if ($location.path().match(/catalog\/results/)) {
1910             url = url.replace(/advanced/, '/results?');
1911             var first = true;
1912             angular.forEach($location.search(), function(val, key) {
1913                 if (!first) url += '&';
1914                 first = false;
1915                 url += encodeURIComponent(key) 
1916                     + '=' + encodeURIComponent(val);
1917             });
1918         }
1919
1920         // if we're displaying the advanced search form, select
1921         // whatever default pane the user has chosen via workstation
1922         // preference
1923         if (url.match(/\/opac\/advanced$/)) {
1924             egCore.hatch.getItem('eg.search.adv_pane').then(function(adv_pane_val){
1925                 if (adv_pane_val) {
1926                     url += '?pane=' + encodeURIComponent(adv_pane_val);
1927                 }
1928
1929                 $scope.catalog_url = url;
1930             });
1931         } else {
1932             $scope.catalog_url = url;
1933         }
1934
1935     }
1936
1937     function init_parts_url() {
1938         $scope.parts_url = $location
1939             .absUrl()
1940             .replace(
1941                 /\/staff.*/,
1942                 '/conify/global/biblio/monograph_part?r='+$scope.record_id
1943             );
1944     }
1945
1946     $scope.set_record_tab = function(tab) {
1947         $scope.record_tab = tab;
1948
1949         switch(tab) {
1950
1951             case 'monoparts':
1952                 init_parts_url();
1953                 break;
1954
1955             case 'catalog':
1956                 init_cat_url();
1957                 break;
1958
1959             case 'holds':
1960                 $scope.detail_hold_record_id = $scope.record_id; 
1961                 // refresh the holds grid
1962                 provider.refresh();
1963
1964                 break;
1965         }
1966     }
1967
1968     $scope.set_default_record_tab = function() {
1969         egCore.hatch.setLocalItem(
1970             'eg.cat.default_record_tab', $scope.record_tab);
1971         $timeout(function(){$scope.default_tab = $scope.record_tab});
1972     }
1973
1974     var tab;
1975     if ($scope.record_id) {
1976         $scope.default_tab = egCore.hatch.getLocalItem( 'eg.cat.default_record_tab' );
1977         tab = $routeParams.record_tab || $scope.default_tab || 'catalog';
1978
1979     } else {
1980         tab = $routeParams.record_tab || 'catalog';
1981     }
1982     $scope.set_record_tab(tab);
1983
1984 }])
1985
1986 .controller('AuthorityCtrl',
1987        ['$scope','$routeParams','$location','$window','$q','egCore',
1988 function($scope , $routeParams , $location , $window , $q , egCore) {
1989
1990     // set record ID on page load if available...
1991     $scope.authority_id = $routeParams.authority_id;
1992
1993     if ($routeParams.authority_id) $scope.from_route = true;
1994     else $scope.from_route = false;
1995
1996     $scope.stop_unload = false;
1997 }])
1998
1999 .controller('URLVerifyCtrl',
2000        ['$scope','$location',
2001 function($scope , $location) {
2002     $scope.verifyurls_url = $location.absUrl().replace(/\/staff.*/, '/url_verify/sessions');
2003 }])
2004
2005 .controller('VandelayCtrl',
2006        ['$scope','$location', 'egCore', '$uibModal',
2007 function($scope , $location, egCore, $uibModal) {
2008     $scope.vandelay_url = $location.absUrl().replace(/\/staff\/cat\/catalog\/vandelay/, '/vandelay/vandelay');
2009     $scope.funcs = {};
2010     $scope.funcs.edit_marc_modal = function(bre, callback){
2011         var marcArgs = { 'marc_xml': bre.marc() };
2012         var vqbibrecId = bre.id();
2013         $uibModal.open({
2014             templateUrl: './cat/catalog/t_edit_marc_modal',
2015             backdrop: 'static',
2016             size: 'lg',
2017             controller: ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
2018                 $scope.focusMe = true;
2019                 $scope.recordId = vqbibrecId;
2020                 $scope.args = marcArgs;
2021                 $scope.dirty_flag = false;
2022                 $scope.ok = function(marg){
2023                     $uibModalInstance.close(marg);
2024                 };
2025                 $scope.cancel = function(){ $uibModalInstance.dismiss() }
2026             }]
2027         }).result.then(function(res){
2028             var new_xml = res.marc_xml;
2029             egCore.pcrud.retrieve('vqbr', vqbibrecId).then(function(vqbib){
2030                 vqbib.marc(new_xml);
2031                 egCore.pcrud.update(vqbib).then( function(){ callback(vqbibrecId); });
2032             });
2033         });
2034     };
2035 }])
2036
2037 .controller('ManageAuthoritiesCtrl',
2038        ['$scope','$location',
2039 function($scope , $location) {
2040     $scope.manageauthorities_url = $location.absUrl().replace(/\/staff.*/, '/cat/authority/list');
2041 }])
2042
2043 .controller('BatchEditCtrl',
2044        ['$scope','$location','$routeParams',
2045 function($scope , $location , $routeParams) {
2046     $scope.batchedit_url = $location.absUrl().replace(/\/eg.*/, '/opac/extras/merge_template');
2047     if ($routeParams.container_type) {
2048         switch ($routeParams.container_type) {
2049             case 'bucket':
2050                 $scope.batchedit_url += '?recordSource=b&containerid=' + $routeParams.container_id;
2051                 break;
2052             case 'record':
2053                 $scope.batchedit_url += '?recordSource=r&recid=' + $routeParams.container_id;
2054                 break;
2055         };
2056     }
2057 }])
2058
2059  
2060 .filter('boolText', function(){
2061     return function (v) {
2062         return v == 't';
2063     }
2064 })
2065
2066 .factory('conjoinedSvc', 
2067        ['egCore','$q',
2068 function(egCore , $q) {
2069
2070     var service = {
2071         items : [], // record search results
2072         index : 0, // search grid index
2073         rid : null
2074     };
2075
2076     service.flesh = {   
2077         flesh : 4, 
2078         flesh_fields : {
2079             bpbcm : ['target_copy','peer_type'],
2080             acp : ['call_number'],
2081             acn : ['record'],
2082             bre : ['simple_record']
2083         },
2084         // avoid fetching the MARC blob by specifying which
2085         // fields on the bre to select.  More may be needed.
2086         // note that fleshed fields are explicitly selected.
2087         select : { bre : ['id'] },
2088         order_by : { bpbcm : ['id'] },
2089     }
2090
2091     // resolved with the last received copy
2092     service.fetch = function(rid) {
2093         if (!rid && !service.rid) return $q.when();
2094
2095         if (rid) service.rid = rid;
2096         service.items = [];
2097         service.index = 0;
2098
2099         return egCore.pcrud.search(
2100             'bpbcm',
2101             {peer_record : service.rid},
2102             service.flesh,
2103             {atomic : true}
2104         ).then( function(list) { // finished
2105             service.items = list;
2106             return service.items;
2107         });
2108     }
2109
2110     // returns a promise resolved with the list of peer bib types
2111     service.get_peer_types = function() {
2112         if (egCore.env.bpt)
2113             return $q.when(egCore.env.bpt.list);
2114
2115         return egCore.pcrud.retrieveAll('bpt', null, {atomic : true})
2116         .then(function(list) {
2117             egCore.env.absorbList(list, 'bpt');
2118             return list;
2119         });
2120     };
2121
2122     return service;
2123 }])
2124
2125