LP#1712854: Provide context-relevant default sort orders
[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 match = url.match(/\/+opac\/+record\/+(\d+)/);
585         if (match) {
586             $scope.record_id = match[1];
587             egCore.hatch.setLocalItem("eg.cat.last_record_retrieved", $scope.record_id);
588             $scope.holdings_record_id_changed($scope.record_id);
589             conjoinedSvc.fetch($scope.record_id).then(function(){
590                 $scope.conjoinedGridDataProvider.refresh();
591             });
592             init_parts_url();
593             $location.update_path('/cat/catalog/record/' + $scope.record_id);
594             // update_path() bypasses the controller for path 
595             // /cat/catalog/record/:record_id. Manually set title here too.
596             egCore.strings.setPageTitle(
597                 egCore.strings.PAGE_TITLE_BIB_DETAIL,
598                 egCore.strings.PAGE_TITLE_CATALOG_CONTEXT,
599                 {record_id : $scope.record_id}
600             );
601         } else {
602             delete $scope.record_id;
603             $scope.from_route = false;
604         }
605
606         // child scope is executing this function, so our digest doesn't fire ... thus,
607         $scope.$apply();
608
609         if (!$scope.in_opac_call) {
610             if ($scope.record_id && !$scope.record_tab) {
611                 $scope.default_tab = egCore.hatch.getLocalItem( 'eg.cat.default_record_tab' );
612                 tab = $routeParams.record_tab || $scope.default_tab || 'catalog';
613             } else {
614                 tab = $routeParams.record_tab || 'catalog';
615             }
616             $scope.set_record_tab(tab);
617         } else {
618             $scope.in_opac_call = false;
619         }
620
621         if ($scope.opac_iframe && $location.path().match(/cat\/catalog/)) {
622             var doc = $scope.opac_iframe.dom.contentWindow.document;
623             $(doc).find('#hold_usr_search').show();
624             $(doc).find('#hold_usr_search').on('click', function() {
625                 patron_search_dialog().result.then(function(barc) {
626                     $(doc).find('#hold_usr_input').val(barc);
627                     $(doc).find('#hold_usr_input').change();
628                 });
629             });
630             $(doc).find('#select_basket_action').on('change', function() {
631                 if (this.options[this.selectedIndex].value && this.options[this.selectedIndex].value == "add_cart_to_bucket") {
632                     $scope.add_cart_to_record_bucket();
633                 }
634             });
635         }
636
637     }
638
639     // xulG catalog handlers
640     $scope.handlers = { }
641
642     // ------------------------------------------------------------------
643     // Conjoined items
644
645     $scope.conjoinedGridControls = {};
646     $scope.conjoinedGridDataProvider = egGridDataProvider.instance({
647         get : function(offset, count) {
648             return this.arrayNotifier(conjoinedSvc.items, offset, count);
649         }
650     });
651
652     $scope.changeConjoinedType = function () {
653         var peers = egCore.idl.Clone($scope.conjoinedGridControls.selectedItems());
654         angular.forEach(peers, function (p) {
655             p.target_copy(p.target_copy().id());
656             p.peer_type(p.peer_type().id());
657         });
658
659         var conjoinedGridDataProviderRef = $scope.conjoinedGridDataProvider;
660
661         return $uibModal.open({
662             templateUrl: './cat/catalog/t_conjoined_selector',
663             backdrop: 'static',
664             animation: true,
665             controller:
666                    ['$scope','$uibModalInstance',
667             function($scope , $uibModalInstance) {
668                 $scope.update = true;
669
670                 $scope.peer_type = null;
671                 $scope.peer_type_list = [];
672                 conjoinedSvc.get_peer_types().then(function(list){
673                     $scope.peer_type_list = list;
674                 });
675     
676                 $scope.ok = function(type) {
677                     var promises = [];
678     
679                     angular.forEach(peers, function (p) {
680                         p.ischanged(1);
681                         p.peer_type(type);
682                         promises.push(egCore.pcrud.update(p));
683                     });
684     
685                     return $q.all(promises)
686                         .then(function(){$uibModalInstance.close()})
687                         .then(function(){return conjoinedSvc.fetch()})
688                         .then(function(){conjoinedGridDataProviderRef.refresh()});
689                 }
690     
691                 $scope.cancel = function($event) {
692                     $uibModalInstance.dismiss();
693                     $event.preventDefault();
694                 }
695             }]
696         });
697         
698     }
699
700     $scope.refreshConjoined = function () {
701         conjoinedSvc.fetch($scope.record_id)
702         .then(function(){$scope.conjoinedGridDataProvider.refresh();});
703     }
704
705     $scope.deleteSelectedConjoined = function () {
706         var peers = $scope.conjoinedGridControls.selectedItems();
707
708         if (peers.length > 0) {
709             egConfirmDialog.open(
710                 egCore.strings.CONFIRM_DELETE_PEERS,
711                 egCore.strings.CONFIRM_DELETE_PEERS_MESSAGE,
712                 {peers : peers.length}
713             ).result.then(function() {
714                 angular.forEach(peers, function (p) {
715                     p.isdeleted(1);
716                 });
717
718                 egCore.pcrud.remove(peers).then(function() {
719                     return conjoinedSvc.fetch();
720                 }).then(function() {
721                     $scope.conjoinedGridDataProvider.refresh();
722                 });
723             });
724         }
725     }
726     if ($scope.record_id)
727         conjoinedSvc.fetch($scope.record_id);
728
729     // ------------------------------------------------------------------
730     // Holdings
731
732     $scope.holdingsGridControls = {
733         activateItem : function (item) {
734             $scope.selectedHoldingsVolCopyEdit();
735         }
736     };
737     $scope.holdingsGridDataProvider = egGridDataProvider.instance({
738         get : function(offset, count) {
739             return this.arrayNotifier(holdingsSvcInst.copies, offset, count);
740         }
741     });
742
743     $scope.add_copies_to_bucket = function() {
744         var copy_list = gatherSelectedHoldingsIds();
745         if (copy_list.length == 0) return;
746
747         return $uibModal.open({
748             templateUrl: './cat/catalog/t_add_to_bucket',
749             backdrop: 'static',
750             animation: true,
751             size: 'md',
752             controller:
753                    ['$scope','$uibModalInstance',
754             function($scope , $uibModalInstance) {
755
756                 $scope.bucket_id = 0;
757                 $scope.newBucketName = '';
758                 $scope.allBuckets = [];
759
760                 egCore.net.request(
761                     'open-ils.actor',
762                     'open-ils.actor.container.retrieve_by_class.authoritative',
763                     egCore.auth.token(), egCore.auth.user().id(),
764                     'copy', 'staff_client'
765                 ).then(function(buckets) { $scope.allBuckets = buckets; });
766
767                 $scope.add_to_bucket = function() {
768                     var promises = [];
769                     angular.forEach(copy_list, function (cp) {
770                         var item = new egCore.idl.ccbi()
771                         item.bucket($scope.bucket_id);
772                         item.target_copy(cp);
773                         promises.push(
774                             egCore.net.request(
775                                 'open-ils.actor',
776                                 'open-ils.actor.container.item.create',
777                                 egCore.auth.token(), 'copy', item
778                             )
779                         );
780
781                         return $q.all(promises).then(function() {
782                             $uibModalInstance.close();
783                         });
784                     });
785                 }
786
787                 $scope.add_to_new_bucket = function() {
788                     var bucket = new egCore.idl.ccb();
789                     bucket.owner(egCore.auth.user().id());
790                     bucket.name($scope.newBucketName);
791                     bucket.description('');
792                     bucket.btype('staff_client');
793
794                     return egCore.net.request(
795                         'open-ils.actor',
796                         'open-ils.actor.container.create',
797                         egCore.auth.token(), 'copy', bucket
798                     ).then(function(bucket) {
799                         $scope.bucket_id = bucket;
800                         $scope.add_to_bucket();
801                     });
802                 }
803
804                 $scope.cancel = function() {
805                     $uibModalInstance.dismiss();
806                 }
807             }]
808         });
809     }
810
811     // TODO: refactor common code between cat/catalog/app.js and cat/item/app.js 
812
813     $scope.need_one_selected = function() {
814         var items = $scope.holdingsGridControls.selectedItems();
815         if (items.length == 1) return false;
816         return true;
817     };
818
819     $scope.make_copies_bookable = function() {
820
821         var copies_by_record = {};
822         var record_list = [];
823         angular.forEach(
824             $scope.holdingsGridControls.selectedItems(),
825             function (item) {
826                 var record_id = item['call_number.record.id'];
827                 if (typeof copies_by_record[ record_id ] == 'undefined') {
828                     copies_by_record[ record_id ] = [];
829                     record_list.push( record_id );
830                 }
831                 copies_by_record[ record_id ].push(item.id);
832             }
833         );
834
835         var promises = [];
836         var combined_results = [];
837         angular.forEach(record_list, function(record_id) {
838             promises.push(
839                 egCore.net.request(
840                     'open-ils.booking',
841                     'open-ils.booking.resources.create_from_copies',
842                     egCore.auth.token(),
843                     copies_by_record[record_id]
844                 ).then(function(results) {
845                     if (results && results['brsrc']) {
846                         combined_results = combined_results.concat(results['brsrc']);
847                     }
848                 })
849             );
850         });
851
852         $q.all(promises).then(function() {
853             if (combined_results.length > 0) {
854                 $uibModal.open({
855                     template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
856                     backdrop: 'static',
857                     animation: true,
858                     size: 'md',
859                     controller:
860                            ['$scope','$location','egCore','$uibModalInstance',
861                     function($scope , $location , egCore , $uibModalInstance) {
862
863                         $scope.funcs = {
864                             ses : egCore.auth.token(),
865                             resultant_brsrc : combined_results.map(function(o) { return o[0]; })
866                         }
867
868                         var booking_path = '/eg/conify/global/booking/resource';
869
870                         $scope.booking_admin_url =
871                             $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
872                     }]
873                 });
874             }
875         });
876     }
877
878     $scope.book_copies_now = function() {
879         var copies_by_record = {};
880         var record_list = [];
881         angular.forEach(
882             $scope.holdingsGridControls.selectedItems(),
883             function (item) {
884                 var record_id = item['call_number.record.id'];
885                 if (typeof copies_by_record[ record_id ] == 'undefined') {
886                     copies_by_record[ record_id ] = [];
887                     record_list.push( record_id );
888                 }
889                 copies_by_record[ record_id ].push(item.id);
890             }
891         );
892
893         var promises = [];
894         var combined_brt = [];
895         var combined_brsrc = [];
896         angular.forEach(record_list, function(record_id) {
897             promises.push(
898                 egCore.net.request(
899                     'open-ils.booking',
900                     'open-ils.booking.resources.create_from_copies',
901                     egCore.auth.token(),
902                     copies_by_record[record_id]
903                 ).then(function(results) {
904                     if (results && results['brt']) {
905                         combined_brt = combined_brt.concat(results['brt']);
906                     }
907                     if (results && results['brsrc']) {
908                         combined_brsrc = combined_brsrc.concat(results['brsrc']);
909                     }
910                 })
911             );
912         });
913
914         $q.all(promises).then(function() {
915             if (combined_brt.length > 0 || combined_brsrc.length > 0) {
916                 $uibModal.open({
917                     template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
918                     backdrop: 'static',
919                     animation: true,
920                     size: 'md',
921                     controller:
922                            ['$scope','$location','egCore','$uibModalInstance',
923                     function($scope , $location , egCore , $uibModalInstance) {
924
925                         $scope.funcs = {
926                             ses : egCore.auth.token(),
927                             bresv_interface_opts : {
928                                 booking_results : {
929                                      brt : combined_brt
930                                     ,brsrc : combined_brsrc
931                                 }
932                             }
933                         }
934
935                         var booking_path = '/eg/booking/reservation';
936
937                         $scope.booking_admin_url =
938                             $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
939
940                     }]
941                 });
942             }
943         });
944     }
945
946
947     $scope.requestItems = function() {
948         var copy_list = gatherSelectedHoldingsIds();
949         if (copy_list.length == 0) return;
950
951         return $uibModal.open({
952             templateUrl: './cat/catalog/t_request_items',
953             animation: true,
954             controller:
955                    ['$scope','$uibModalInstance',
956             function($scope , $uibModalInstance) {
957                 $scope.user = null;
958                 $scope.first_user_fetch = true;
959
960                 $scope.hold_data = {
961                     hold_type : 'C',
962                     copy_list : copy_list,
963                     pickup_lib: egCore.org.get(egCore.auth.user().ws_ou()),
964                     user      : egCore.auth.user().id()
965                 };
966
967                 egUser.get( $scope.hold_data.user ).then(function(u) {
968                     $scope.user = u;
969                     $scope.barcode = u.card().barcode();
970                     $scope.user_name = egUser.format_name(u);
971                     $scope.hold_data.user = u.id();
972                 });
973
974                 $scope.user_name = '';
975                 $scope.barcode = '';
976                 $scope.$watch('barcode', function (n) {
977                     if (!$scope.first_user_fetch) {
978                         egUser.getByBarcode(n).then(function(u) {
979                             $scope.user = u;
980                             $scope.user_name = egUser.format_name(u);
981                             $scope.hold_data.user = u.id();
982                         }, function() {
983                             $scope.user = null;
984                             $scope.user_name = '';
985                             delete $scope.hold_data.user;
986                         });
987                     }
988                     $scope.first_user_fetch = false;
989                 });
990
991                 $scope.ok = function(h) {
992                     var args = {
993                         patronid  : h.user,
994                         hold_type : h.hold_type,
995                         pickup_lib: h.pickup_lib.id(),
996                         depth     : 0
997                     };
998
999                     egCore.net.request(
1000                         'open-ils.circ',
1001                         'open-ils.circ.holds.test_and_create.batch.override',
1002                         egCore.auth.token(), args, h.copy_list
1003                     );
1004
1005                     $uibModalInstance.close();
1006                 }
1007
1008                 $scope.cancel = function($event) {
1009                     $uibModalInstance.dismiss();
1010                     $event.preventDefault();
1011                 }
1012             }]
1013         });
1014     }
1015
1016     $scope.view_place_orders = function() {
1017         if (!$scope.record_id) return;
1018         var url = egCore.env.basePath + 'acq/legacy/lineitem/related/' + $scope.record_id + '?target=bib';
1019         $timeout(function() { $window.open(url, '_blank') });
1020     }
1021
1022     $scope.replaceBarcodes = function() {
1023         var copy_list = gatherSelectedRawCopies();
1024         if (copy_list.length == 0) return;
1025
1026         var holdingsGridDataProviderRef = $scope.holdingsGridDataProvider;
1027
1028         angular.forEach(copy_list, function (cp) {
1029             $uibModal.open({
1030                 templateUrl: './cat/share/t_replace_barcode',
1031                 backdrop: 'static',
1032                 animation: true,
1033                 controller:
1034                            ['$scope','$uibModalInstance',
1035                     function($scope , $uibModalInstance) {
1036                         $scope.isModal = true;
1037                         $scope.focusBarcode = false;
1038                         $scope.focusBarcode2 = true;
1039                         $scope.barcode1 = cp.barcode();
1040
1041                         $scope.updateBarcode = function() {
1042                             $scope.copyNotFound = false;
1043                             $scope.updateOK = false;
1044                 
1045                             egCore.pcrud.search('acp',
1046                                 {deleted : 'f', barcode : $scope.barcode1})
1047                             .then(function(copy) {
1048                 
1049                                 if (!copy) {
1050                                     $scope.focusBarcode = true;
1051                                     $scope.copyNotFound = true;
1052                                     return;
1053                                 }
1054                 
1055                                 $scope.copyId = copy.id();
1056                                 copy.barcode($scope.barcode2);
1057                 
1058                                 egCore.pcrud.update(copy).then(function(stat) {
1059                                     $scope.updateOK = stat;
1060                                     $scope.focusBarcode = true;
1061                                     holdingsSvc.fetchAgain().then(function (){
1062                                         holdingsGridDataProviderRef.refresh();
1063                                     });
1064                                 });
1065
1066                             });
1067                             $uibModalInstance.close();
1068                         }
1069
1070                         $scope.cancel = function($event) {
1071                             $uibModalInstance.dismiss();
1072                             $event.preventDefault();
1073                         }
1074                     }
1075                 ]
1076             });
1077         });
1078     }
1079
1080     // refresh the list of holdings when the record_id is changed.
1081     $scope.holdings_record_id_changed = function(id) {
1082         if ($scope.record_id != id) $scope.record_id = id;
1083         console.log('record id changed to ' + id + ', loading new holdings');
1084         holdingsSvcInst.fetch({
1085             rid : $scope.record_id,
1086             org : $scope.holdings_ou,
1087             copy: $scope.holdings_show_vols ? $scope.holdings_show_copies : false,
1088             vol : $scope.holdings_show_vols,
1089             empty: $scope.holdings_show_empty,
1090             empty_org: $scope.holdings_show_empty_org
1091         }).then(function() {
1092             $scope.holdingsGridDataProvider.refresh();
1093         });
1094     }
1095
1096     // refresh the list of holdings when the filter lib is changed.
1097     $scope.holdings_ou = egCore.org.get(egCore.auth.user().ws_ou());
1098     $scope.holdings_ou_changed = function(org) {
1099         $scope.holdings_ou = org;
1100         holdingsSvcInst.fetch({
1101             rid : $scope.record_id,
1102             org : $scope.holdings_ou,
1103             copy: $scope.holdings_show_vols ? $scope.holdings_show_copies : false,
1104             vol : $scope.holdings_show_vols,
1105             empty: $scope.holdings_show_empty,
1106             empty_org: $scope.holdings_show_empty_org
1107         }).then(function() {
1108             $scope.holdingsGridDataProvider.refresh();
1109         });
1110     }
1111
1112     $scope.holdings_cb_changed = function(cb,newVal,norefresh) {
1113         $scope[cb] = newVal;
1114         var x = $scope.holdings_show_vols ? $scope.holdings_show_copies : false;
1115         $('#holdings_show_copies').prop('checked', x);
1116         egCore.hatch.setItem('cat.' + cb, newVal);
1117         if (!norefresh) holdingsSvcInst.fetch({
1118             rid : $scope.record_id,
1119             org : $scope.holdings_ou,
1120             copy: $scope.holdings_show_vols ? $scope.holdings_show_copies : false,
1121             vol : $scope.holdings_show_vols,
1122             empty: $scope.holdings_show_empty,
1123             empty_org: $scope.holdings_show_empty_org
1124         }).then(function() {
1125             $scope.holdingsGridDataProvider.refresh();
1126         });
1127     }
1128
1129     egCore.hatch.getItem('cat.holdings_show_vols').then(function(x){
1130         if (typeof x ==  'undefined') x = true;
1131         $scope.holdings_cb_changed('holdings_show_vols',x,true);
1132         $('#holdings_show_vols').prop('checked', x);
1133     }).then(function(){
1134         egCore.hatch.getItem('cat.holdings_show_copies').then(function(x){
1135             if (typeof x ==  'undefined') x = true;
1136             $scope.holdings_cb_changed('holdings_show_copies',x,true);
1137             x = $scope.holdings_show_vols ? x : false;
1138             $('#holdings_show_copies').prop('checked', x);
1139         }).then(function(){
1140             egCore.hatch.getItem('cat.holdings_show_empty').then(function(x){
1141                 if (typeof x ==  'undefined') x = true;
1142                 $scope.holdings_cb_changed('holdings_show_empty',x);
1143                 $('#holdings_show_empty').prop('checked', x);
1144             }).then(function(){
1145                 egCore.hatch.getItem('cat.holdings_show_empty_org').then(function(x){
1146                     if (typeof x ==  'undefined') x = true;
1147                     $scope.holdings_cb_changed('holdings_show_empty_org',x);
1148                     $('#holdings_show_empty_org').prop('checked', x);
1149                 })
1150             })
1151         })
1152     });
1153
1154     $scope.vols_not_shown = function () {
1155         return !$scope.holdings_show_vols;
1156     }
1157
1158     $scope.copies_not_shown = function () {
1159         return !$scope.holdings_show_copies;
1160     }
1161
1162     $scope.empty_org_not_shown = function () {
1163         return !$scope.holdings_show_empty_org;
1164     }
1165
1166     $scope.holdings_checkbox_handler = function (item) {
1167         $scope.holdings_cb_changed(item.checkbox,item.checked);
1168     }
1169
1170     function gatherSelectedHoldingsIds () {
1171         var cp_id_list = [];
1172         angular.forEach(
1173             $scope.holdingsGridControls.selectedItems(),
1174             function (item) { cp_id_list = cp_id_list.concat(item.id_list) }
1175         );
1176         return cp_id_list;
1177     }
1178
1179     function gatherSelectedRawCopies () {
1180         var cp_list = [];
1181         angular.forEach(
1182             $scope.holdingsGridControls.selectedItems(),
1183             function (item) { if (item.raw) cp_list = cp_list.concat(item.raw) }
1184         );
1185         return cp_list;
1186     }
1187
1188     function gatherSelectedEmptyVolumeIds () {
1189         var cn_id_list = [];
1190         angular.forEach(
1191             $scope.holdingsGridControls.selectedItems(),
1192             function (item) {
1193                 if (item.copy_count == 0 || (!item.id && item.call_number))
1194                     // we are in a compressed row with no copies, or we are in a single
1195                     // call number row with no copy (testing for presence of 'id')
1196                     // In either case, the call number is 'empty'
1197                     cn_id_list.push(item.call_number.id)
1198             }
1199         );
1200         return cn_id_list;
1201     }
1202
1203     function gatherSelectedVolumeIds () {
1204         var cn_id_list = [];
1205         angular.forEach(
1206             $scope.holdingsGridControls.selectedItems(),
1207             function (item) {
1208                 if (cn_id_list.indexOf(item.call_number.id) == -1)
1209                     cn_id_list.push(item.call_number.id)
1210             }
1211         );
1212         return cn_id_list;
1213     }
1214
1215     $scope.selectedHoldingsDelete = function (vols, copies) {
1216
1217         var cnHash = {};
1218         var perCnCopies = {};
1219
1220         var cn_count = 0;
1221         var cp_count = 0;
1222
1223         angular.forEach(
1224             $scope.holdingsGridControls.selectedItems(),
1225             function (item) {
1226                 if (vols && item.raw_call_number) {
1227                     cnHash[item.call_number.id] = egCore.idl.Clone(item.raw_call_number);
1228                     cnHash[item.call_number.id].isdeleted(1);
1229                     cn_count++;
1230                 } else if (copies) {
1231                     angular.forEach(egCore.idl.Clone(item.raw), function (cp) {
1232                         cp.isdeleted(1);
1233                         cp_count++;
1234                         var cn_id = cp.call_number().id();
1235                         if (!cnHash[cn_id]) {
1236                             cnHash[cn_id] = cp.call_number();
1237                             perCnCopies[cn_id] = [cp];
1238                         } else {
1239                             perCnCopies[cn_id].push(cp);
1240                         }
1241                         cp.call_number(cn_id); // prevent loops in JSON-ification
1242                     });
1243
1244                 }
1245             }
1246         );
1247
1248         angular.forEach(perCnCopies, function (v, k) {
1249             if (vols) {
1250                 cnHash[k].isdeleted(1);
1251                 cn_count++;
1252             }
1253             cnHash[k].copies(v);
1254         });
1255
1256         cnList = [];
1257         angular.forEach(cnHash, function (v, k) {
1258             cnList.push(v);
1259         });
1260
1261         if (cnList.length == 0) return;
1262
1263         var flags = {};
1264         if (vols && copies) flags.force_delete_copies = 1;
1265
1266         egConfirmDialog.open(
1267             egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES,
1268             egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES_MESSAGE,
1269             {copies : cp_count, volumes : cn_count}
1270         ).result.then(function() {
1271             egCore.net.request(
1272                 'open-ils.cat',
1273                 'open-ils.cat.asset.volume.fleshed.batch.update.override',
1274                 egCore.auth.token(), cnList, 1, flags
1275             ).then(function(update_count) {
1276                 holdingsSvcInst.fetchAgain().then(function() {
1277                     $scope.holdingsGridDataProvider.refresh();
1278                 });
1279             });
1280         });
1281     }
1282     $scope.selectedHoldingsCopyDelete = function () { $scope.selectedHoldingsDelete(false,true) }
1283     $scope.selectedHoldingsVolCopyDelete = function () { $scope.selectedHoldingsDelete(true,true) }
1284     $scope.selectedHoldingsEmptyVolCopyDelete = function () { $scope.selectedHoldingsDelete(true,false) }
1285
1286     spawnHoldingsAdd = function (add_vols,add_copies){
1287         var raw = [];
1288         if (!add_vols && add_copies) { // just a copy on existing volumes
1289             angular.forEach(gatherSelectedVolumeIds(), function (v) {
1290                 raw.push( {callnumber : v} );
1291             });
1292         } else if (add_vols) {
1293             if (typeof $scope.holdingsGridControls.selectedItems == "function" &&
1294                 $scope.holdingsGridControls.selectedItems().length > 0) {
1295                 angular.forEach($scope.holdingsGridControls.selectedItems(),
1296                     function (item) {
1297                         raw.push({
1298                             owner : item.owner_id,
1299                             label : ((item.call_number) ? item.call_number.label : null)
1300                         });
1301                     });
1302             } else {
1303                 raw.push({
1304                     owner : egCore.auth.user().ws_ou()
1305                 });
1306             }
1307         }
1308
1309         if (raw.length == 0) raw.push({});
1310
1311         egCore.net.request(
1312             'open-ils.actor',
1313             'open-ils.actor.anon_cache.set_value',
1314             null, 'edit-these-copies', {
1315                 record_id: $scope.record_id,
1316                 raw: raw,
1317                 hide_vols : false,
1318                 hide_copies : !add_copies
1319             }
1320         ).then(function(key) {
1321             if (key) {
1322                 var url = egCore.env.basePath + 'cat/volcopy/' + key;
1323                 $timeout(function() { $window.open(url, '_blank') });
1324             } else {
1325                 alert('Could not create anonymous cache key!');
1326             }
1327         });
1328     }
1329     $scope.selectedHoldingsVolCopyAdd = function () { spawnHoldingsAdd(true,true) }
1330     $scope.selectedHoldingsCopyAdd = function () { spawnHoldingsAdd(false,true) }
1331     $scope.selectedHoldingsVolAdd = function () { spawnHoldingsAdd(true,false) }
1332
1333     spawnHoldingsEdit = function (hide_vols,hide_copies){
1334         egCore.net.request(
1335             'open-ils.actor',
1336             'open-ils.actor.anon_cache.set_value',
1337             null, 'edit-these-copies', {
1338                 record_id: $scope.record_id,
1339                 copies: gatherSelectedHoldingsIds(),
1340                 raw: gatherSelectedEmptyVolumeIds().map(
1341                     function(v){ return { callnumber : v } }
1342                 ),
1343                 hide_vols : hide_vols,
1344                 hide_copies : hide_copies
1345             }
1346         ).then(function(key) {
1347             if (key) {
1348                 var url = egCore.env.basePath + 'cat/volcopy/' + key;
1349                 $timeout(function() { $window.open(url, '_blank') });
1350             } else {
1351                 alert('Could not create anonymous cache key!');
1352             }
1353         });
1354     }
1355     $scope.selectedHoldingsVolCopyEdit = function () { spawnHoldingsEdit(false,false) }
1356     $scope.selectedHoldingsVolEdit = function () { spawnHoldingsEdit(false,true) }
1357     $scope.selectedHoldingsCopyEdit = function () { spawnHoldingsEdit(true,false) }
1358
1359     $scope.selectedHoldingsItemStatus = function (){
1360         var url = egCore.env.basePath + 'cat/item/search/' + gatherSelectedHoldingsIds().join(',')
1361         $timeout(function() { $window.open(url, '_blank') });
1362     }
1363
1364     $scope.markFromSelectedAsHoldingsTarget = function() {
1365         egCore.hatch.setLocalItem(
1366             'eg.cat.transfer_target_lib',
1367             $scope.holdingsGridControls.selectedItems()[0].owner_id
1368         );
1369         egCore.hatch.setLocalItem(
1370             'eg.cat.transfer_target_record',
1371             $scope.record_id
1372         );
1373         if ($scope.holdingsGridControls.selectedItems()[0].call_number.id) { // cn.id missing when vols are collapsed, or we are on an empty lib
1374             egCore.hatch.setLocalItem(
1375                 'eg.cat.transfer_target_vol',
1376                 $scope.holdingsGridControls.selectedItems()[0].call_number.id
1377             );
1378         } else {
1379             // clear out the stale value if we're on a lib-only
1380             // or vol-collapsed row
1381             egCore.hatch.removeLocalItem('eg.cat.transfer_target_vol');
1382         }
1383         ngToast.create(egCore.strings.MARK_HOLDINGS_TARGET);
1384     }
1385
1386     $scope.selectedHoldingsItemStatusDetail = function (){
1387         angular.forEach(
1388             gatherSelectedHoldingsIds(),
1389             function (cid) {
1390                 var url = egCore.env.basePath +
1391                           'cat/item/' + cid;
1392                 $timeout(function() { $window.open(url, '_blank') });
1393             }
1394         );
1395     }
1396
1397     $scope.transferVolumes = function (){
1398         var target_record = egCore.hatch.getLocalItem('eg.cat.transfer_target_record');
1399         var target_lib = egCore.hatch.getLocalItem('eg.cat.transfer_target_lib');
1400         if (!target_lib
1401             && (!target_record || ($scope.record_id == target_record) )
1402         ) return;
1403
1404         var vols_to_move = {};
1405         if (target_lib) {
1406             // we're moving volumes to a different library
1407             var vol_ids = gatherSelectedVolumeIds();
1408             if (vol_ids.length) {
1409                 vols_to_move[target_lib] = vol_ids;
1410
1411                 // if we're *only* switching libs,
1412                 // grab the current record as the target
1413                 target_record = target_record || $scope.record_id;
1414             }
1415         } else {
1416             // we're moving volumes to the same library they exist in
1417             // currently, but on a different record
1418             var items = $scope.holdingsGridControls.selectedItems();
1419             angular.forEach(items, function(item) {
1420                 if (!(item.call_number.owning_lib in vols_to_move)) {
1421                     vols_to_move[item.call_number.owning_lib] = new Array;
1422                 }
1423                 vols_to_move[item.call_number.owning_lib].push(item.call_number.id);
1424             });
1425         }
1426
1427         var promises = [];        
1428         angular.forEach(vols_to_move, function(vols, owning_lib) {
1429             promises.push(egCore.net.request(
1430                 'open-ils.cat',
1431                 'open-ils.cat.asset.volume.batch.transfer.override',
1432                 egCore.auth.token(), {
1433                     docid   : target_record,
1434                     lib     : owning_lib,
1435                     volumes : vols
1436                 }
1437             ));
1438         });
1439         $q.all(promises).then(function(success) {
1440             if (success) {
1441                 ngToast.create(egCore.strings.VOLS_TRANSFERED);
1442                 holdingsSvcInst.fetchAgain().then(function() {
1443                     $scope.holdingsGridDataProvider.refresh();
1444                 });
1445             } else {
1446                 alert('Could not transfer volumes!');
1447             }
1448         });
1449     }
1450
1451     // this "transfers" selected copies to a new owning library,
1452     // auto-creating volumes as required
1453     $scope.transferItemsAutoFill = function() {
1454         var target_record = egCore.hatch.getLocalItem('eg.cat.transfer_target_record');
1455         var target_lib = egCore.hatch.getLocalItem('eg.cat.transfer_target_lib');
1456         if (!target_lib
1457             && (!target_record || ($scope.record_id == target_record) )
1458         ) return;
1459
1460         var items = $scope.holdingsGridControls.selectedItems();
1461         if (!items.length) {
1462             return;
1463         }
1464
1465         var vols_to_move   = {};
1466         var copies_to_move = {};
1467         angular.forEach(items, function(item) {
1468             var needs_move = false;
1469             if (target_lib
1470                 && (item.call_number.owning_lib != target_lib)) {
1471                     item.call_number.owning_lib = target_lib;
1472                     needs_move = true;
1473             }
1474             if (target_record
1475                 && (item.call_number.record != target_record)) {
1476                     item.call_number.record = target_record;
1477                     needs_move = true;
1478             }
1479             if (needs_move) {
1480                 if (item.call_number.id in vols_to_move) {
1481                     copies_to_move[item.call_number.id].push(item.id);
1482                 } else {
1483                     vols_to_move[item.call_number.id] = item.call_number;
1484                     copies_to_move[item.call_number.id] = new Array;
1485                     copies_to_move[item.call_number.id].push(item.id);
1486                 }
1487             }
1488         });
1489
1490         var promises = [];
1491         angular.forEach(vols_to_move, function(vol) {
1492             promises.push(egCore.net.request(
1493                 'open-ils.cat',
1494                 'open-ils.cat.call_number.find_or_create',
1495                 egCore.auth.token(),
1496                 vol.label,
1497                 vol.record, // may be new
1498                 vol.owning_lib, // may be new
1499                 vol.prefix.id,
1500                 vol.suffix.id,
1501                 vol.label_class
1502             ).then(function(resp) {
1503                 var evt = egCore.evt.parse(resp);
1504                 if (evt) return;
1505                 return egCore.net.request(
1506                     'open-ils.cat',
1507                     'open-ils.cat.transfer_copies_to_volume',
1508                     egCore.auth.token(),
1509                     resp.acn_id,
1510                     copies_to_move[vol.id]
1511                 );
1512             }));
1513         });
1514         $q.all(promises).then(function() {
1515             ngToast.create(egCore.strings.ITEMS_TRANSFERED);
1516             holdingsSvcInst.fetchAgain().then(function() {
1517                 $scope.holdingsGridDataProvider.refresh();
1518             });
1519         });
1520     }
1521
1522     $scope.gridCellHandlers = {};
1523     $scope.gridCellHandlers.copyAlertsEdit = function(id) {
1524         egCirc.manage_copy_alerts([id]).then(function() {
1525             // update grid items?
1526         });
1527     };
1528
1529     $scope.transferItems = function (){
1530         var xfer_target = egCore.hatch.getLocalItem('eg.cat.transfer_target_vol');
1531
1532         if (!xfer_target) {
1533             // we have no specific volume, let's try to fill in the
1534             // blanks instead
1535             return $scope.transferItemsAutoFill();
1536         }
1537
1538         var copy_ids = gatherSelectedHoldingsIds();
1539         if (copy_ids.length > 0) {
1540             egCore.net.request(
1541                 'open-ils.cat',
1542                 'open-ils.cat.transfer_copies_to_volume',
1543                 egCore.auth.token(),
1544                 xfer_target,
1545                 copy_ids
1546             ).then(
1547                 function(resp) { // oncomplete
1548                     var evt = egCore.evt.parse(resp);
1549                     if (evt) {
1550                         egConfirmDialog.open(
1551                             egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_TITLE,
1552                             egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_BODY,
1553                             {'evt_desc': evt.desc}
1554                         ).result.then(function() {
1555                             egCore.net.request(
1556                                 'open-ils.cat',
1557                                 'open-ils.cat.transfer_copies_to_volume.override',
1558                                 egCore.auth.token(),
1559                                 xfer_target,
1560                                 copy_ids,
1561                                 { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
1562                             ).then(function(resp) {
1563                                 holdingsSvcInst.fetchAgain().then(function() {
1564                                     $scope.holdingsGridDataProvider.refresh();
1565                                 });
1566                             });
1567                         });
1568                     } else {
1569                         ngToast.create(egCore.strings.ITEMS_TRANSFERED);
1570                         holdingsSvcInst.fetchAgain().then(function() {
1571                             $scope.holdingsGridDataProvider.refresh();
1572                         });
1573                     }
1574                 },
1575                 null, // onerror
1576                 null // onprogress
1577             )
1578         }
1579     }
1580
1581     $scope.selectedHoldingsItemStatusTgrEvt = function (){
1582         angular.forEach(
1583             gatherSelectedHoldingsIds(),
1584             function (cid) {
1585                 var url = egCore.env.basePath +
1586                           'cat/item/' + cid + '/triggered_events';
1587                 $timeout(function() { $window.open(url, '_blank') });
1588             }
1589         );
1590     }
1591
1592     $scope.selectedHoldingsItemStatusHolds = function (){
1593         angular.forEach(
1594             gatherSelectedHoldingsIds(),
1595             function (cid) {
1596                 var url = egCore.env.basePath +
1597                           'cat/item/' + cid + '/holds';
1598                 $timeout(function() { $window.open(url, '_blank') });
1599             }
1600         );
1601     }
1602
1603     $scope.selectedHoldingsPrintLabels = function() {
1604         egCore.net.request(
1605             'open-ils.actor',
1606             'open-ils.actor.anon_cache.set_value',
1607             null, 'print-labels-these-copies', {
1608                 copies : gatherSelectedHoldingsIds()
1609             }
1610         ).then(function(key) {
1611             if (key) {
1612                 var url = egCore.env.basePath + 'cat/printlabels/' + key;
1613                 $timeout(function() { $window.open(url, '_blank') });
1614             } else {
1615                 alert('Could not create anonymous cache key!');
1616             }
1617         });
1618     }
1619
1620     $scope.selectedHoldingsDamaged = function () {
1621         var copy_list = gatherSelectedRawCopies();
1622         if (copy_list.length == 0) return;
1623
1624         angular.forEach(copy_list, function(cp) {
1625             egCirc.mark_damaged({
1626                 id: cp.id(),
1627                 barcode: cp.barcode(),
1628                 circ_lib: cp.circ_lib().id()
1629             }).then(function() {
1630                 holdingsSvcInst.fetchAgain().then(function() {
1631                     $scope.holdingsGridDataProvider.refresh();
1632                 });
1633             });
1634         });
1635     }
1636
1637     $scope.selectedHoldingsMissing = function () {
1638         egCirc.mark_missing(gatherSelectedHoldingsIds()).then(function() {
1639             holdingsSvcInst.fetchAgain().then(function() {
1640                 $scope.holdingsGridDataProvider.refresh();
1641             });
1642         });
1643     }
1644
1645     $scope.selectedHoldingsCopyAlertsAdd = function() {
1646         egCirc.add_copy_alerts(gatherSelectedHoldingsIds()).then(function() {
1647             // no need to refresh grid
1648         });
1649     }
1650     $scope.selectedHoldingsCopyAlertsManage = function() {
1651         egCirc.manage_copy_alerts(gatherSelectedHoldingsIds()).then(function() {
1652             // no need to refresh grid
1653         });
1654     }
1655
1656     $scope.attach_to_peer_bib = function() {
1657         var copy_list = gatherSelectedHoldingsIds();
1658         if (copy_list.length == 0) return;
1659
1660         egCore.hatch.getItem('eg.cat.marked_conjoined_record').then(function(target_record) {
1661             if (!target_record) return;
1662
1663             return $uibModal.open({
1664                 templateUrl: './cat/catalog/t_conjoined_selector',
1665                 backdrop: 'static',
1666                 animation: true,
1667                 controller:
1668                        ['$scope','$uibModalInstance',
1669                 function($scope , $uibModalInstance) {
1670                     $scope.update = false;
1671
1672                     $scope.peer_type = null;
1673                     $scope.peer_type_list = [];
1674                     conjoinedSvc.get_peer_types().then(function(list){
1675                         $scope.peer_type_list = list;
1676                     });
1677     
1678                     $scope.ok = function(type) {
1679                         var promises = [];
1680     
1681                         angular.forEach(copy_list, function (cp) {
1682                             var n = new egCore.idl.bpbcm();
1683                             n.isnew(true);
1684                             n.peer_record(target_record);
1685                             n.target_copy(cp);
1686                             n.peer_type(type);
1687                             promises.push(egCore.pcrud.create(n));
1688                         });
1689     
1690                         return $q.all(promises).then(function(){$uibModalInstance.close()});
1691                     }
1692     
1693                     $scope.cancel = function($event) {
1694                         $uibModalInstance.dismiss();
1695                         $event.preventDefault();
1696                     }
1697                 }]
1698             });
1699         });
1700     }
1701
1702
1703     // ------------------------------------------------------------------
1704     // Holds 
1705     var provider = egGridDataProvider.instance({});
1706     $scope.hold_grid_data_provider = provider;
1707     $scope.grid_actions = egHoldGridActions;
1708     $scope.grid_actions.refresh = function () { holds = []; hold_count = 0; provider.refresh() };
1709     $scope.hold_grid_controls = {};
1710
1711     var holds = []; // current list of holds
1712     var hold_count = 0;
1713     provider.get = function(offset, count) {
1714         if ($scope.record_tab != 'holds') return $q.when();
1715
1716         // see if we have the requested range cached
1717         if (holds[offset]) {
1718             return provider.arrayNotifier(holds, offset, count);
1719         }
1720
1721         hold_count = 0;
1722         holds = [];
1723         var restrictions = {
1724                 is_staff_request : 'true',
1725                 fulfillment_time : null,
1726                 cancel_time      : null,
1727                 record_id        : $scope.record_id,
1728                 pickup_lib       : egCore.org.descendants($scope.pickup_ou.id(), true)
1729         };
1730
1731         var order_by = [{ request_time : null }];
1732         if (provider.sort && provider.sort.length) {
1733             order_by = [];
1734             angular.forEach(provider.sort, function (c) {
1735                 if (!angular.isObject(c)) {
1736                     if (c.match(/^hold\./)) {
1737                         var i = c.replace('hold.','');
1738                         var ob = {};
1739                         ob[i] = null;
1740                         order_by.push(ob);
1741                     }
1742                 } else {
1743                     var i = Object.keys(c)[0];
1744                     var direction = c[i];
1745                     if (i.match(/^hold\./)) {
1746                         i = i.replace('hold.','');
1747                         var ob = {}
1748                         ob[i] = {dir:direction};
1749                         order_by.push(ob);
1750                     }
1751                 }
1752             });
1753         }
1754
1755         egProgressDialog.open({max : 1, value : 0});
1756         var first = true;
1757         return egHolds.fetch_wide_holds(
1758             restrictions,
1759             order_by
1760         ).then(function () {
1761                 return provider.arrayNotifier(holds, offset, count);
1762             },
1763             null,
1764             function(hold_data) {
1765                 if (first) {
1766                     hold_count = hold_data;
1767                     first = false;
1768                     egProgressDialog.update({max:hold_count});
1769                 } else {
1770                     egProgressDialog.increment();
1771                     var new_item = { id : hold_data.id, hold : hold_data };
1772                     new_item.status_string =
1773                         egCore.strings['HOLD_STATUS_' + hold_data.hold_status]
1774                         || hold_data.hold_status;
1775
1776                     holds.push(new_item);
1777                 }
1778             }
1779         ).finally(egProgressDialog.close);
1780
1781     }
1782
1783     $scope.detail_view = function(action, user_data, items) {
1784         if (h = items[0]) {
1785             $scope.detail_hold_id = h.hold.id;
1786         }
1787     }
1788
1789     $scope.list_view = function(items) {
1790          $scope.detail_hold_id = null;
1791     }
1792
1793     // refresh the list of record holds when the pickup lib is changed.
1794     $scope.pickup_ou = egCore.org.get(egCore.auth.user().ws_ou());
1795     $scope.pickup_ou_changed = function(org) {
1796         $scope.pickup_ou = org;
1797         holds = []
1798         hold_count = 0;
1799         provider.refresh();
1800     }
1801
1802     function map_prefix_to_subhash (h,pf) {
1803         var newhash = {};
1804         angular.forEach(Object.keys(h), function (k) {
1805             if (k.startsWith(pf)) {
1806                 var nk = k.substr(pf.length);
1807                 newhash[nk] = h[k];
1808             }
1809         });
1810         return newhash;
1811     }
1812
1813     $scope.print_holds = function() {
1814         var pholds = [];
1815         angular.forEach(holds, function(item) {
1816             pholds.push({
1817                 hold : item.hold,
1818                 status_string : item.status_string,
1819                 patron_first : item.hold.usr_first_given_name,
1820                 patron_last : item.hold.usr_family_name,
1821                 patron_alias : item.hold.usr_alias,
1822                 patron_barcode : item.hold.ucard_barcode,
1823                 copy : map_prefix_to_subhash(item.hold,'cp_'),
1824                 volume : map_prefix_to_subhash(item.hold,'cn_'),
1825                 title : item.hold.title,
1826                 author : item.hold.author
1827             });
1828         });
1829
1830         egCore.print.print({
1831             context : 'receipt', 
1832             template : 'holds_for_bib', 
1833             scope : {holds : pholds}
1834         });
1835     }
1836
1837     $scope.current_hold_transfer_dest = egCore.hatch.getLocalItem ('eg.circ.hold.title_transfer_target');
1838
1839     $scope.mark_hold_transfer_dest = function() {
1840         $scope.current_hold_transfer_dest = $scope.record_id;
1841         egCore.hatch.setLocalItem(
1842             'eg.circ.hold.title_transfer_target', $scope.record_id);
1843         ngToast.create(egCore.strings.HOLD_TRANSFER_DEST_MARKED);
1844     }
1845
1846     // UI presents this option as "all holds"
1847     $scope.transfer_holds_to_marked = function() {
1848         var hold_ids = $scope.hold_grid_controls.allItems().map(
1849             function(hold_data) {return hold_data.hold.id});
1850         egHolds.transfer_to_marked_title(hold_ids);
1851     }
1852
1853     // ------------------------------------------------------------------
1854     // Initialize the selected tab
1855
1856     // we explicitly initialize catalog_url because otherwise Firefox
1857     // ends up setting it to $BASE_URL/{{url}}, which then messes
1858     // things up. See LP#1708951
1859     $scope.catalog_url = '';
1860
1861     function init_cat_url() {
1862         // Set the initial catalog URL.  This only happens once.
1863         // The URL is otherwise generated through user navigation.
1864         if ($scope.catalog_url) return;
1865
1866         var url = $location.absUrl().replace(/\/staff.*/, '/opac/advanced');
1867
1868         // A record ID in the path indicates a request for the record-
1869         // specific page.
1870         if ($routeParams.record_id) {
1871             url = url.replace(/advanced/, '/record/' + $scope.record_id);
1872         }
1873
1874         // Jumping directly to the results page by passing a search
1875         // query via the URL.  Copy all URL params to the iframe url.
1876         if ($location.path().match(/catalog\/results/)) {
1877             url = url.replace(/advanced/, '/results?');
1878             var first = true;
1879             angular.forEach($location.search(), function(val, key) {
1880                 if (!first) url += '&';
1881                 first = false;
1882                 url += encodeURIComponent(key) 
1883                     + '=' + encodeURIComponent(val);
1884             });
1885         }
1886
1887         // if we're displaying the advanced search form, select
1888         // whatever default pane the user has chosen via workstation
1889         // preference
1890         if (url.match(/\/opac\/advanced$/)) {
1891             var adv_pane = egCore.hatch.getLocalItem('eg.search.adv_pane');
1892             if (adv_pane) {
1893                 url += '?pane=' + encodeURIComponent(adv_pane);
1894             }
1895         }
1896
1897         $scope.catalog_url = url;
1898     }
1899
1900     function init_parts_url() {
1901         $scope.parts_url = $location
1902             .absUrl()
1903             .replace(
1904                 /\/staff.*/,
1905                 '/conify/global/biblio/monograph_part?r='+$scope.record_id
1906             );
1907     }
1908
1909     $scope.set_record_tab = function(tab) {
1910         $scope.record_tab = tab;
1911
1912         switch(tab) {
1913
1914             case 'monoparts':
1915                 init_parts_url();
1916                 break;
1917
1918             case 'catalog':
1919                 init_cat_url();
1920                 break;
1921
1922             case 'holds':
1923                 $scope.detail_hold_record_id = $scope.record_id; 
1924                 // refresh the holds grid
1925                 provider.refresh();
1926
1927                 break;
1928         }
1929     }
1930
1931     $scope.set_default_record_tab = function() {
1932         egCore.hatch.setLocalItem(
1933             'eg.cat.default_record_tab', $scope.record_tab);
1934         $timeout(function(){$scope.default_tab = $scope.record_tab});
1935     }
1936
1937     var tab;
1938     if ($scope.record_id) {
1939         $scope.default_tab = egCore.hatch.getLocalItem( 'eg.cat.default_record_tab' );
1940         tab = $routeParams.record_tab || $scope.default_tab || 'catalog';
1941
1942     } else {
1943         tab = $routeParams.record_tab || 'catalog';
1944     }
1945     $scope.set_record_tab(tab);
1946
1947 }])
1948
1949 .controller('AuthorityCtrl',
1950        ['$scope','$routeParams','$location','$window','$q','egCore',
1951 function($scope , $routeParams , $location , $window , $q , egCore) {
1952
1953     // set record ID on page load if available...
1954     $scope.authority_id = $routeParams.authority_id;
1955
1956     if ($routeParams.authority_id) $scope.from_route = true;
1957     else $scope.from_route = false;
1958
1959     $scope.stop_unload = false;
1960 }])
1961
1962 .controller('URLVerifyCtrl',
1963        ['$scope','$location',
1964 function($scope , $location) {
1965     $scope.verifyurls_url = $location.absUrl().replace(/\/staff.*/, '/url_verify/sessions');
1966 }])
1967
1968 .controller('VandelayCtrl',
1969        ['$scope','$location', 'egCore', '$uibModal',
1970 function($scope , $location, egCore, $uibModal) {
1971     $scope.vandelay_url = $location.absUrl().replace(/\/staff\/cat\/catalog\/vandelay/, '/vandelay/vandelay');
1972     $scope.funcs = {};
1973     $scope.funcs.edit_marc_modal = function(bre, callback){
1974         var marcArgs = { 'marc_xml': bre.marc() };
1975         var vqbibrecId = bre.id();
1976         $uibModal.open({
1977             templateUrl: './cat/catalog/t_edit_marc_modal',
1978             backdrop: 'static',
1979             size: 'lg',
1980             controller: ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
1981                 $scope.focusMe = true;
1982                 $scope.recordId = vqbibrecId;
1983                 $scope.args = marcArgs;
1984                 $scope.dirty_flag = false;
1985                 $scope.ok = function(marg){
1986                     $uibModalInstance.close(marg);
1987                 };
1988                 $scope.cancel = function(){ $uibModalInstance.dismiss() }
1989             }]
1990         }).result.then(function(res){
1991             var new_xml = res.marc_xml;
1992             egCore.pcrud.retrieve('vqbr', vqbibrecId).then(function(vqbib){
1993                 vqbib.marc(new_xml);
1994                 egCore.pcrud.update(vqbib).then( function(){ callback(vqbibrecId); });
1995             });
1996         });
1997     };
1998 }])
1999
2000 .controller('ManageAuthoritiesCtrl',
2001        ['$scope','$location',
2002 function($scope , $location) {
2003     $scope.manageauthorities_url = $location.absUrl().replace(/\/staff.*/, '/cat/authority/list');
2004 }])
2005
2006 .controller('BatchEditCtrl',
2007        ['$scope','$location','$routeParams',
2008 function($scope , $location , $routeParams) {
2009     $scope.batchedit_url = $location.absUrl().replace(/\/eg.*/, '/opac/extras/merge_template');
2010     if ($routeParams.container_type) {
2011         switch ($routeParams.container_type) {
2012             case 'bucket':
2013                 $scope.batchedit_url += '?recordSource=b&containerid=' + $routeParams.container_id;
2014                 break;
2015             case 'record':
2016                 $scope.batchedit_url += '?recordSource=r&recid=' + $routeParams.container_id;
2017                 break;
2018         };
2019     }
2020 }])
2021
2022  
2023 .filter('boolText', function(){
2024     return function (v) {
2025         return v == 't';
2026     }
2027 })
2028
2029 .factory('conjoinedSvc', 
2030        ['egCore','$q',
2031 function(egCore , $q) {
2032
2033     var service = {
2034         items : [], // record search results
2035         index : 0, // search grid index
2036         rid : null
2037     };
2038
2039     service.flesh = {   
2040         flesh : 4, 
2041         flesh_fields : {
2042             bpbcm : ['target_copy','peer_type'],
2043             acp : ['call_number'],
2044             acn : ['record'],
2045             bre : ['simple_record']
2046         },
2047         // avoid fetching the MARC blob by specifying which
2048         // fields on the bre to select.  More may be needed.
2049         // note that fleshed fields are explicitly selected.
2050         select : { bre : ['id'] },
2051         order_by : { bpbcm : ['id'] },
2052     }
2053
2054     // resolved with the last received copy
2055     service.fetch = function(rid) {
2056         if (!rid && !service.rid) return $q.when();
2057
2058         if (rid) service.rid = rid;
2059         service.items = [];
2060         service.index = 0;
2061
2062         return egCore.pcrud.search(
2063             'bpbcm',
2064             {peer_record : service.rid},
2065             service.flesh,
2066             {atomic : true}
2067         ).then( function(list) { // finished
2068             service.items = list;
2069             return service.items;
2070         });
2071     }
2072
2073     // returns a promise resolved with the list of peer bib types
2074     service.get_peer_types = function() {
2075         if (egCore.env.bpt)
2076             return $q.when(egCore.env.bpt.list);
2077
2078         return egCore.pcrud.retrieveAll('bpt', null, {atomic : true})
2079         .then(function(list) {
2080             egCore.env.absorbList(list, 'bpt');
2081             return list;
2082         });
2083     };
2084
2085     return service;
2086 }])
2087
2088