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