]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/cat/item/app.js
d873abe5d80ea83626bf01be8f41cde6c58b8fbe
[Evergreen.git] / Open-ILS / web / js / ui / default / staff / cat / item / app.js
1 /**
2  * Item Display
3  */
4
5 angular.module('egItemStatus', 
6     ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod'])
7
8 .filter('boolText', function(){
9     return function (v) {
10         return v == 't';
11     }
12 })
13
14 .config(function($routeProvider, $locationProvider, $compileProvider) {
15     $locationProvider.html5Mode(true);
16     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
17
18     var resolver = {delay : function(egStartup) {return egStartup.go()}};
19
20     // search page shows the list view by default
21     $routeProvider.when('/cat/item/search', {
22         templateUrl: './cat/item/t_list',
23         controller: 'ListCtrl',
24         resolve : resolver
25     });
26
27     // search page shows the list view by default
28     $routeProvider.when('/cat/item/search/:idList', {
29         templateUrl: './cat/item/t_list',
30         controller: 'ListCtrl',
31         resolve : resolver
32     });
33
34     $routeProvider.when('/cat/item/:id', {
35         templateUrl: './cat/item/t_view',
36         controller: 'ViewCtrl',
37         resolve : resolver
38     });
39
40     $routeProvider.when('/cat/item/:id/:tab', {
41         templateUrl: './cat/item/t_view',
42         controller: 'ViewCtrl',
43         resolve : resolver
44     });
45
46     // default page / bucket view
47     $routeProvider.otherwise({redirectTo : '/cat/item/search'});
48 })
49
50 .factory('itemSvc', 
51        ['egCore',
52 function(egCore) {
53
54     var service = {
55         copies : [], // copy barcode search results
56         index : 0 // search grid index
57     };
58
59     service.flesh = {   
60         flesh : 3, 
61         flesh_fields : {
62             acp : ['call_number','location','status','location','floating','circ_modifier','age_protect'],
63             acn : ['record','prefix','suffix'],
64             bre : ['simple_record','creator','editor']
65         },
66         select : { 
67             // avoid fleshing MARC on the bre
68             // note: don't add simple_record.. not sure why
69             bre : ['id','tcn_value','creator','editor'],
70         } 
71     }
72
73     // resolved with the last received copy
74     service.fetch = function(barcode, id, noListDupes) {
75         var promise;
76
77         if (barcode) {
78             promise = egCore.pcrud.search('acp', 
79                 {barcode : barcode, deleted : 'f'}, service.flesh);
80         } else {
81             promise = egCore.pcrud.retrieve('acp', id, service.flesh);
82         }
83
84         var lastRes;
85         return promise.then(
86             function() {return lastRes},
87             null, // error
88
89             // notify reads the stream of copies, one at a time.
90             function(copy) {
91
92                 var flatCopy;
93                 if (noListDupes) {
94                     // use the existing copy if possible
95                     flatCopy = service.copies.filter(
96                         function(c) {return c.id == copy.id()})[0];
97                 }
98
99                 if (!flatCopy) {
100                     flatCopy = egCore.idl.toHash(copy, true);
101                     flatCopy.index = service.index++;
102                     service.copies.unshift(flatCopy);
103                 }
104
105                 return lastRes = {
106                     copy : copy, 
107                     index : flatCopy.index
108                 }
109             }
110         );
111     }
112
113     return service;
114 }])
115
116 /**
117  * Search bar along the top of the page.
118  * Parent scope for list and detail views
119  */
120 .controller('SearchCtrl', 
121        ['$scope','$location','egCore','egGridDataProvider','itemSvc',
122 function($scope , $location , egCore , egGridDataProvider , itemSvc) {
123     $scope.args = {}; // search args
124
125     // sub-scopes (search / detail-view) apply their version 
126     // of retrieval function to $scope.context.search
127     // and display toggling via $scope.context.toggleDisplay
128     $scope.context = {
129         selectBarcode : true
130     };
131
132     $scope.toggleView = function($event) {
133         $scope.context.toggleDisplay();
134         $event.preventDefault(); // avoid form submission
135     }
136 }])
137
138 /**
139  * List view - grid stuff
140  */
141 .controller('ListCtrl', 
142        ['$scope','$q','$routeParams','$location','$timeout','$window','egCore','egGridDataProvider','itemSvc','egUser','$uibModal','egCirc','egConfirmDialog',
143 function($scope , $q , $routeParams , $location , $timeout , $window , egCore , egGridDataProvider , itemSvc , egUser , $uibModal , egCirc , egConfirmDialog) {
144     var copyId = [];
145     var cp_list = $routeParams.idList;
146     if (cp_list) {
147         copyId = cp_list.split(',');
148     }
149
150     $scope.context.page = 'list';
151
152     /*
153     var provider = egGridDataProvider.instance();
154     provider.get = function(offset, count) {
155     }
156     */
157
158     $scope.gridDataProvider = egGridDataProvider.instance({
159         get : function(offset, count) {
160             //return provider.arrayNotifier(itemSvc.copies, offset, count);
161             return this.arrayNotifier(itemSvc.copies, offset, count);
162         }
163     });
164
165     // If a copy was just displayed in the detail view, ensure it's
166     // focused in the list view.
167     var selected = false;
168     var copyGrid = $scope.gridControls = {
169         itemRetrieved : function(item) {
170             if (selected || !itemSvc.copy) return;
171             if (itemSvc.copy.id() == item.id) {
172                 copyGrid.selectItems([item.index]);
173                 selected = true;
174             }
175         }
176     };
177
178     $scope.$watch('barcodesFromFile', function(newVal, oldVal) {
179         if (newVal && newVal != oldVal) {
180             $scope.args.barcode = '';
181             var barcodes = [];
182
183             angular.forEach(newVal.split(/\n/), function(line) {
184                 if (!line) return;
185                 // scrub any trailing spaces or commas from the barcode
186                 line = line.replace(/(.*?)($|\s.*|,.*)/,'$1');
187                 barcodes.push(line);
188             });
189
190             itemSvc.fetch(barcodes).then(
191                 function() {
192                     copyGrid.refresh();
193                     copyGrid.selectItems([itemSvc.copies[0].index]);
194                 }
195             );
196         }
197     });
198
199     $scope.context.search = function(args) {
200         if (!args.barcode) return;
201         $scope.context.itemNotFound = false;
202         itemSvc.fetch(args.barcode).then(function(res) {
203             if (res) {
204                 copyGrid.refresh();
205                 copyGrid.selectItems([res.index]);
206                 $scope.args.barcode = '';
207             } else {
208                 $scope.context.itemNotFound = true;
209             }
210             $scope.context.selectBarcode = true;
211         })
212     }
213
214     var add_barcode_to_list = function (b) {
215         $scope.context.search({barcode:b});
216     }
217
218     $scope.context.toggleDisplay = function() {
219         var item = copyGrid.selectedItems()[0];
220         if (item) 
221             $location.path('/cat/item/' + item.id);
222     }
223
224     $scope.context.show_triggered_events = function() {
225         var item = copyGrid.selectedItems()[0];
226         if (item) 
227             $location.path('/cat/item/' + item.id + '/triggered_events');
228     }
229
230     function gatherSelectedRecordIds () {
231         var rid_list = [];
232         angular.forEach(
233             copyGrid.selectedItems(),
234             function (item) {
235                 if (rid_list.indexOf(item['call_number.record.id']) == -1)
236                     rid_list.push(item['call_number.record.id'])
237             }
238         );
239         return rid_list;
240     }
241
242     function gatherSelectedVolumeIds (rid) {
243         var cn_id_list = [];
244         angular.forEach(
245             copyGrid.selectedItems(),
246             function (item) {
247                 if (rid && item['call_number.record.id'] != rid) return;
248                 if (cn_id_list.indexOf(item['call_number.id']) == -1)
249                     cn_id_list.push(item['call_number.id'])
250             }
251         );
252         return cn_id_list;
253     }
254
255     function gatherSelectedHoldingsIds (rid) {
256         var cp_id_list = [];
257         angular.forEach(
258             copyGrid.selectedItems(),
259             function (item) {
260                 if (rid && item['call_number.record.id'] != rid) return;
261                 cp_id_list.push(item.id)
262             }
263         );
264         return cp_id_list;
265     }
266
267     $scope.add_copies_to_bucket = function() {
268         var copy_list = gatherSelectedHoldingsIds();
269         if (copy_list.length == 0) return;
270
271         return $uibModal.open({
272             templateUrl: './cat/catalog/t_add_to_bucket',
273             animation: true,
274             size: 'md',
275             controller:
276                    ['$scope','$uibModalInstance',
277             function($scope , $uibModalInstance) {
278
279                 $scope.bucket_id = 0;
280                 $scope.newBucketName = '';
281                 $scope.allBuckets = [];
282
283                 egCore.net.request(
284                     'open-ils.actor',
285                     'open-ils.actor.container.retrieve_by_class.authoritative',
286                     egCore.auth.token(), egCore.auth.user().id(),
287                     'copy', 'staff_client'
288                 ).then(function(buckets) { $scope.allBuckets = buckets; });
289
290                 $scope.add_to_bucket = function() {
291                     var promises = [];
292                     angular.forEach(copy_list, function (cp) {
293                         var item = new egCore.idl.ccbi()
294                         item.bucket($scope.bucket_id);
295                         item.target_copy(cp);
296                         promises.push(
297                             egCore.net.request(
298                                 'open-ils.actor',
299                                 'open-ils.actor.container.item.create',
300                                 egCore.auth.token(), 'copy', item
301                             )
302                         );
303
304                         return $q.all(promises).then(function() {
305                             $uibModalInstance.close();
306                         });
307                     });
308                 }
309
310                 $scope.add_to_new_bucket = function() {
311                     var bucket = new egCore.idl.ccb();
312                     bucket.owner(egCore.auth.user().id());
313                     bucket.name($scope.newBucketName);
314                     bucket.description('');
315                     bucket.btype('staff_client');
316
317                     return egCore.net.request(
318                         'open-ils.actor',
319                         'open-ils.actor.container.create',
320                         egCore.auth.token(), 'copy', bucket
321                     ).then(function(bucket) {
322                         $scope.bucket_id = bucket;
323                         $scope.add_to_bucket();
324                     });
325                 }
326
327                 $scope.cancel = function() {
328                     $uibModalInstance.dismiss();
329                 }
330             }]
331         });
332     }
333
334     $scope.make_copies_bookable = function() {
335
336         var copies_by_record = {};
337         var record_list = [];
338         angular.forEach(
339             copyGrid.selectedItems(),
340             function (item) {
341                 var record_id = item['call_number.record.id'];
342                 if (typeof copies_by_record[ record_id ] == 'undefined') {
343                     copies_by_record[ record_id ] = [];
344                     record_list.push( record_id );
345                 }
346                 copies_by_record[ record_id ].push(item.id);
347             }
348         );
349
350         var promises = [];
351         var combined_results = [];
352         angular.forEach(record_list, function(record_id) {
353             promises.push(
354                 egCore.net.request(
355                     'open-ils.booking',
356                     'open-ils.booking.resources.create_from_copies',
357                     egCore.auth.token(),
358                     copies_by_record[record_id]
359                 ).then(function(results) {
360                     if (results && results['brsrc']) {
361                         combined_results = combined_results.concat(results['brsrc']);
362                     }
363                 })
364             );
365         });
366
367         $q.all(promises).then(function() {
368             if (combined_results.length > 0) {
369                 $uibModal.open({
370                     template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
371                     animation: true,
372                     size: 'md',
373                     controller:
374                            ['$scope','$location','egCore','$uibModalInstance',
375                     function($scope , $location , egCore , $uibModalInstance) {
376
377                         $scope.funcs = {
378                             ses : egCore.auth.token(),
379                             resultant_brsrc : combined_results.map(function(o) { return o[0]; })
380                         }
381
382                         var booking_path = '/eg/conify/global/booking/resource';
383
384                         $scope.booking_admin_url =
385                             $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
386                     }]
387                 });
388             }
389         });
390     }
391
392     $scope.requestItems = function() {
393         var copy_list = gatherSelectedHoldingsIds();
394         if (copy_list.length == 0) return;
395
396         return $uibModal.open({
397             templateUrl: './cat/catalog/t_request_items',
398             animation: true,
399             controller:
400                    ['$scope','$uibModalInstance','egUser',
401             function($scope , $uibModalInstance , egUser) {
402                 $scope.user = null;
403                 $scope.first_user_fetch = true;
404
405                 $scope.hold_data = {
406                     hold_type : 'C',
407                     copy_list : copy_list,
408                     pickup_lib: egCore.org.get(egCore.auth.user().ws_ou()),
409                     user      : egCore.auth.user().id()
410                 };
411
412                 egUser.get( $scope.hold_data.user ).then(function(u) {
413                     $scope.user = u;
414                     $scope.barcode = u.card().barcode();
415                     $scope.user_name = egUser.format_name(u);
416                     $scope.hold_data.user = u.id();
417                 });
418
419                 $scope.user_name = '';
420                 $scope.barcode = '';
421                 $scope.$watch('barcode', function (n) {
422                     if (!$scope.first_user_fetch) {
423                         egUser.getByBarcode(n).then(function(u) {
424                             $scope.user = u;
425                             $scope.user_name = egUser.format_name(u);
426                             $scope.hold_data.user = u.id();
427                         }, function() {
428                             $scope.user = null;
429                             $scope.user_name = '';
430                             delete $scope.hold_data.user;
431                         });
432                     }
433                     $scope.first_user_fetch = false;
434                 });
435
436                 $scope.ok = function(h) {
437                     var args = {
438                         patronid  : h.user,
439                         hold_type : h.hold_type,
440                         pickup_lib: h.pickup_lib.id(),
441                         depth     : 0
442                     };
443
444                     egCore.net.request(
445                         'open-ils.circ',
446                         'open-ils.circ.holds.test_and_create.batch.override',
447                         egCore.auth.token(), args, h.copy_list
448                     );
449
450                     $uibModalInstance.close();
451                 }
452
453                 $scope.cancel = function($event) {
454                     $uibModalInstance.dismiss();
455                     $event.preventDefault();
456                 }
457             }]
458         });
459     }
460
461     $scope.replaceBarcodes = function() {
462         angular.forEach(copyGrid.selectedItems(), function (cp) {
463             $uibModal.open({
464                 templateUrl: './cat/share/t_replace_barcode',
465                 animation: true,
466                 controller:
467                            ['$scope','$uibModalInstance',
468                     function($scope , $uibModalInstance) {
469                         $scope.isModal = true;
470                         $scope.focusBarcode = false;
471                         $scope.focusBarcode2 = true;
472                         $scope.barcode1 = cp.barcode;
473
474                         $scope.updateBarcode = function() {
475                             $scope.copyNotFound = false;
476                             $scope.updateOK = false;
477
478                             egCore.pcrud.search('acp',
479                                 {deleted : 'f', barcode : $scope.barcode1})
480                             .then(function(copy) {
481
482                                 if (!copy) {
483                                     $scope.focusBarcode = true;
484                                     $scope.copyNotFound = true;
485                                     return;
486                                 }
487
488                                 $scope.copyId = copy.id();
489                                 copy.barcode($scope.barcode2);
490
491                                 egCore.pcrud.update(copy).then(function(stat) {
492                                     $scope.updateOK = stat;
493                                     $scope.focusBarcode = true;
494                                     if (stat) add_barcode_to_list(copy.barcode());
495                                 });
496
497                             });
498                             $uibModalInstance.close();
499                         }
500
501                         $scope.cancel = function($event) {
502                             $uibModalInstance.dismiss();
503                             $event.preventDefault();
504                         }
505                     }
506                 ]
507             });
508         });
509     }
510
511     $scope.attach_to_peer_bib = function() {
512         if (copyGrid.selectedItems().length == 0) return;
513
514         egCore.hatch.getItem('eg.cat.marked_conjoined_record').then(function(target_record) {
515             if (!target_record) return;
516
517             return $uibModal.open({
518                 templateUrl: './cat/catalog/t_conjoined_selector',
519                 animation: true,
520                 controller:
521                        ['$scope','$uibModalInstance',
522                 function($scope , $uibModalInstance) {
523                     $scope.update = false;
524
525                     $scope.peer_type = null;
526                     $scope.peer_type_list = [];
527
528                     get_peer_types = function() {
529                         if (egCore.env.bpt)
530                             return $q.when(egCore.env.bpt.list);
531
532                         return egCore.pcrud.retrieveAll('bpt', null, {atomic : true})
533                         .then(function(list) {
534                             egCore.env.absorbList(list, 'bpt');
535                             return list;
536                         });
537                     }
538
539                     get_peer_types().then(function(list){
540                         $scope.peer_type_list = list;
541                     });
542
543                     $scope.ok = function(type) {
544                         var promises = [];
545
546                         angular.forEach(copyGrid.selectedItems(), function (cp) {
547                             var n = new egCore.idl.bpbcm();
548                             n.isnew(true);
549                             n.peer_record(target_record);
550                             n.target_copy(cp.id);
551                             n.peer_type(type);
552                             promises.push(egCore.pcrud.create(n).then(function(){add_barcode_to_list(cp.barcode)}));
553                         });
554
555                         return $q.all(promises).then(function(){$uibModalInstance.close()});
556                     }
557
558                     $scope.cancel = function($event) {
559                         $uibModalInstance.dismiss();
560                         $event.preventDefault();
561                     }
562                 }]
563             });
564         });
565     }
566
567     $scope.selectedHoldingsCopyDelete = function () {
568         var copy_list = gatherSelectedHoldingsIds();
569         if (copy_list.length == 0) return;
570
571         var copy_objects = [];
572         egCore.pcrud.search('acp',
573             {deleted : 'f', id : copy_list},
574             { flesh : 1, flesh_fields : { acp : ['call_number'] } }
575         ).then(function(copy) {
576             copy_objects.push(copy);
577         }).then(function() {
578
579             var cnHash = {};
580             var perCnCopies = {};
581
582             var cn_count = 0;
583             var cp_count = 0;
584
585             angular.forEach(
586                 copy_objects,
587                 function (cp) {
588                     cp.isdeleted(1);
589                     cp_count++;
590                     var cn_id = cp.call_number().id();
591                     if (!cnHash[cn_id]) {
592                         cnHash[cn_id] = cp.call_number();
593                         perCnCopies[cn_id] = [cp];
594                     } else {
595                         perCnCopies[cn_id].push(cp);
596                     }
597                     cp.call_number(cn_id); // prevent loops in JSON-ification
598                 }
599             );
600
601             angular.forEach(perCnCopies, function (v, k) {
602                 cnHash[k].copies(v);
603             });
604
605             cnList = [];
606             angular.forEach(cnHash, function (v, k) {
607                 cnList.push(v);
608             });
609
610             if (cnList.length == 0) return;
611
612             var flags = {};
613
614             egConfirmDialog.open(
615                 egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES,
616                 egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES_MESSAGE,
617                 {copies : cp_count, volumes : cn_count}
618             ).result.then(function() {
619                 egCore.net.request(
620                     'open-ils.cat',
621                     'open-ils.cat.asset.volume.fleshed.batch.update.override',
622                     egCore.auth.token(), cnList, 1, flags
623                 ).then(function(){
624                     angular.forEach(copyGrid.selectedItems(), function(cp){add_barcode_to_list(cp.barcode)});
625                 });
626             });
627         });
628     }
629
630     $scope.selectedHoldingsItemStatusTgrEvt= function() {
631         var item = copyGrid.selectedItems()[0];
632         if (item)
633             $location.path('/cat/item/' + item.id + '/triggered_events');
634     }
635
636     $scope.selectedHoldingsItemStatusHolds= function() {
637         var item = copyGrid.selectedItems()[0];
638         if (item)
639             $location.path('/cat/item/' + item.id + '/holds');
640     }
641
642     $scope.cancel_transit = function () {
643         var initial_list = copyGrid.selectedItems();
644         angular.forEach(copyGrid.selectedItems(), function(cp) {
645             egCirc.find_copy_transit(null, {copy_barcode:cp.barcode})
646                 .then(function(t) { return egCirc.abort_transit(t.id())    })
647                 .then(function()  { return add_barcode_to_list(cp.barcode) });
648         });
649     }
650
651     $scope.selectedHoldingsDamaged = function () {
652         var initial_list = copyGrid.selectedItems();
653         egCirc.mark_damaged(gatherSelectedHoldingsIds()).then(function(){
654             angular.forEach(initial_list, function(cp){add_barcode_to_list(cp.barcode)});
655         });
656     }
657
658     $scope.selectedHoldingsMissing = function () {
659         var initial_list = copyGrid.selectedItems();
660         egCirc.mark_missing(gatherSelectedHoldingsIds()).then(function(){
661             angular.forEach(initial_list, function(cp){add_barcode_to_list(cp.barcode)});
662         });
663     }
664
665     $scope.checkin = function () {
666         angular.forEach(copyGrid.selectedItems(), function (cp) {
667             egCirc.checkin({copy_barcode:cp.barcode}).then(
668                 function() { add_barcode_to_list(cp.barcode) }
669             );
670         });
671     }
672
673     $scope.renew = function () {
674         angular.forEach(copyGrid.selectedItems(), function (cp) {
675             egCirc.renew({copy_barcode:cp.barcode}).then(
676                 function() { add_barcode_to_list(cp.barcode) }
677             );
678         });
679     }
680
681
682     var spawnHoldingsAdd = function (vols,copies){
683         angular.forEach(gatherSelectedRecordIds(), function (r) {
684             var raw = [];
685             if (copies) { // just a copy on existing volumes
686                 angular.forEach(gatherSelectedVolumeIds(r), function (v) {
687                     raw.push( {callnumber : v} );
688                 });
689             } else if (vols) {
690                 angular.forEach(
691                     gatherSelectedHoldingsIds(r),
692                     function (i) {
693                         angular.forEach(copyGrid.selectedItems(), function(item) {
694                             if (i == item.id) raw.push({owner : item['call_number.owning_lib']});
695                         });
696                     }
697                 );
698             }
699
700             if (raw.length == 0) raw.push({});
701
702             egCore.net.request(
703                 'open-ils.actor',
704                 'open-ils.actor.anon_cache.set_value',
705                 null, 'edit-these-copies', {
706                     record_id: r,
707                     raw: raw,
708                     hide_vols : false,
709                     hide_copies : false
710                 }
711             ).then(function(key) {
712                 if (key) {
713                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
714                     $timeout(function() { $window.open(url, '_blank') });
715                 } else {
716                     alert('Could not create anonymous cache key!');
717                 }
718             });
719         });
720     }
721     $scope.selectedHoldingsVolCopyAdd = function () { spawnHoldingsAdd(true,false) }
722     $scope.selectedHoldingsCopyAdd = function () { spawnHoldingsAdd(false,true) }
723
724     $scope.showBibHolds = function () {
725         angular.forEach(gatherSelectedRecordIds(), function (r) {
726             var url = egCore.env.basePath + 'cat/catalog/record/' + r + '/holds';
727             $timeout(function() { $window.open(url, '_blank') });
728         });
729     }
730
731     var spawnHoldingsEdit = function (hide_vols,hide_copies){
732         angular.forEach(gatherSelectedRecordIds(), function (r) {
733             egCore.net.request(
734                 'open-ils.actor',
735                 'open-ils.actor.anon_cache.set_value',
736                 null, 'edit-these-copies', {
737                     record_id: r,
738                     copies: gatherSelectedHoldingsIds(r),
739                     raw: {},
740                     hide_vols : hide_vols,
741                     hide_copies : hide_copies
742                 }
743             ).then(function(key) {
744                 if (key) {
745                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
746                     $timeout(function() { $window.open(url, '_blank') });
747                 } else {
748                     alert('Could not create anonymous cache key!');
749                 }
750             });
751         });
752     }
753     $scope.selectedHoldingsVolCopyEdit = function () { spawnHoldingsEdit(false,false) }
754     $scope.selectedHoldingsVolEdit = function () { spawnHoldingsEdit(false,true) }
755     $scope.selectedHoldingsCopyEdit = function () { spawnHoldingsEdit(true,false) }
756
757     // this "transfers" selected copies to a new owning library,
758     // auto-creating volumes and deleting unused volumes as required.
759     $scope.changeItemOwningLib = function() {
760         var xfer_target = egCore.hatch.getLocalItem('eg.cat.volume_transfer_target');
761         var items = copyGrid.selectedItems();
762         if (!xfer_target || !items.length) {
763             return;
764         }
765         var vols_to_move   = {};
766         var copies_to_move = {};
767         angular.forEach(items, function(item) {
768             if (item['call_number.owning_lib'] != xfer_target) {
769                 if (item['call_number.id'] in vols_to_move) {
770                     copies_to_move[item['call_number.id']].push(item.id);
771                 } else {
772                     vols_to_move[item['call_number.id']] = {
773                         label       : item['call_number.label'],
774                         label_class : item['call_number.label_class'],
775                         record      : item['call_number.record.id'],
776                         prefix      : item['call_number.prefix.id'],
777                         suffix      : item['call_number.suffix.id']
778                     };
779                     copies_to_move[item['call_number.id']] = new Array;
780                     copies_to_move[item['call_number.id']].push(item.id);
781                 }
782             }
783         });
784
785         var promises = [];
786         angular.forEach(vols_to_move, function(vol) {
787             promises.push(egCore.net.request(
788                 'open-ils.cat',
789                 'open-ils.cat.call_number.find_or_create',
790                 egCore.auth.token(),
791                 vol.label,
792                 vol.record,
793                 xfer_target,
794                 vol.prefix,
795                 vol.suffix,
796                 vol.label_class
797             ).then(function(resp) {
798                 var evt = egCore.evt.parse(resp);
799                 if (evt) return;
800                 return egCore.net.request(
801                     'open-ils.cat',
802                     'open-ils.cat.transfer_copies_to_volume',
803                     egCore.auth.token(),
804                     resp.acn_id,
805                     copies_to_move[vol.id]
806                 );
807             }));
808         });
809
810         angular.forEach(
811             copyGrid.selectedItems(),
812             function(cp){
813                 promises.push(
814                     function(){ add_barcode_to_list(cp.barcode) }
815                 )
816             }
817         );
818         $q.all(promises);
819     }
820
821     $scope.transferItems = function (){
822         var xfer_target = egCore.hatch.getLocalItem('eg.cat.item_transfer_target');
823         var copy_ids = gatherSelectedHoldingsIds();
824         if (xfer_target && copy_ids.length > 0) {
825             egCore.net.request(
826                 'open-ils.cat',
827                 'open-ils.cat.transfer_copies_to_volume',
828                 egCore.auth.token(),
829                 xfer_target,
830                 copy_ids
831             ).then(
832                 function(resp) { // oncomplete
833                     var evt = egCore.evt.parse(resp);
834                     egConfirmDialog.open(
835                         egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_TITLE,
836                         egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_BODY,
837                         {'evt_desc': evt.desc}
838                     ).result.then(function() {
839                         egCore.net.request(
840                             'open-ils.cat',
841                             'open-ils.cat.transfer_copies_to_volume.override',
842                             egCore.auth.token(),
843                             xfer_target,
844                             copy_ids,
845                             { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
846                         );
847                     });
848                 },
849                 null, // onerror
850                 null // onprogress
851             ).then(function() {
852                     angular.forEach(copyGrid.selectedItems(), function(cp){add_barcode_to_list(cp.barcode)});
853             });
854         }
855     }
856
857     $scope.print_list = function() {
858         var print_data = { copies : copyGrid.allItems() };
859
860         if (print_data.copies.length == 0) return $q.when();
861
862         return egCore.print.print({
863             template : 'item_status',
864             scope : print_data
865         });
866     }
867
868     if (copyId.length > 0) {
869         itemSvc.fetch(null,copyId).then(
870             function() {
871                 copyGrid.refresh();
872             }
873         );
874     }
875
876 }])
877
878 /**
879  * Detail view -- shows one copy
880  */
881 .controller('ViewCtrl', 
882        ['$scope','$q','$location','$routeParams','$timeout','$window','egCore','itemSvc','egBilling',
883 function($scope , $q , $location , $routeParams , $timeout , $window , egCore , itemSvc , egBilling) {
884     var copyId = $routeParams.id;
885     $scope.tab = $routeParams.tab || 'summary';
886     $scope.context.page = 'detail';
887     $scope.summaryRecord = null;
888
889     $scope.edit = false;
890     if ($scope.tab == 'edit') {
891         $scope.tab = 'summary';
892         $scope.edit = true;
893     }
894
895
896     // use the cached record info
897     if (itemSvc.copy)
898         $scope.recordId = itemSvc.copy.call_number().record().id();
899
900     function loadCopy(barcode) {
901         $scope.context.itemNotFound = false;
902
903         // Avoid re-fetching the same copy while jumping tabs.
904         // In addition to being quicker, this helps to avoid flickering
905         // of the top panel which is always visible in the detail view.
906         //
907         // 'barcode' represents the loading of a new item - refetch it
908         // regardless of whether it matches the current item.
909         if (!barcode && itemSvc.copy && itemSvc.copy.id() == copyId) {
910             $scope.copy = itemSvc.copy;
911             $scope.recordId = itemSvc.copy.call_number().record().id();
912             return $q.when();
913         }
914
915         delete $scope.copy;
916         delete itemSvc.copy;
917
918         var deferred = $q.defer();
919         itemSvc.fetch(barcode, copyId, true).then(function(res) {
920             $scope.context.selectBarcode = true;
921
922             if (!res) {
923                 copyId = null;
924                 $scope.context.itemNotFound = true;
925                 deferred.reject(); // avoid propagation of data fetch calls
926                 return;
927             }
928
929             var copy = res.copy;
930             itemSvc.copy = copy;
931
932
933             $scope.copy = copy;
934             $scope.recordId = copy.call_number().record().id();
935             $scope.args.barcode = '';
936
937             // locally flesh org units
938             copy.circ_lib(egCore.org.get(copy.circ_lib()));
939             copy.call_number().owning_lib(
940                 egCore.org.get(copy.call_number().owning_lib()));
941
942             var r = copy.call_number().record();
943             if (r.owner()) r.owner(egCore.org.get(r.owner())); 
944
945             // make boolean for auto-magic true/false display
946             angular.forEach(
947                 ['ref','opac_visible','holdable','circulate'],
948                 function(field) { copy[field](Boolean(copy[field]() == 't')) }
949             );
950
951             // finally, if this is a different copy, redirect.
952             // Note that we flesh first since the copy we just
953             // fetched will be used after the redirect.
954             if (copyId && copyId != copy.id()) {
955                 // if a new barcode is scanned in the detail view,
956                 // update the url to match the ID of the new copy
957                 $location.path('/cat/item/' + copy.id() + '/' + $scope.tab);
958                 deferred.reject(); // avoid propagation of data fetch calls
959                 return;
960             }
961             copyId = copy.id();
962
963             deferred.resolve();
964         });
965
966         return deferred.promise;
967     }
968
969     // if loadPrev load the two most recent circulations
970     function loadCurrentCirc(loadPrev) {
971         delete $scope.circ;
972         delete $scope.circ_summary;
973         delete $scope.prev_circ_summary;
974         delete $scope.prev_circ_usr;
975         if (!copyId) return;
976         
977         egCore.pcrud.search('circ', 
978             {target_copy : copyId},
979             {   flesh : 2,
980                 flesh_fields : {
981                     circ : [
982                         'usr',
983                         'workstation',                                         
984                         'checkin_workstation',                                 
985                         'duration_rule',                                       
986                         'max_fine_rule',                                       
987                         'recurring_fine_rule'   
988                     ],
989                     au : ['card']
990                 },
991                 order_by : {circ : 'xact_start desc'}, 
992                 limit :  1
993             }
994
995         ).then(null, null, function(circ) {
996             $scope.circ = circ;
997
998             // load the chain for this circ
999             egCore.net.request(
1000                 'open-ils.circ',
1001                 'open-ils.circ.renewal_chain.retrieve_by_circ.summary',
1002                 egCore.auth.token(), $scope.circ.id()
1003             ).then(function(summary) {
1004                 $scope.circ_summary = summary.summary;
1005             });
1006
1007             if (!loadPrev) return;
1008
1009             // load the chain for the previous circ, plus the user
1010             egCore.net.request(
1011                 'open-ils.circ',
1012                 'open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary',
1013                 egCore.auth.token(), $scope.circ.id()
1014
1015             ).then(null, null, function(summary) {
1016                 $scope.prev_circ_summary = summary.summary;
1017
1018                 if (summary.usr) { // aged circs have no 'usr'.
1019                     egCore.pcrud.retrieve('au', summary.usr,
1020                         {flesh : 1, flesh_fields : {au : ['card']}})
1021
1022                     .then(function(user) { $scope.prev_circ_usr = user });
1023                 }
1024             });
1025         });
1026     }
1027
1028     var maxHistory;
1029     function fetchMaxCircHistory() {
1030         if (maxHistory) return $q.when(maxHistory);
1031         return egCore.org.settings(
1032             'circ.item_checkout_history.max')
1033         .then(function(set) {
1034             maxHistory = set['circ.item_checkout_history.max'] || 4;
1035             return maxHistory;
1036         });
1037     }
1038
1039     $scope.addBilling = function(circ) {
1040         egBilling.showBillDialog({
1041             xact_id : circ.id(),
1042             patron : circ.usr()
1043         });
1044     }
1045
1046     $scope.retrieveAllPatrons = function() {
1047         var users = new Set();
1048         angular.forEach($scope.circ_list.map(function(circ) { return circ.usr(); }),function(usr) {
1049             // aged circs have no 'usr'.
1050             if (usr) users.add(usr);
1051         });
1052         users.forEach(function(usr) {
1053             $timeout(function() {
1054                 var url = $location.absUrl().replace(
1055                     /\/cat\/.*/,
1056                     '/circ/patron/' + usr.id() + '/checkout');
1057                 $window.open(url, '_blank')
1058             });
1059         });
1060     }
1061
1062     function loadCircHistory() {
1063         $scope.circ_list = [];
1064
1065         var copy_org = 
1066             itemSvc.copy.call_number().id() == -1 ?
1067             itemSvc.copy.circ_lib().id() :
1068             itemSvc.copy.call_number().owning_lib().id()
1069
1070         // there is an extra layer of permissibility over circ
1071         // history views
1072         egCore.perm.hasPermAt('VIEW_COPY_CHECKOUT_HISTORY', true)
1073         .then(function(orgIds) {
1074
1075             if (orgIds.indexOf(copy_org) == -1) {
1076                 console.log('User is not allowed to view circ history');
1077                 return $q.when(0);
1078             }
1079
1080             return fetchMaxCircHistory();
1081
1082         }).then(function(count) {
1083
1084             egCore.pcrud.search('combcirc', 
1085                 {target_copy : copyId},
1086                 {   flesh : 2,
1087                     flesh_fields : {
1088                         combcirc : [
1089                             'usr',
1090                             'workstation',                                         
1091                             'checkin_workstation',                                 
1092                             'recurring_fine_rule'   
1093                         ],
1094                         au : ['card']
1095                     },
1096                     order_by : {combcirc : 'xact_start desc'}, 
1097                     limit :  count
1098                 }
1099
1100             ).then(null, null, function(circ) {
1101
1102                 // flesh circ_lib locally
1103                 circ.circ_lib(egCore.org.get(circ.circ_lib()));
1104                 circ.checkin_lib(egCore.org.get(circ.checkin_lib()));
1105                 $scope.circ_list.push(circ);
1106             });
1107         });
1108     }
1109
1110
1111     function loadCircCounts() {
1112
1113         delete $scope.circ_counts;
1114         $scope.total_circs = 0;
1115         $scope.total_circs_this_year = 0;
1116         $scope.total_circs_prev_year = 0;
1117         if (!copyId) return;
1118
1119         egCore.pcrud.search('circbyyr', 
1120             {copy : copyId}, null, {atomic : true})
1121
1122         .then(function(counts) {
1123             $scope.circ_counts = counts;
1124
1125             angular.forEach(counts, function(count) {
1126                 $scope.total_circs += Number(count.count());
1127             });
1128
1129             var this_year = counts.filter(function(c) {
1130                 return c.year() == new Date().getFullYear();
1131             });
1132
1133             $scope.total_circs_this_year = 
1134                 this_year.length ? this_year[0].count() : 0;
1135
1136             var prev_year = counts.filter(function(c) {
1137                 return c.year() == new Date().getFullYear() - 1;
1138             });
1139
1140             $scope.total_circs_prev_year = 
1141                 prev_year.length ? prev_year[0].count() : 0;
1142
1143         });
1144     }
1145
1146     function loadHolds() {
1147         delete $scope.hold;
1148         if (!copyId) return;
1149
1150         egCore.pcrud.search('ahr', 
1151             {   current_copy : copyId, 
1152                 cancel_time : null, 
1153                 fulfillment_time : null,
1154                 capture_time : {'<>' : null}
1155             }, {
1156                 flesh : 2,
1157                 flesh_fields : {
1158                     ahr : ['requestor', 'usr'],
1159                     au  : ['card']
1160                 }
1161             }
1162         ).then(null, null, function(hold) {
1163             $scope.hold = hold;
1164             hold.pickup_lib(egCore.org.get(hold.pickup_lib()));
1165             if (hold.current_shelf_lib()) {
1166                 hold.current_shelf_lib(
1167                     egCore.org.get(hold.current_shelf_lib()));
1168             }
1169             hold.behind_desk(Boolean(hold.behind_desk() == 't'));
1170         });
1171     }
1172
1173     function loadTransits() {
1174         delete $scope.transit;
1175         delete $scope.hold_transit;
1176         if (!copyId) return;
1177
1178         egCore.pcrud.search('atc', 
1179             {target_copy : copyId},
1180             {order_by : {atc : 'source_send_time DESC'}}
1181
1182         ).then(null, null, function(transit) {
1183             $scope.transit = transit;
1184             transit.source(egCore.org.get(transit.source()));
1185             transit.dest(egCore.org.get(transit.dest()));
1186         })
1187     }
1188
1189
1190     // we don't need all data on all tabs, so fetch what's needed when needed.
1191     function loadTabData() {
1192         switch($scope.tab) {
1193             case 'summary':
1194                 loadCurrentCirc();
1195                 loadCircCounts();
1196                 break;
1197
1198             case 'circs':
1199                 loadCurrentCirc(true);
1200                 break;
1201
1202             case 'circ_list':
1203                 loadCircHistory();
1204                 break;
1205
1206             case 'holds':
1207                 loadHolds()
1208                 loadTransits();
1209                 break;
1210
1211             case 'triggered_events':
1212                 var url = $location.absUrl().replace(/\/staff.*/, '/actor/user/event_log');
1213                 url += '?copy_id=' + encodeURIComponent(copyId);
1214                 $scope.triggered_events_url = url;
1215                 $scope.funcs = {};
1216         }
1217
1218         if ($scope.edit) {
1219             egCore.net.request(
1220                 'open-ils.actor',
1221                 'open-ils.actor.anon_cache.set_value',
1222                 null, 'edit-these-copies', {
1223                     record_id: $scope.recordId,
1224                     copies: [copyId],
1225                     hide_vols : true,
1226                     hide_copies : false
1227                 }
1228             ).then(function(key) {
1229                 if (key) {
1230                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
1231                     $window.location.href = url;
1232                 } else {
1233                     alert('Could not create anonymous cache key!');
1234                 }
1235             });
1236         }
1237
1238         return;
1239     }
1240
1241     $scope.context.toggleDisplay = function() {
1242         $location.path('/cat/item/search');
1243     }
1244
1245     // handle the barcode scan box, which will replace our current copy
1246     $scope.context.search = function(args) {
1247         loadCopy(args.barcode).then(loadTabData);
1248     }
1249
1250     $scope.context.show_triggered_events = function() {
1251         $location.path('/cat/item/' + copyId + '/triggered_events');
1252     }
1253
1254     loadCopy().then(loadTabData);
1255 }])