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