1f1ac586913f63408f50e942726d25a8f06d9388
[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'])
11
12 .config(['ngToastProvider', function(ngToastProvider) {
13   ngToastProvider.configure({
14     verticalPosition: 'bottom',
15     animation: 'fade'
16   });
17 }])
18
19 .config(function($routeProvider, $locationProvider, $compileProvider) {
20     $locationProvider.html5Mode(true);
21     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
22
23     var resolver = {delay : ['egCore','egStartup','egUser', function(egCore, egStartup, egUser) {
24         egCore.env.classLoaders.aous = function() {
25             return egCore.org.settings([
26                 'cat.marc_control_number_identifier'
27             ]).then(function(settings) {
28                 // local settings are cached within egOrg.  Caching them
29                 // again in egEnv just simplifies the syntax for access.
30                 egCore.env.aous = settings;
31             });
32         }
33         egCore.env.loadClasses.push('aous');
34         return egStartup.go()
35     }]};
36
37     $routeProvider.when('/cat/catalog/index', {
38         templateUrl: './cat/catalog/t_catalog',
39         controller: 'CatalogCtrl',
40         resolve : resolver
41     });
42
43     // Jump directly to the results page.  Any URL parameter 
44     // supported by the embedded catalog is supported here.
45     $routeProvider.when('/cat/catalog/results', {
46         templateUrl: './cat/catalog/t_catalog',
47         controller: 'CatalogCtrl',
48         resolve : resolver
49     });
50
51     $routeProvider.when('/cat/catalog/retrieve_by_id', {
52         templateUrl: './cat/catalog/t_retrieve_by_id',
53         controller: 'CatalogRecordRetrieve',
54         resolve : resolver
55     });
56
57     $routeProvider.when('/cat/catalog/retrieve_by_tcn', {
58         templateUrl: './cat/catalog/t_retrieve_by_tcn',
59         controller: 'CatalogRecordRetrieve',
60         resolve : resolver
61     });
62
63     $routeProvider.when('/cat/catalog/new_bib', {
64         templateUrl: './cat/catalog/t_new_bib',
65         controller: 'NewBibCtrl',
66         resolve : resolver
67     });
68
69     // create some catalog page-specific mappings
70     $routeProvider.when('/cat/catalog/record/:record_id', {
71         templateUrl: './cat/catalog/t_catalog',
72         controller: 'CatalogCtrl',
73         resolve : resolver
74     });
75
76     // create some catalog page-specific mappings
77     $routeProvider.when('/cat/catalog/record/:record_id/:record_tab', {
78         templateUrl: './cat/catalog/t_catalog',
79         controller: 'CatalogCtrl',
80         resolve : resolver
81     });
82
83     $routeProvider.when('/cat/catalog/batchEdit', {
84         templateUrl: './cat/catalog/t_batchedit',
85         controller: 'BatchEditCtrl',
86         resolve : resolver
87     });
88
89     $routeProvider.when('/cat/catalog/batchEdit/:container_type/:container_id', {
90         templateUrl: './cat/catalog/t_batchedit',
91         controller: 'BatchEditCtrl',
92         resolve : resolver
93     });
94
95     $routeProvider.when('/cat/catalog/vandelay', {
96         templateUrl: './cat/catalog/t_vandelay',
97         controller: 'VandelayCtrl',
98         resolve : resolver
99     });
100
101     $routeProvider.when('/cat/catalog/verifyURLs', {
102         templateUrl: './cat/catalog/t_verifyurls',
103         controller: 'URLVerifyCtrl',
104         resolve : resolver
105     });
106
107     $routeProvider.when('/cat/catalog/manageAuthorities', {
108         templateUrl: './cat/catalog/t_manageauthorities',
109         controller: 'ManageAuthoritiesCtrl',
110         resolve : resolver
111     });
112
113     $routeProvider.when('/cat/catalog/authority/:authority_id/marc_edit', {
114         templateUrl: './cat/catalog/t_authority',
115         controller: 'AuthorityCtrl',
116         resolve : resolver
117     });
118
119     $routeProvider.otherwise({redirectTo : '/cat/catalog/index'});
120 })
121
122
123 /**
124  * */
125 .controller('CatalogRecordRetrieve',
126        ['$scope','$routeParams','$location','$q','egCore',
127 function($scope , $routeParams , $location , $q , egCore ) {
128
129     $scope.focusMe = true;
130
131     // jump to the patron checkout UI
132     function loadRecord(record_id) {
133         $location
134         .path('/cat/catalog/record/' + record_id);
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.submitTCN = function(args) {
148         $scope.recordNotFound = null;
149         $scope.moreRecordsFound = null;
150         if (!args.record_tcn) return;
151
152         // blur so next time it's set to true it will re-apply select()
153         $scope.selectMe = false;
154
155         // lookup TCN
156         egCore.net.request(
157             'open-ils.search',
158             'open-ils.search.biblio.tcn',
159             args.record_tcn)
160
161         .then(function(resp) { // get_barcodes
162
163             if (evt = egCore.evt.parse(resp)) {
164                 alert(evt); // FIXME
165                 return;
166             }
167
168             if (!resp.count) {
169                 $scope.recordNotFound = args.record_tcn;
170                 $scope.selectMe = true;
171                 return;
172             }
173
174             if (resp.count > 1) {
175                 $scope.moreRecordsFound = args.record_tcn;
176                 $scope.selectMe = true;
177                 return;
178             }
179
180             var record_id = resp.ids[0];
181             return loadRecord(record_id);
182         });
183     }
184
185 }])
186
187 .controller('NewBibCtrl',
188        ['$scope','$routeParams','$location','$window','$q','egCore',
189         'egGridDataProvider','egHoldGridActions','$timeout','holdingsSvc',
190 function($scope , $routeParams , $location , $window , $q , egCore) {
191
192     $scope.have_template = false;
193     $scope.marc_template = '';
194     $scope.stop_unload = false;
195     $scope.template_list = [];
196     $scope.template_name = '';
197     $scope.new_bib_id = 0;
198
199     egCore.net.request(
200         'open-ils.cat',
201         'open-ils.cat.marc_template.types.retrieve'
202     ).then(function(resp) {
203         angular.forEach(resp, function(name) {
204             $scope.template_list.push(name);
205         });
206         $scope.template_list.sort();
207     });
208     $scope.template_name = egCore.hatch.getSessionItem('eg.cat.last_bib_marc_template');
209     if (!$scope.template_name) {
210         egCore.hatch.getItem('cat.default_bib_marc_template').then(function(template) {
211             $scope.template_name = template;
212         });
213     }
214
215     $scope.loadTemplate = function() {
216         if ($scope.template_name) {
217             egCore.net.request(
218                 'open-ils.cat',
219                 'open-ils.cat.biblio.marc_template.retrieve',
220                 $scope.template_name
221             ).then(function(template) {
222                 $scope.marc_template = template;
223                 $scope.have_template = true;
224                 egCore.hatch.setSessionItem('eg.cat.last_bib_marc_template', $scope.template_name);
225             });
226         }
227     }
228
229     $scope.setDefaultTemplate = function() {
230         var hatch_key = "cat.default_bib_marc_template";
231         if ($scope.template_name) {
232             egCore.hatch.setItem(hatch_key, $scope.template_name);
233         } else {
234             egCore.hatch.removeItem(hatch_key);
235         }
236     }
237
238     $scope.$watch('new_bib_id', function(newVal, oldVal) {
239         if (newVal) {
240             $location.path('/cat/catalog/record/' + $scope.new_bib_id);
241         }
242     });
243     
244
245 }])
246 .controller('CatalogCtrl',
247        ['$scope','$routeParams','$location','$window','$q','egCore','egHolds','egCirc','egConfirmDialog','ngToast',
248         'egGridDataProvider','egHoldGridActions','egProgressDialog','$timeout','$uibModal','holdingsSvc','egUser','conjoinedSvc',
249         '$cookies',
250 function($scope , $routeParams , $location , $window , $q , egCore , egHolds , egCirc , egConfirmDialog , ngToast ,
251          egGridDataProvider , egHoldGridActions , egProgressDialog , $timeout , $uibModal , holdingsSvc , egUser , conjoinedSvc,
252          $cookies
253 ) {
254
255     var holdingsSvcInst = new holdingsSvc();
256
257     // set record ID on page load if available...
258     $scope.record_id = $routeParams.record_id;
259     $scope.summary_pane_record;
260
261     if ($routeParams.record_id) $scope.from_route = true;
262     else $scope.from_route = false;
263
264     // set search and preferred library cookies
265     egCore.hatch.getItem('eg.search.search_lib').then(function(val) {
266         $cookies.put('eg_search_lib', val, { path : '/' });
267     });
268     egCore.hatch.getItem('eg.search.pref_lib').then(function(val) {
269         $cookies.put('eg_pref_lib', val, { path : '/' });
270     });
271
272     // will hold a ref to the opac iframe
273     $scope.opac_iframe = null;
274     $scope.parts_iframe = null;
275
276     $scope.search_result_index = 1;
277     $scope.search_result_hit_count = 1;
278
279     $scope.$watch(
280         'opac_iframe.dom.contentWindow.search_result_index',
281         function (n,o) {
282             if (!isNaN(parseInt(n)))
283                 $scope.search_result_index = n + 1;
284         }
285     );
286
287     $scope.$watch(
288         'opac_iframe.dom.contentWindow.search_result_hit_count',
289         function (n,o) {
290             if (!isNaN(parseInt(n)))
291                 $scope.search_result_hit_count = n;
292         }
293     );
294
295     $scope.in_opac_call = false;
296     $scope.opac_call = function (opac_frame_function, force_opac_tab) {
297         if ($scope.opac_iframe) {
298             if (force_opac_tab) $scope.record_tab = 'catalog';
299             $scope.in_opac_call = true;
300             $scope.opac_iframe.dom.contentWindow[opac_frame_function]();
301             if (opac_frame_function == 'rdetailBackToResults') {
302                 $location.update_path('/cat/catalog/index');
303             }
304         }
305     }
306
307     $scope.add_to_record_bucket = function() {
308         var recId = $scope.record_id;
309         return $uibModal.open({
310             templateUrl: './cat/catalog/t_add_to_bucket',
311             animation: true,
312             size: 'md',
313             controller:
314                    ['$scope','$uibModalInstance',
315             function($scope , $uibModalInstance) {
316
317                 $scope.bucket_id = 0;
318                 $scope.newBucketName = '';
319                 $scope.allBuckets = [];
320                 egCore.net.request(
321                     'open-ils.actor',
322                     'open-ils.actor.container.retrieve_by_class.authoritative',
323                     egCore.auth.token(), egCore.auth.user().id(),
324                     'biblio', 'staff_client'
325                 ).then(function(buckets) { $scope.allBuckets = buckets; });
326
327                 $scope.add_to_bucket = function() {
328                     var item = new egCore.idl.cbrebi();
329                     item.bucket($scope.bucket_id);
330                     item.target_biblio_record_entry(recId);
331                     egCore.net.request(
332                         'open-ils.actor',
333                         'open-ils.actor.container.item.create',
334                         egCore.auth.token(), 'biblio', item
335                     ).then(function(resp) {
336                         $uibModalInstance.close();
337                     });
338                 }
339
340                 $scope.add_to_new_bucket = function() {
341                     var bucket = new egCore.idl.cbreb();
342                     bucket.owner(egCore.auth.user().id());
343                     bucket.name($scope.newBucketName);
344                     bucket.description('');
345                     bucket.btype('staff_client');
346
347                     egCore.net.request(
348                         'open-ils.actor',
349                         'open-ils.actor.container.create',
350                         egCore.auth.token(), 'biblio', bucket
351                     ).then(function(bucket) {
352                         $scope.bucket_id = bucket;
353                         $scope.add_to_bucket();
354                     });
355                 }
356
357                 $scope.cancel = function() {
358                     $uibModalInstance.dismiss();
359                 }
360             }]
361         });
362     }
363
364     $scope.current_overlay_target     = egCore.hatch.getLocalItem('eg.cat.marked_overlay_record');
365     $scope.current_voltransfer_target = egCore.hatch.getLocalItem('eg.cat.marked_volume_transfer_record');
366     $scope.current_conjoined_target   = egCore.hatch.getLocalItem('eg.cat.marked_conjoined_record');
367
368     $scope.markConjoined = function () {
369         $scope.current_conjoined_target = $scope.record_id;
370         egCore.hatch.setLocalItem('eg.cat.marked_conjoined_record',$scope.record_id);
371         ngToast.create(egCore.strings.MARK_CONJ_TARGET);
372     };
373
374     $scope.markVolTransfer = function () {
375         ngToast.create(egCore.strings.MARK_VOL_TARGET);
376         $scope.current_voltransfer_target = $scope.record_id;
377         egCore.hatch.setLocalItem('eg.cat.marked_volume_transfer_record',$scope.record_id);
378     };
379
380     $scope.markOverlay = function () {
381         $scope.current_overlay_target = $scope.record_id;
382         egCore.hatch.setLocalItem('eg.cat.marked_overlay_record',$scope.record_id);
383         ngToast.create(egCore.strings.MARK_OVERLAY_TARGET);
384     };
385
386     $scope.clearRecordMarks = function () {
387         $scope.current_overlay_target     = null;
388         $scope.current_voltransfer_target = null;
389         $scope.current_conjoined_target   = null;
390         $scope.current_hold_transfer_dest = null;
391         egCore.hatch.removeLocalItem('eg.cat.marked_volume_transfer_record');
392         egCore.hatch.removeLocalItem('eg.cat.marked_conjoined_record');
393         egCore.hatch.removeLocalItem('eg.cat.marked_overlay_record');
394         egCore.hatch.removeLocalItem('eg.circ.hold.title_transfer_target');
395     }
396
397     $scope.stop_unload = false;
398     $scope.$watch('stop_unload',
399         function(newVal, oldVal) {
400             if (newVal && newVal != oldVal && $scope.opac_iframe) {
401                 $($scope.opac_iframe.dom.contentWindow).on('beforeunload', function(){
402                     return 'There is unsaved data in this record.'
403                 });
404             } else {
405                 if ($scope.opac_iframe)
406                     $($scope.opac_iframe.dom.contentWindow).off('beforeunload');
407             }
408         }
409     );
410
411     // Set the "last bib" cookie, if we have that
412     if ($scope.record_id)
413         egCore.hatch.setLocalItem("eg.cat.last_record_retrieved", $scope.record_id);
414
415     $scope.refresh_record_callback = function (record_id) {
416         egCore.pcrud.retrieve('bre', record_id, {
417             flesh : 1,
418             flesh_fields : {
419                 bre : ['simple_record','creator','editor']
420             }
421         }).then(function(rec) {
422             rec.owner(egCore.org.get(rec.owner()));
423             $scope.summary_pane_record = rec;
424         });
425
426         return record_id;
427     }
428
429     // also set it when the iframe changes to a new record
430     $scope.handle_page = function(url) {
431
432         if (!url || url == 'about:blank') {
433             // nothing loaded.  If we already have a record ID, leave it.
434             return;
435         }
436
437         var match = url.match(/\/+opac\/+record\/+(\d+)/);
438         if (match) {
439             $scope.record_id = match[1];
440             egCore.hatch.setLocalItem("eg.cat.last_record_retrieved", $scope.record_id);
441             $scope.holdings_record_id_changed($scope.record_id);
442             conjoinedSvc.fetch($scope.record_id).then(function(){
443                 $scope.conjoinedGridDataProvider.refresh();
444             });
445             init_parts_url();
446             $location.update_path('/cat/catalog/record/' + $scope.record_id);
447         } else {
448             delete $scope.record_id;
449             $scope.from_route = false;
450         }
451
452         // child scope is executing this function, so our digest doesn't fire ... thus,
453         $scope.$apply();
454
455         if (!$scope.in_opac_call) {
456             if ($scope.record_id) {
457                 $scope.default_tab = egCore.hatch.getLocalItem( 'eg.cat.default_record_tab' );
458                 tab = $routeParams.record_tab || $scope.default_tab || 'catalog';
459             } else {
460                 tab = $routeParams.record_tab || 'catalog';
461             }
462             $scope.set_record_tab(tab);
463         } else {
464             $scope.in_opac_call = false;
465         }
466     }
467
468     // xulG catalog handlers
469     $scope.handlers = { }
470
471     // ------------------------------------------------------------------
472     // Conjoined items
473
474     $scope.conjoinedGridControls = {};
475     $scope.conjoinedGridDataProvider = egGridDataProvider.instance({
476         get : function(offset, count) {
477             return this.arrayNotifier(conjoinedSvc.items, offset, count);
478         }
479     });
480
481     $scope.changeConjoinedType = function () {
482         var peers = egCore.idl.Clone($scope.conjoinedGridControls.selectedItems());
483         angular.forEach(peers, function (p) {
484             p.target_copy(p.target_copy().id());
485             p.peer_type(p.peer_type().id());
486         });
487
488         var conjoinedGridDataProviderRef = $scope.conjoinedGridDataProvider;
489
490         return $uibModal.open({
491             templateUrl: './cat/catalog/t_conjoined_selector',
492             animation: true,
493             controller:
494                    ['$scope','$uibModalInstance',
495             function($scope , $uibModalInstance) {
496                 $scope.update = true;
497
498                 $scope.peer_type = null;
499                 $scope.peer_type_list = [];
500                 conjoinedSvc.get_peer_types().then(function(list){
501                     $scope.peer_type_list = list;
502                 });
503     
504                 $scope.ok = function(type) {
505                     var promises = [];
506     
507                     angular.forEach(peers, function (p) {
508                         p.ischanged(1);
509                         p.peer_type(type);
510                         promises.push(egCore.pcrud.update(p));
511                     });
512     
513                     return $q.all(promises)
514                         .then(function(){$uibModalInstance.close()})
515                         .then(function(){return conjoinedSvc.fetch()})
516                         .then(function(){conjoinedGridDataProviderRef.refresh()});
517                 }
518     
519                 $scope.cancel = function($event) {
520                     $uibModalInstance.dismiss();
521                     $event.preventDefault();
522                 }
523             }]
524         });
525         
526     }
527
528     $scope.refreshConjoined = function () {
529         conjoinedSvc.fetch($scope.record_id)
530         .then(function(){$scope.conjoinedGridDataProvider.refresh();});
531     }
532
533     $scope.deleteSelectedConjoined = function () {
534         var peers = $scope.conjoinedGridControls.selectedItems();
535
536         if (peers.length > 0) {
537             egConfirmDialog.open(
538                 egCore.strings.CONFIRM_DELETE_PEERS,
539                 egCore.strings.CONFIRM_DELETE_PEERS_MESSAGE,
540                 {peers : peers.length}
541             ).result.then(function() {
542                 angular.forEach(peers, function (p) {
543                     p.isdeleted(1);
544                 });
545
546                 egCore.pcrud.remove(peers).then(function() {
547                     return conjoinedSvc.fetch();
548                 }).then(function() {
549                     $scope.conjoinedGridDataProvider.refresh();
550                 });
551             });
552         }
553     }
554     if ($scope.record_id)
555         conjoinedSvc.fetch($scope.record_id);
556
557     // ------------------------------------------------------------------
558     // Holdings
559
560     $scope.holdingsGridControls = {
561         activateItem : function (item) {
562             $scope.selectedHoldingsVolCopyEdit();
563         }
564     };
565     $scope.holdingsGridDataProvider = egGridDataProvider.instance({
566         get : function(offset, count) {
567             return this.arrayNotifier(holdingsSvcInst.copies, offset, count);
568         }
569     });
570
571     $scope.add_copies_to_bucket = function() {
572         var copy_list = gatherSelectedHoldingsIds();
573         if (copy_list.length == 0) return;
574
575         return $uibModal.open({
576             templateUrl: './cat/catalog/t_add_to_bucket',
577             animation: true,
578             size: 'md',
579             controller:
580                    ['$scope','$uibModalInstance',
581             function($scope , $uibModalInstance) {
582
583                 $scope.bucket_id = 0;
584                 $scope.newBucketName = '';
585                 $scope.allBuckets = [];
586
587                 egCore.net.request(
588                     'open-ils.actor',
589                     'open-ils.actor.container.retrieve_by_class.authoritative',
590                     egCore.auth.token(), egCore.auth.user().id(),
591                     'copy', 'staff_client'
592                 ).then(function(buckets) { $scope.allBuckets = buckets; });
593
594                 $scope.add_to_bucket = function() {
595                     var promises = [];
596                     angular.forEach(copy_list, function (cp) {
597                         var item = new egCore.idl.ccbi()
598                         item.bucket($scope.bucket_id);
599                         item.target_copy(cp);
600                         promises.push(
601                             egCore.net.request(
602                                 'open-ils.actor',
603                                 'open-ils.actor.container.item.create',
604                                 egCore.auth.token(), 'copy', item
605                             )
606                         );
607
608                         return $q.all(promises).then(function() {
609                             $uibModalInstance.close();
610                         });
611                     });
612                 }
613
614                 $scope.add_to_new_bucket = function() {
615                     var bucket = new egCore.idl.ccb();
616                     bucket.owner(egCore.auth.user().id());
617                     bucket.name($scope.newBucketName);
618                     bucket.description('');
619                     bucket.btype('staff_client');
620
621                     return egCore.net.request(
622                         'open-ils.actor',
623                         'open-ils.actor.container.create',
624                         egCore.auth.token(), 'copy', bucket
625                     ).then(function(bucket) {
626                         $scope.bucket_id = bucket;
627                         $scope.add_to_bucket();
628                     });
629                 }
630
631                 $scope.cancel = function() {
632                     $uibModalInstance.dismiss();
633                 }
634             }]
635         });
636     }
637
638     // TODO: refactor common code between cat/catalog/app.js and cat/item/app.js 
639
640     $scope.need_one_selected = function() {
641         var items = $scope.holdingsGridControls.selectedItems();
642         if (items.length == 1) return false;
643         return true;
644     };
645
646     $scope.make_copies_bookable = function() {
647
648         var copies_by_record = {};
649         var record_list = [];
650         angular.forEach(
651             $scope.holdingsGridControls.selectedItems(),
652             function (item) {
653                 var record_id = item['call_number.record.id'];
654                 if (typeof copies_by_record[ record_id ] == 'undefined') {
655                     copies_by_record[ record_id ] = [];
656                     record_list.push( record_id );
657                 }
658                 copies_by_record[ record_id ].push(item.id);
659             }
660         );
661
662         var promises = [];
663         var combined_results = [];
664         angular.forEach(record_list, function(record_id) {
665             promises.push(
666                 egCore.net.request(
667                     'open-ils.booking',
668                     'open-ils.booking.resources.create_from_copies',
669                     egCore.auth.token(),
670                     copies_by_record[record_id]
671                 ).then(function(results) {
672                     if (results && results['brsrc']) {
673                         combined_results = combined_results.concat(results['brsrc']);
674                     }
675                 })
676             );
677         });
678
679         $q.all(promises).then(function() {
680             if (combined_results.length > 0) {
681                 $uibModal.open({
682                     template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
683                     animation: true,
684                     size: 'md',
685                     controller:
686                            ['$scope','$location','egCore','$uibModalInstance',
687                     function($scope , $location , egCore , $uibModalInstance) {
688
689                         $scope.funcs = {
690                             ses : egCore.auth.token(),
691                             resultant_brsrc : combined_results.map(function(o) { return o[0]; })
692                         }
693
694                         var booking_path = '/eg/conify/global/booking/resource';
695
696                         $scope.booking_admin_url =
697                             $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
698                     }]
699                 });
700             }
701         });
702     }
703
704     $scope.book_copies_now = function() {
705         var copies_by_record = {};
706         var record_list = [];
707         angular.forEach(
708             $scope.holdingsGridControls.selectedItems(),
709             function (item) {
710                 var record_id = item['call_number.record.id'];
711                 if (typeof copies_by_record[ record_id ] == 'undefined') {
712                     copies_by_record[ record_id ] = [];
713                     record_list.push( record_id );
714                 }
715                 copies_by_record[ record_id ].push(item.id);
716             }
717         );
718
719         var promises = [];
720         var combined_brt = [];
721         var combined_brsrc = [];
722         angular.forEach(record_list, function(record_id) {
723             promises.push(
724                 egCore.net.request(
725                     'open-ils.booking',
726                     'open-ils.booking.resources.create_from_copies',
727                     egCore.auth.token(),
728                     copies_by_record[record_id]
729                 ).then(function(results) {
730                     if (results && results['brt']) {
731                         combined_brt = combined_brt.concat(results['brt']);
732                     }
733                     if (results && results['brsrc']) {
734                         combined_brsrc = combined_brsrc.concat(results['brsrc']);
735                     }
736                 })
737             );
738         });
739
740         $q.all(promises).then(function() {
741             if (combined_brt.length > 0 || combined_brsrc.length > 0) {
742                 $uibModal.open({
743                     template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
744                     animation: true,
745                     size: 'md',
746                     controller:
747                            ['$scope','$location','egCore','$uibModalInstance',
748                     function($scope , $location , egCore , $uibModalInstance) {
749
750                         $scope.funcs = {
751                             ses : egCore.auth.token(),
752                             bresv_interface_opts : {
753                                 booking_results : {
754                                      brt : combined_brt
755                                     ,brsrc : combined_brsrc
756                                 }
757                             }
758                         }
759
760                         var booking_path = '/eg/booking/reservation';
761
762                         $scope.booking_admin_url =
763                             $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
764
765                     }]
766                 });
767             }
768         });
769     }
770
771
772     $scope.requestItems = function() {
773         var copy_list = gatherSelectedHoldingsIds();
774         if (copy_list.length == 0) return;
775
776         return $uibModal.open({
777             templateUrl: './cat/catalog/t_request_items',
778             animation: true,
779             controller:
780                    ['$scope','$uibModalInstance',
781             function($scope , $uibModalInstance) {
782                 $scope.user = null;
783                 $scope.first_user_fetch = true;
784
785                 $scope.hold_data = {
786                     hold_type : 'C',
787                     copy_list : copy_list,
788                     pickup_lib: egCore.org.get(egCore.auth.user().ws_ou()),
789                     user      : egCore.auth.user().id()
790                 };
791
792                 egUser.get( $scope.hold_data.user ).then(function(u) {
793                     $scope.user = u;
794                     $scope.barcode = u.card().barcode();
795                     $scope.user_name = egUser.format_name(u);
796                     $scope.hold_data.user = u.id();
797                 });
798
799                 $scope.user_name = '';
800                 $scope.barcode = '';
801                 $scope.$watch('barcode', function (n) {
802                     if (!$scope.first_user_fetch) {
803                         egUser.getByBarcode(n).then(function(u) {
804                             $scope.user = u;
805                             $scope.user_name = egUser.format_name(u);
806                             $scope.hold_data.user = u.id();
807                         }, function() {
808                             $scope.user = null;
809                             $scope.user_name = '';
810                             delete $scope.hold_data.user;
811                         });
812                     }
813                     $scope.first_user_fetch = false;
814                 });
815
816                 $scope.ok = function(h) {
817                     var args = {
818                         patronid  : h.user,
819                         hold_type : h.hold_type,
820                         pickup_lib: h.pickup_lib.id(),
821                         depth     : 0
822                     };
823
824                     egCore.net.request(
825                         'open-ils.circ',
826                         'open-ils.circ.holds.test_and_create.batch.override',
827                         egCore.auth.token(), args, h.copy_list
828                     );
829
830                     $uibModalInstance.close();
831                 }
832
833                 $scope.cancel = function($event) {
834                     $uibModalInstance.dismiss();
835                     $event.preventDefault();
836                 }
837             }]
838         });
839     }
840
841     $scope.view_place_orders = function() {
842         if (!$scope.record_id) return;
843         var url = egCore.env.basePath + 'acq/legacy/lineitem/related/' + $scope.record_id + '?target=bib';
844         $timeout(function() { $window.open(url, '_blank') });
845     }
846
847     $scope.replaceBarcodes = function() {
848         var copy_list = gatherSelectedRawCopies();
849         if (copy_list.length == 0) return;
850
851         var holdingsGridDataProviderRef = $scope.holdingsGridDataProvider;
852
853         angular.forEach(copy_list, function (cp) {
854             $uibModal.open({
855                 templateUrl: './cat/share/t_replace_barcode',
856                 animation: true,
857                 controller:
858                            ['$scope','$uibModalInstance',
859                     function($scope , $uibModalInstance) {
860                         $scope.isModal = true;
861                         $scope.focusBarcode = false;
862                         $scope.focusBarcode2 = true;
863                         $scope.barcode1 = cp.barcode();
864
865                         $scope.updateBarcode = function() {
866                             $scope.copyNotFound = false;
867                             $scope.updateOK = false;
868                 
869                             egCore.pcrud.search('acp',
870                                 {deleted : 'f', barcode : $scope.barcode1})
871                             .then(function(copy) {
872                 
873                                 if (!copy) {
874                                     $scope.focusBarcode = true;
875                                     $scope.copyNotFound = true;
876                                     return;
877                                 }
878                 
879                                 $scope.copyId = copy.id();
880                                 copy.barcode($scope.barcode2);
881                 
882                                 egCore.pcrud.update(copy).then(function(stat) {
883                                     $scope.updateOK = stat;
884                                     $scope.focusBarcode = true;
885                                     holdingsSvc.fetchAgain().then(function (){
886                                         holdingsGridDataProviderRef.refresh();
887                                     });
888                                 });
889
890                             });
891                             $uibModalInstance.close();
892                         }
893
894                         $scope.cancel = function($event) {
895                             $uibModalInstance.dismiss();
896                             $event.preventDefault();
897                         }
898                     }
899                 ]
900             });
901         });
902     }
903
904     // refresh the list of holdings when the record_id is changed.
905     $scope.holdings_record_id_changed = function(id) {
906         if ($scope.record_id != id) $scope.record_id = id;
907         console.log('record id changed to ' + id + ', loading new holdings');
908         holdingsSvcInst.fetch({
909             rid : $scope.record_id,
910             org : $scope.holdings_ou,
911             copy: $scope.holdings_show_copies,
912             vol : $scope.holdings_show_vols,
913             empty: $scope.holdings_show_empty
914         }).then(function() {
915             $scope.holdingsGridDataProvider.refresh();
916         });
917     }
918
919     // refresh the list of holdings when the filter lib is changed.
920     $scope.holdings_ou = egCore.org.get(egCore.auth.user().ws_ou());
921     $scope.holdings_ou_changed = function(org) {
922         $scope.holdings_ou = org;
923         holdingsSvcInst.fetch({
924             rid : $scope.record_id,
925             org : $scope.holdings_ou,
926             copy: $scope.holdings_show_copies,
927             vol : $scope.holdings_show_vols,
928             empty: $scope.holdings_show_empty
929         }).then(function() {
930             $scope.holdingsGridDataProvider.refresh();
931         });
932     }
933
934     $scope.holdings_cb_changed = function(cb,newVal,norefresh) {
935         $scope[cb] = newVal;
936         egCore.hatch.setItem('cat.' + cb, newVal);
937         if (!norefresh) holdingsSvcInst.fetch({
938             rid : $scope.record_id,
939             org : $scope.holdings_ou,
940             copy: $scope.holdings_show_copies,
941             vol : $scope.holdings_show_vols,
942             empty: $scope.holdings_show_empty
943         }).then(function() {
944             $scope.holdingsGridDataProvider.refresh();
945         });
946     }
947
948     egCore.hatch.getItem('cat.holdings_show_vols').then(function(x){
949         if (typeof x ==  'undefined') x = true;
950         $scope.holdings_cb_changed('holdings_show_vols',x,true);
951         $('#holdings_show_vols').prop('checked', x);
952     }).then(function(){
953         egCore.hatch.getItem('cat.holdings_show_copies').then(function(x){
954             if (typeof x ==  'undefined') x = true;
955             $scope.holdings_cb_changed('holdings_show_copies',x,true);
956             $('#holdings_show_copies').prop('checked', x);
957         }).then(function(){
958             egCore.hatch.getItem('cat.holdings_show_empty').then(function(x){
959                 if (typeof x ==  'undefined') x = true;
960                 $scope.holdings_cb_changed('holdings_show_empty',x);
961                 $('#holdings_show_empty').prop('checked', x);
962             })
963         })
964     });
965
966     $scope.vols_not_shown = function () {
967         return !$scope.holdings_show_vols;
968     }
969
970     $scope.copies_not_shown = function () {
971         return !$scope.holdings_show_copies;
972     }
973
974     $scope.holdings_checkbox_handler = function (item) {
975         $scope.holdings_cb_changed(item.checkbox,item.checked);
976     }
977
978     function gatherSelectedHoldingsIds () {
979         var cp_id_list = [];
980         angular.forEach(
981             $scope.holdingsGridControls.selectedItems(),
982             function (item) { cp_id_list = cp_id_list.concat(item.id_list) }
983         );
984         return cp_id_list;
985     }
986
987     function gatherSelectedRawCopies () {
988         var cp_list = [];
989         angular.forEach(
990             $scope.holdingsGridControls.selectedItems(),
991             function (item) { if (item.raw) cp_list = cp_list.concat(item.raw) }
992         );
993         return cp_list;
994     }
995
996     function gatherSelectedEmptyVolumeIds () {
997         var cn_id_list = [];
998         angular.forEach(
999             $scope.holdingsGridControls.selectedItems(),
1000             function (item) {
1001                 if (item.copy_count == 0)
1002                     cn_id_list.push(item.call_number.id)
1003             }
1004         );
1005         return cn_id_list;
1006     }
1007
1008     function gatherSelectedVolumeIds () {
1009         var cn_id_list = [];
1010         angular.forEach(
1011             $scope.holdingsGridControls.selectedItems(),
1012             function (item) {
1013                 if (cn_id_list.indexOf(item.call_number.id) == -1)
1014                     cn_id_list.push(item.call_number.id)
1015             }
1016         );
1017         return cn_id_list;
1018     }
1019
1020     $scope.selectedHoldingsDelete = function (vols, copies) {
1021
1022         var cnHash = {};
1023         var perCnCopies = {};
1024
1025         var cn_count = 0;
1026         var cp_count = 0;
1027
1028         angular.forEach(
1029             $scope.holdingsGridControls.selectedItems(),
1030             function (item) {
1031                 if (vols && item.raw_call_number) {
1032                     cnHash[item.call_number.id] = egCore.idl.Clone(item.raw_call_number);
1033                     cnHash[item.call_number.id].isdeleted(1);
1034                     cn_count++;
1035                 } else if (copies) {
1036                     angular.forEach(egCore.idl.Clone(item.raw), function (cp) {
1037                         cp.isdeleted(1);
1038                         cp_count++;
1039                         var cn_id = cp.call_number().id();
1040                         if (!cnHash[cn_id]) {
1041                             cnHash[cn_id] = cp.call_number();
1042                             perCnCopies[cn_id] = [cp];
1043                         } else {
1044                             perCnCopies[cn_id].push(cp);
1045                         }
1046                         cp.call_number(cn_id); // prevent loops in JSON-ification
1047                     });
1048
1049                 }
1050             }
1051         );
1052
1053         angular.forEach(perCnCopies, function (v, k) {
1054             if (vols) {
1055                 cnHash[k].isdeleted(1);
1056                 cn_count++;
1057             }
1058             cnHash[k].copies(v);
1059         });
1060
1061         cnList = [];
1062         angular.forEach(cnHash, function (v, k) {
1063             cnList.push(v);
1064         });
1065
1066         if (cnList.length == 0) return;
1067
1068         var flags = {};
1069         if (vols && copies) flags.force_delete_copies = 1;
1070
1071         egConfirmDialog.open(
1072             egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES,
1073             egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES_MESSAGE,
1074             {copies : cp_count, volumes : cn_count}
1075         ).result.then(function() {
1076             egCore.net.request(
1077                 'open-ils.cat',
1078                 'open-ils.cat.asset.volume.fleshed.batch.update.override',
1079                 egCore.auth.token(), cnList, 1, flags
1080             ).then(function(update_count) {
1081                 holdingsSvcInst.fetchAgain().then(function() {
1082                     $scope.holdingsGridDataProvider.refresh();
1083                 });
1084             });
1085         });
1086     }
1087     $scope.selectedHoldingsCopyDelete = function () { $scope.selectedHoldingsDelete(false,true) }
1088     $scope.selectedHoldingsVolCopyDelete = function () { $scope.selectedHoldingsDelete(true,true) }
1089     $scope.selectedHoldingsEmptyVolCopyDelete = function () { $scope.selectedHoldingsDelete(true,false) }
1090
1091     spawnHoldingsAdd = function (vols,copies){
1092         var raw = [];
1093         if (copies) { // just a copy on existing volumes
1094             angular.forEach(gatherSelectedVolumeIds(), function (v) {
1095                 raw.push( {callnumber : v} );
1096             });
1097         } else if (vols) {
1098             if (typeof $scope.holdingsGridControls.selectedItems == "function" &&
1099                 $scope.holdingsGridControls.selectedItems().length > 0) {
1100                 angular.forEach($scope.holdingsGridControls.selectedItems(),
1101                     function (item) {
1102                         raw.push({
1103                             owner : item.owner_id,
1104                             label : item.call_number.label
1105                         });
1106                     });
1107             } else {
1108                 raw.push({
1109                     owner : egCore.auth.user().ws_ou()
1110                 });
1111             }
1112         }
1113
1114         if (raw.length == 0) raw.push({});
1115
1116         egCore.net.request(
1117             'open-ils.actor',
1118             'open-ils.actor.anon_cache.set_value',
1119             null, 'edit-these-copies', {
1120                 record_id: $scope.record_id,
1121                 raw: raw,
1122                 hide_vols : false,
1123                 hide_copies : false
1124             }
1125         ).then(function(key) {
1126             if (key) {
1127                 var url = egCore.env.basePath + 'cat/volcopy/' + key;
1128                 $timeout(function() { $window.open(url, '_blank') });
1129             } else {
1130                 alert('Could not create anonymous cache key!');
1131             }
1132         });
1133     }
1134     $scope.selectedHoldingsVolCopyAdd = function () { spawnHoldingsAdd(true,false) }
1135     $scope.selectedHoldingsCopyAdd = function () { spawnHoldingsAdd(false,true) }
1136
1137     spawnHoldingsEdit = function (hide_vols,hide_copies){
1138         egCore.net.request(
1139             'open-ils.actor',
1140             'open-ils.actor.anon_cache.set_value',
1141             null, 'edit-these-copies', {
1142                 record_id: $scope.record_id,
1143                 copies: gatherSelectedHoldingsIds(),
1144                 raw: gatherSelectedEmptyVolumeIds().map(
1145                     function(v){ return { callnumber : v } }
1146                 ),
1147                 hide_vols : hide_vols,
1148                 hide_copies : hide_copies
1149             }
1150         ).then(function(key) {
1151             if (key) {
1152                 var url = egCore.env.basePath + 'cat/volcopy/' + key;
1153                 $timeout(function() { $window.open(url, '_blank') });
1154             } else {
1155                 alert('Could not create anonymous cache key!');
1156             }
1157         });
1158     }
1159     $scope.selectedHoldingsVolCopyEdit = function () { spawnHoldingsEdit(false,false) }
1160     $scope.selectedHoldingsVolEdit = function () { spawnHoldingsEdit(false,true) }
1161     $scope.selectedHoldingsCopyEdit = function () { spawnHoldingsEdit(true,false) }
1162
1163     $scope.selectedHoldingsItemStatus = function (){
1164         var url = egCore.env.basePath + 'cat/item/search/' + gatherSelectedHoldingsIds().join(',')
1165         $timeout(function() { $window.open(url, '_blank') });
1166     }
1167
1168     $scope.markVolAsItemTarget = function() {
1169         if ($scope.holdingsGridControls.selectedItems()[0].call_number.id) { // cn.id missing when vols are collapsed
1170             egCore.hatch.setLocalItem(
1171                 'eg.cat.item_transfer_target',
1172                 $scope.holdingsGridControls.selectedItems()[0].call_number.id
1173             );
1174             ngToast.create(egCore.strings.MARK_ITEM_TARGET);
1175         }
1176     }
1177
1178     $scope.markLibAsVolTarget = function() {
1179         return $uibModal.open({
1180             templateUrl: './cat/catalog/t_choose_vol_target_lib',
1181             animation: true,
1182             controller:
1183                    ['$scope','$uibModalInstance',
1184             function($scope , $uibModalInstance) {
1185
1186                 var orgId = egCore.hatch.getLocalItem('eg.cat.volume_transfer_target') || 1;
1187                 $scope.org = egCore.org.get(orgId);
1188                 $scope.cant_have_vols = function (id) { return !egCore.org.CanHaveVolumes(id); };
1189                 $scope.ok = function(org) {
1190                     egCore.hatch.setLocalItem(
1191                         'eg.cat.volume_transfer_target',
1192                         org.id()
1193                     );
1194                     $uibModalInstance.close();
1195                 }
1196                 $scope.cancel = function($event) {
1197                     $uibModalInstance.dismiss();
1198                     $event.preventDefault();
1199                 }
1200             }]
1201         });
1202     }
1203     $scope.markLibFromSelectedAsVolTarget = function() {
1204         egCore.hatch.setLocalItem(
1205             'eg.cat.volume_transfer_target',
1206             $scope.holdingsGridControls.selectedItems()[0].owner_id
1207         );
1208         ngToast.create(egCore.strings.MARK_VOL_TARGET);
1209     }
1210
1211     $scope.selectedHoldingsItemStatusDetail = function (){
1212         angular.forEach(
1213             gatherSelectedHoldingsIds(),
1214             function (cid) {
1215                 var url = egCore.env.basePath +
1216                           'cat/item/' + cid;
1217                 $timeout(function() { $window.open(url, '_blank') });
1218             }
1219         );
1220     }
1221
1222     $scope.transferVolumesToRecord = function (){
1223         var target_record = egCore.hatch.getLocalItem('eg.cat.marked_volume_transfer_record');
1224         if (!target_record) return;
1225         if ($scope.record_id == target_record) return;
1226         var items = $scope.holdingsGridControls.selectedItems();
1227         if (!items.length) return;
1228
1229         var vols_to_move   = {};
1230         angular.forEach(items, function(item) {
1231             if (!(item.call_number.owning_lib in vols_to_move)) {
1232                 vols_to_move[item.call_number.owning_lib] = new Array;
1233             }
1234             vols_to_move[item.call_number.owning_lib].push(item.call_number.id);
1235         });
1236
1237         var promises = [];        
1238         angular.forEach(vols_to_move, function(vols, owning_lib) {
1239             promises.push(egCore.net.request(
1240                 'open-ils.cat',
1241                 'open-ils.cat.asset.volume.batch.transfer.override',
1242                 egCore.auth.token(), {
1243                     docid   : target_record,
1244                     lib     : owning_lib,
1245                     volumes : vols
1246                 }
1247             ));
1248         });
1249         $q.all(promises).then(function(success) {
1250             if (success) {
1251                 ngToast.create(egCore.strings.VOLS_TRANSFERED);
1252                 holdingsSvcInst.fetchAgain().then(function() {
1253                     $scope.holdingsGridDataProvider.refresh();
1254                 });
1255             } else {
1256                 alert('Could not transfer volumes!');
1257             }
1258         });
1259     }
1260
1261     function transferVolumes(new_record){
1262         var xfer_target = egCore.hatch.getLocalItem('eg.cat.volume_transfer_target');
1263
1264         if (xfer_target) {
1265             egCore.net.request(
1266                 'open-ils.cat',
1267                 'open-ils.cat.asset.volume.batch.transfer.override',
1268                 egCore.auth.token(), {
1269                     docid   : (new_record ? new_record : $scope.record_id),
1270                     lib     : xfer_target,
1271                     volumes : gatherSelectedVolumeIds()
1272                 }
1273             ).then(function(success) {
1274                 if (success) {
1275                     ngToast.create(egCore.strings.VOLS_TRANSFERED);
1276                     holdingsSvcInst.fetchAgain().then(function() {
1277                         $scope.holdingsGridDataProvider.refresh();
1278                     });
1279                 } else {
1280                     alert('Could not transfer volumes!');
1281                 }
1282             });
1283         }
1284         
1285     }
1286
1287     $scope.transferVolumesToLibrary = function() {
1288         transferVolumes();
1289     }
1290
1291     $scope.transferVolumesToRecordAndLibrary = function() {
1292         var target_record = egCore.hatch.getLocalItem('eg.cat.marked_volume_transfer_record');
1293         if (!target_record) return;
1294         transferVolumes(target_record);
1295     }
1296
1297     // this "transfers" selected copies to a new owning library,
1298     // auto-creating volumes and deleting unused volumes as required.
1299     $scope.changeItemOwningLib = function() {
1300         var xfer_target = egCore.hatch.getLocalItem('eg.cat.volume_transfer_target');
1301         var items = $scope.holdingsGridControls.selectedItems();
1302         if (!xfer_target || !items.length) {
1303             return;
1304         }
1305         var vols_to_move   = {};
1306         var copies_to_move = {};
1307         angular.forEach(items, function(item) {
1308             if (item.call_number.owning_lib != xfer_target) {
1309                 if (item.call_number.id in vols_to_move) {
1310                     copies_to_move[item.call_number.id].push(item.id);
1311                 } else {
1312                     vols_to_move[item.call_number.id] = item.call_number;
1313                     copies_to_move[item.call_number.id] = new Array;
1314                     copies_to_move[item.call_number.id].push(item.id);
1315                 }
1316             }
1317         });
1318     
1319         var promises = [];
1320         angular.forEach(vols_to_move, function(vol) {
1321             promises.push(egCore.net.request(
1322                 'open-ils.cat',
1323                 'open-ils.cat.call_number.find_or_create',
1324                 egCore.auth.token(),
1325                 vol.label,
1326                 vol.record,
1327                 xfer_target,
1328                 vol.prefix.id,
1329                 vol.suffix.id,
1330                 vol.label_class
1331             ).then(function(resp) {
1332                 var evt = egCore.evt.parse(resp);
1333                 if (evt) return;
1334                 return egCore.net.request(
1335                     'open-ils.cat',
1336                     'open-ils.cat.transfer_copies_to_volume',
1337                     egCore.auth.token(),
1338                     resp.acn_id,
1339                     copies_to_move[vol.id]
1340                 );
1341             }));
1342         });
1343         $q.all(promises).then(function() {
1344             ngToast.create(egCore.strings.ITEMS_TRANSFERED);
1345             holdingsSvcInst.fetchAgain().then(function() {
1346                 $scope.holdingsGridDataProvider.refresh();
1347             });
1348         });
1349     }
1350
1351     $scope.transferItems = function (){
1352         var xfer_target = egCore.hatch.getLocalItem('eg.cat.item_transfer_target');
1353         var copy_ids = gatherSelectedHoldingsIds();
1354         if (xfer_target && copy_ids.length > 0) {
1355             egCore.net.request(
1356                 'open-ils.cat',
1357                 'open-ils.cat.transfer_copies_to_volume',
1358                 egCore.auth.token(),
1359                 xfer_target,
1360                 copy_ids
1361             ).then(
1362                 function(resp) { // oncomplete
1363                     var evt = egCore.evt.parse(resp);
1364                     if (evt) {
1365                         egConfirmDialog.open(
1366                             egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_TITLE,
1367                             egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_BODY,
1368                             {'evt_desc': evt.desc}
1369                         ).result.then(function() {
1370                             egCore.net.request(
1371                                 'open-ils.cat',
1372                                 'open-ils.cat.transfer_copies_to_volume.override',
1373                                 egCore.auth.token(),
1374                                 xfer_target,
1375                                 copy_ids,
1376                                 { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
1377                             ).then(function(resp) {
1378                                 holdingsSvcInst.fetchAgain().then(function() {
1379                                     $scope.holdingsGridDataProvider.refresh();
1380                                 });
1381                             });
1382                         });
1383                     } else {
1384                         ngToast.create(egCore.strings.ITEMS_TRANSFERED);
1385                         holdingsSvcInst.fetchAgain().then(function() {
1386                             $scope.holdingsGridDataProvider.refresh();
1387                         });
1388                     }
1389                 },
1390                 null, // onerror
1391                 null // onprogress
1392             )
1393         }
1394     }
1395
1396     $scope.selectedHoldingsItemStatusTgrEvt = function (){
1397         angular.forEach(
1398             gatherSelectedHoldingsIds(),
1399             function (cid) {
1400                 var url = egCore.env.basePath +
1401                           'cat/item/' + cid + '/triggered_events';
1402                 $timeout(function() { $window.open(url, '_blank') });
1403             }
1404         );
1405     }
1406
1407     $scope.selectedHoldingsItemStatusHolds = function (){
1408         angular.forEach(
1409             gatherSelectedHoldingsIds(),
1410             function (cid) {
1411                 var url = egCore.env.basePath +
1412                           'cat/item/' + cid + '/holds';
1413                 $timeout(function() { $window.open(url, '_blank') });
1414             }
1415         );
1416     }
1417
1418     $scope.selectedHoldingsDamaged = function () {
1419         egCirc.mark_damaged(gatherSelectedHoldingsIds()).then(function() {
1420             holdingsSvcInst.fetchAgain().then(function() {
1421                 $scope.holdingsGridDataProvider.refresh();
1422             });
1423         });
1424     }
1425
1426     $scope.selectedHoldingsMissing = function () {
1427         egCirc.mark_missing(gatherSelectedHoldingsIds()).then(function() {
1428             holdingsSvcInst.fetchAgain().then(function() {
1429                 $scope.holdingsGridDataProvider.refresh();
1430             });
1431         });
1432     }
1433
1434     $scope.attach_to_peer_bib = function() {
1435         var copy_list = gatherSelectedHoldingsIds();
1436         if (copy_list.length == 0) return;
1437
1438         egCore.hatch.getItem('eg.cat.marked_conjoined_record').then(function(target_record) {
1439             if (!target_record) return;
1440
1441             return $uibModal.open({
1442                 templateUrl: './cat/catalog/t_conjoined_selector',
1443                 animation: true,
1444                 controller:
1445                        ['$scope','$uibModalInstance',
1446                 function($scope , $uibModalInstance) {
1447                     $scope.update = false;
1448
1449                     $scope.peer_type = null;
1450                     $scope.peer_type_list = [];
1451                     conjoinedSvc.get_peer_types().then(function(list){
1452                         $scope.peer_type_list = list;
1453                     });
1454     
1455                     $scope.ok = function(type) {
1456                         var promises = [];
1457     
1458                         angular.forEach(copy_list, function (cp) {
1459                             var n = new egCore.idl.bpbcm();
1460                             n.isnew(true);
1461                             n.peer_record(target_record);
1462                             n.target_copy(cp);
1463                             n.peer_type(type);
1464                             promises.push(egCore.pcrud.create(n));
1465                         });
1466     
1467                         return $q.all(promises).then(function(){$uibModalInstance.close()});
1468                     }
1469     
1470                     $scope.cancel = function($event) {
1471                         $uibModalInstance.dismiss();
1472                         $event.preventDefault();
1473                     }
1474                 }]
1475             });
1476         });
1477     }
1478
1479
1480     // ------------------------------------------------------------------
1481     // Holds 
1482     var provider = egGridDataProvider.instance({});
1483     $scope.hold_grid_data_provider = provider;
1484     $scope.grid_actions = egHoldGridActions;
1485     $scope.grid_actions.refresh = function () { provider.refresh() };
1486     $scope.hold_grid_controls = {};
1487
1488     var hold_ids = []; // current list of holds
1489     function fetchHolds(offset, count) {
1490         var ids = hold_ids.slice(offset, offset + count);
1491
1492         return egHolds.fetch_holds(ids).then(null, null,
1493             function(hold_data) { 
1494                 return hold_data;
1495             }
1496         );
1497     }
1498
1499     provider.get = function(offset, count) {
1500         if ($scope.record_tab != 'holds') return $q.when();
1501         var deferred = $q.defer();
1502         hold_ids = []; // no caching ATM
1503
1504         // open a determinate progress dialog, max value set below.
1505         egProgressDialog.open({max : 1, value : 0});
1506
1507         // fetch the IDs
1508         egCore.net.request(
1509             'open-ils.circ',
1510             'open-ils.circ.holds.retrieve_all_from_title',
1511             egCore.auth.token(), $scope.record_id, 
1512             {pickup_lib : egCore.org.descendants($scope.pickup_ou.id(), true)}
1513         ).then(
1514             function(hold_data) {
1515                 angular.forEach(hold_data, function(list, type) {
1516                     hold_ids = hold_ids.concat(list);
1517                 });
1518
1519                 // Set the max value of the progress bar to the lesser of
1520                 // the total number of holds to fetch or the page size
1521                 // of the grid.
1522                 egProgressDialog.update(
1523                     {max : Math.min(hold_ids.length, count)});
1524
1525                 var holds_fetched = 0;
1526                 fetchHolds(offset, count)
1527                 .then(deferred.resolve, null, 
1528                     function(hold_data) {
1529                         holds_fetched++;
1530                         deferred.notify(hold_data);
1531                         egProgressDialog.increment();
1532                     }
1533                 )['finally'](egProgressDialog.close);
1534             }
1535         );
1536
1537         return deferred.promise;
1538     }
1539
1540     $scope.detail_view = function(action, user_data, items) {
1541         if (h = items[0]) {
1542             $scope.detail_hold_id = h.hold.id();
1543         }
1544     }
1545
1546     $scope.list_view = function(items) {
1547          $scope.detail_hold_id = null;
1548     }
1549
1550     // refresh the list of record holds when the pickup lib is changed.
1551     $scope.pickup_ou = egCore.org.get(egCore.auth.user().ws_ou());
1552     $scope.pickup_ou_changed = function(org) {
1553         $scope.pickup_ou = org;
1554         provider.refresh();
1555     }
1556
1557     $scope.print_holds = function() {
1558         var holds = [];
1559         angular.forEach($scope.hold_grid_controls.allItems(), function(item) {
1560             holds.push({
1561                 hold : egCore.idl.toHash(item.hold),
1562                 patron_last : item.patron_last,
1563                 patron_alias : item.patron_alias,
1564                 patron_barcode : item.patron_barcode,
1565                 copy : egCore.idl.toHash(item.copy),
1566                 volume : egCore.idl.toHash(item.volume),
1567                 title : item.mvr.title(),
1568                 author : item.mvr.author()
1569             });
1570         });
1571
1572         egCore.print.print({
1573             context : 'receipt', 
1574             template : 'holds_for_bib', 
1575             scope : {holds : holds}
1576         });
1577     }
1578
1579     $scope.current_hold_transfer_dest = egCore.hatch.getLocalItem ('eg.circ.hold.title_transfer_target');
1580
1581     $scope.mark_hold_transfer_dest = function() {
1582         $scope.current_hold_transfer_dest = $scope.record_id;
1583         egCore.hatch.setLocalItem(
1584             'eg.circ.hold.title_transfer_target', $scope.record_id);
1585         ngToast.create(egCore.strings.HOLD_TRANSFER_DEST_MARKED);
1586     }
1587
1588     // UI presents this option as "all holds"
1589     $scope.transfer_holds_to_marked = function() {
1590         var hold_ids = $scope.hold_grid_controls.allItems().map(
1591             function(hold_data) {return hold_data.hold.id()});
1592         egHolds.transfer_to_marked_title(hold_ids);
1593     }
1594
1595     // ------------------------------------------------------------------
1596     // Initialize the selected tab
1597
1598     function init_cat_url() {
1599         // Set the initial catalog URL.  This only happens once.
1600         // The URL is otherwise generated through user navigation.
1601         if ($scope.catalog_url) return; 
1602
1603         var url = $location.absUrl().replace(/\/staff.*/, '/opac/advanced');
1604
1605         // A record ID in the path indicates a request for the record-
1606         // specific page.
1607         if ($routeParams.record_id) {
1608             url = url.replace(/advanced/, '/record/' + $scope.record_id);
1609         }
1610
1611         // Jumping directly to the results page by passing a search
1612         // query via the URL.  Copy all URL params to the iframe url.
1613         if ($location.path().match(/catalog\/results/)) {
1614             url = url.replace(/advanced/, '/results?');
1615             var first = true;
1616             angular.forEach($location.search(), function(val, key) {
1617                 if (!first) url += '&';
1618                 first = false;
1619                 url += encodeURIComponent(key) 
1620                     + '=' + encodeURIComponent(val);
1621             });
1622         }
1623
1624         // if we're displaying the advanced search form, select
1625         // whatever default pane the user has chosen via workstation
1626         // preference
1627         if (url.match(/\/opac\/advanced$/)) {
1628             var adv_pane = egCore.hatch.getLocalItem('eg.search.adv_pane');
1629             if (adv_pane) {
1630                 url += '?pane=' + encodeURIComponent(adv_pane);
1631             }
1632         }
1633
1634         $scope.catalog_url = url;
1635     }
1636
1637     function init_parts_url() {
1638         $scope.parts_url = $location
1639             .absUrl()
1640             .replace(
1641                 /\/staff.*/,
1642                 '/conify/global/biblio/monograph_part?r='+$scope.record_id
1643             );
1644     }
1645
1646     $scope.set_record_tab = function(tab) {
1647         $scope.record_tab = tab;
1648
1649         switch(tab) {
1650
1651             case 'monoparts':
1652                 init_parts_url();
1653                 break;
1654
1655             case 'catalog':
1656                 init_cat_url();
1657                 break;
1658
1659             case 'holds':
1660                 $scope.detail_hold_record_id = $scope.record_id; 
1661                 // refresh the holds grid
1662                 provider.refresh();
1663
1664                 break;
1665         }
1666     }
1667
1668     $scope.set_default_record_tab = function() {
1669         egCore.hatch.setLocalItem(
1670             'eg.cat.default_record_tab', $scope.record_tab);
1671         $timeout(function(){$scope.default_tab = $scope.record_tab});
1672     }
1673
1674     var tab;
1675     if ($scope.record_id) {
1676         $scope.default_tab = egCore.hatch.getLocalItem( 'eg.cat.default_record_tab' );
1677         tab = $routeParams.record_tab || $scope.default_tab || 'catalog';
1678
1679     } else {
1680         tab = $routeParams.record_tab || 'catalog';
1681     }
1682     $scope.set_record_tab(tab);
1683
1684 }])
1685
1686 .controller('AuthorityCtrl',
1687        ['$scope','$routeParams','$location','$window','$q','egCore',
1688 function($scope , $routeParams , $location , $window , $q , egCore) {
1689
1690     // set record ID on page load if available...
1691     $scope.authority_id = $routeParams.authority_id;
1692
1693     if ($routeParams.authority_id) $scope.from_route = true;
1694     else $scope.from_route = false;
1695
1696     $scope.stop_unload = false;
1697 }])
1698
1699 .controller('URLVerifyCtrl',
1700        ['$scope','$location',
1701 function($scope , $location) {
1702     $scope.verifyurls_url = $location.absUrl().replace(/\/staff.*/, '/url_verify/sessions');
1703 }])
1704
1705 .controller('VandelayCtrl',
1706        ['$scope','$location',
1707 function($scope , $location) {
1708     $scope.vandelay_url = $location.absUrl().replace(/\/staff\/cat\/catalog\/vandelay/, '/vandelay/vandelay');
1709 }])
1710
1711 .controller('ManageAuthoritiesCtrl',
1712        ['$scope','$location',
1713 function($scope , $location) {
1714     $scope.manageauthorities_url = $location.absUrl().replace(/\/staff.*/, '/cat/authority/list');
1715 }])
1716
1717 .controller('BatchEditCtrl',
1718        ['$scope','$location','$routeParams',
1719 function($scope , $location , $routeParams) {
1720     $scope.batchedit_url = $location.absUrl().replace(/\/eg.*/, '/opac/extras/merge_template');
1721     if ($routeParams.container_type) {
1722         switch ($routeParams.container_type) {
1723             case 'bucket':
1724                 $scope.batchedit_url += '?recordSource=b&containerid=' + $routeParams.container_id;
1725                 break;
1726             case 'record':
1727                 $scope.batchedit_url += '?recordSource=r&recid=' + $routeParams.container_id;
1728                 break;
1729         };
1730     }
1731 }])
1732
1733  
1734 .filter('boolText', function(){
1735     return function (v) {
1736         return v == 't';
1737     }
1738 })
1739
1740 .factory('conjoinedSvc', 
1741        ['egCore','$q',
1742 function(egCore , $q) {
1743
1744     var service = {
1745         items : [], // record search results
1746         index : 0, // search grid index
1747         rid : null
1748     };
1749
1750     service.flesh = {   
1751         flesh : 4, 
1752         flesh_fields : {
1753             bpbcm : ['target_copy','peer_type'],
1754             acp : ['call_number'],
1755             acn : ['record'],
1756             bre : ['simple_record']
1757         },
1758         // avoid fetching the MARC blob by specifying which
1759         // fields on the bre to select.  More may be needed.
1760         // note that fleshed fields are explicitly selected.
1761         select : { bre : ['id'] },
1762         order_by : { bpbcm : ['id'] },
1763     }
1764
1765     // resolved with the last received copy
1766     service.fetch = function(rid) {
1767         if (!rid && !service.rid) return $q.when();
1768
1769         if (rid) service.rid = rid;
1770         service.items = [];
1771         service.index = 0;
1772
1773         return egCore.pcrud.search(
1774             'bpbcm',
1775             {peer_record : service.rid},
1776             service.flesh,
1777             {atomic : true}
1778         ).then( function(list) { // finished
1779             service.items = list;
1780             return service.items;
1781         });
1782     }
1783
1784     // returns a promise resolved with the list of peer bib types
1785     service.get_peer_types = function() {
1786         if (egCore.env.bpt)
1787             return $q.when(egCore.env.bpt.list);
1788
1789         return egCore.pcrud.retrieveAll('bpt', null, {atomic : true})
1790         .then(function(list) {
1791             egCore.env.absorbList(list, 'bpt');
1792             return list;
1793         });
1794     };
1795
1796     return service;
1797 }])
1798
1799