]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/cat/item/app.js
LP#1705524: Honor timezone of the acting library where appropriate
[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','egCirc','$uibModal','$q','$timeout','$window','egConfirmDialog',
52 function(egCore , egCirc , $uibModal , $q , $timeout , $window , egConfirmDialog ) {
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',
63                 'age_protect','circ_lib'],
64             acn : ['record','prefix','suffix','label_class'],
65             bre : ['simple_record','creator','editor']
66         },
67         select : { 
68             // avoid fleshing MARC on the bre
69             // note: don't add simple_record.. not sure why
70             bre : ['id','tcn_value','creator','editor'],
71         } 
72     }
73
74     service.circFlesh = {
75         flesh : 2,
76         flesh_fields : {
77             circ : [
78                 'usr',
79                 'workstation',
80                 'checkin_workstation',
81                 'checkin_lib',
82                 'duration_rule',
83                 'max_fine_rule',
84                 'recurring_fine_rule'
85             ],
86             au : ['card']
87         },
88         order_by : {circ : 'xact_start desc'},
89         limit :  1
90     }
91
92     //Retrieve separate copy, aacs, and accs information
93     service.getCopy = function(barcode, id) {
94         if (barcode) return egCore.pcrud.search(
95             'acp', {barcode : barcode, deleted : 'f'},
96             service.flesh).then(function(copy) {return copy});
97
98         return egCore.pcrud.retrieve( 'acp', id, service.flesh)
99             .then(function(copy) {return copy});
100     }
101     service.getCirc = function(id) {
102         return egCore.pcrud.search('aacs', { target_copy : id },
103             service.circFlesh).then(function(circ) {return circ});
104     }
105     service.getSummary = function(id) {
106         return circ_summary = egCore.net.request(
107             'open-ils.circ',
108             'open-ils.circ.renewal_chain.retrieve_by_circ.summary',
109             egCore.auth.token(), id).then(
110                 function(circ_summary) {return circ_summary});
111     }
112
113     //Combine copy, circ, and accs information
114     service.retrieveCopyData = function(barcode, id) {
115         var copyData = {};
116
117         var fetchCopy = function(barcode, id) {
118             return service.getCopy(barcode, id)
119                 .then(function(copy) {
120                     copyData.copy = copy;
121                     return copyData;
122                 });
123         }
124         var fetchCirc = function(copy) {
125             return service.getCirc(copy.id())
126                 .then(function(circ) {
127                     copyData.circ = circ;
128                     return copyData;
129                 });
130         }
131         var fetchSummary = function(circ) {
132             return service.getSummary(circ.id())
133                 .then(function(summary) {
134                     copyData.circ_summary = summary;
135                     return copyData;
136                 });
137         }
138         return fetchCopy(barcode, id).then(function(res) {
139             return fetchCirc(copyData.copy).then(function(res) {
140                 if (copyData.circ) {
141                     return fetchSummary(copyData.circ).then(function() {
142                         return copyData;
143                     });
144                 } else {
145                     return copyData;
146                 }
147             });
148         });
149
150     }
151
152     // resolved with the last received copy
153     service.fetch = function(barcode, id, noListDupes) {
154         var copy;
155         var circ;
156         var circ_summary;
157         var lastRes = {};
158
159         return service.retrieveCopyData(barcode, id)
160         .then(function(copyData) {
161             //Make sure we're getting a completed copyData - no plain acp or circ objects
162             if (copyData.circ) {
163                 // flesh circ_lib locally
164                 copyData.circ.circ_lib(egCore.org.get(copyData.circ.circ_lib()));
165                 copyData.circ.checkin_workstation(
166                     egCore.org.get(copyData.circ.checkin_workstation()));
167             }
168             var flatCopy;
169
170             if (noListDupes) {
171                 // use the existing copy if possible
172                 flatCopy = service.copies.filter(
173                     function(c) {return c.id == copyData.copy.id()})[0];
174             }
175
176             if (!flatCopy) {
177                 flatCopy = egCore.idl.toHash(copyData.copy, true);
178
179                 if (copyData.circ) {
180                     flatCopy._circ = egCore.idl.toHash(copyData.circ, true);
181                     flatCopy._circ_summary = egCore.idl.toHash(copyData.circ_summary, true);
182                     flatCopy._circ_lib = copyData.circ.circ_lib();
183                     flatCopy._duration = copyData.circ.duration();
184                 }
185                 flatCopy.index = service.index++;
186                 service.copies.unshift(flatCopy);
187             }
188
189             //Get in-house use count
190             egCore.pcrud.search('aihu',
191                 {item : flatCopy.id}, {}, {idlist : true, atomic : true})
192             .then(function(uses) {
193                 flatCopy._inHouseUseCount = uses.length;
194                 copyData.copy._inHouseUseCount = uses.length;
195             });
196
197             return lastRes = {
198                 copy : copyData.copy,
199                 index : flatCopy.index
200             }
201         });
202
203
204     }
205
206     service.add_copies_to_bucket = function(copy_list) {
207         if (copy_list.length == 0) return;
208
209         return $uibModal.open({
210             templateUrl: './cat/catalog/t_add_to_bucket',
211             animation: true,
212             size: 'md',
213             controller:
214                    ['$scope','$uibModalInstance',
215             function($scope , $uibModalInstance) {
216
217                 $scope.bucket_id = 0;
218                 $scope.newBucketName = '';
219                 $scope.allBuckets = [];
220
221                 egCore.net.request(
222                     'open-ils.actor',
223                     'open-ils.actor.container.retrieve_by_class.authoritative',
224                     egCore.auth.token(), egCore.auth.user().id(),
225                     'copy', 'staff_client'
226                 ).then(function(buckets) { $scope.allBuckets = buckets; });
227
228                 $scope.add_to_bucket = function() {
229                     var promises = [];
230                     angular.forEach(copy_list, function (cp) {
231                         var item = new egCore.idl.ccbi()
232                         item.bucket($scope.bucket_id);
233                         item.target_copy(cp);
234                         promises.push(
235                             egCore.net.request(
236                                 'open-ils.actor',
237                                 'open-ils.actor.container.item.create',
238                                 egCore.auth.token(), 'copy', item
239                             )
240                         );
241
242                         return $q.all(promises).then(function() {
243                             $uibModalInstance.close();
244                         });
245                     });
246                 }
247
248                 $scope.add_to_new_bucket = function() {
249                     var bucket = new egCore.idl.ccb();
250                     bucket.owner(egCore.auth.user().id());
251                     bucket.name($scope.newBucketName);
252                     bucket.description('');
253                     bucket.btype('staff_client');
254
255                     return egCore.net.request(
256                         'open-ils.actor',
257                         'open-ils.actor.container.create',
258                         egCore.auth.token(), 'copy', bucket
259                     ).then(function(bucket) {
260                         $scope.bucket_id = bucket;
261                         $scope.add_to_bucket();
262                     });
263                 }
264
265                 $scope.cancel = function() {
266                     $uibModalInstance.dismiss();
267                 }
268             }]
269         });
270     }
271
272     service.make_copies_bookable = function(items) {
273
274         var copies_by_record = {};
275         var record_list = [];
276         angular.forEach(
277             items,
278             function (item) {
279                 var record_id = item['call_number.record.id'];
280                 if (typeof copies_by_record[ record_id ] == 'undefined') {
281                     copies_by_record[ record_id ] = [];
282                     record_list.push( record_id );
283                 }
284                 copies_by_record[ record_id ].push(item.id);
285             }
286         );
287
288         var promises = [];
289         var combined_results = [];
290         angular.forEach(record_list, function(record_id) {
291             promises.push(
292                 egCore.net.request(
293                     'open-ils.booking',
294                     'open-ils.booking.resources.create_from_copies',
295                     egCore.auth.token(),
296                     copies_by_record[record_id]
297                 ).then(function(results) {
298                     if (results && results['brsrc']) {
299                         combined_results = combined_results.concat(results['brsrc']);
300                     }
301                 })
302             );
303         });
304
305         $q.all(promises).then(function() {
306             if (combined_results.length > 0) {
307                 $uibModal.open({
308                     template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
309                     animation: true,
310                     size: 'md',
311                     controller:
312                            ['$scope','$location','egCore','$uibModalInstance',
313                     function($scope , $location , egCore , $uibModalInstance) {
314
315                         $scope.funcs = {
316                             ses : egCore.auth.token(),
317                             resultant_brsrc : combined_results.map(function(o) { return o[0]; })
318                         }
319
320                         var booking_path = '/eg/conify/global/booking/resource';
321
322                         $scope.booking_admin_url =
323                             $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
324                     }]
325                 });
326             }
327         });
328     }
329
330     service.book_copies_now = function(items) {
331         var copies_by_record = {};
332         var record_list = [];
333         angular.forEach(
334             items,
335             function (item) {
336                 var record_id = item['call_number.record.id'];
337                 if (typeof copies_by_record[ record_id ] == 'undefined') {
338                     copies_by_record[ record_id ] = [];
339                     record_list.push( record_id );
340                 }
341                 copies_by_record[ record_id ].push(item.id);
342             }
343         );
344
345         var promises = [];
346         var combined_brt = [];
347         var combined_brsrc = [];
348         angular.forEach(record_list, function(record_id) {
349             promises.push(
350                 egCore.net.request(
351                     'open-ils.booking',
352                     'open-ils.booking.resources.create_from_copies',
353                     egCore.auth.token(),
354                     copies_by_record[record_id]
355                 ).then(function(results) {
356                     if (results && results['brt']) {
357                         combined_brt = combined_brt.concat(results['brt']);
358                     }
359                     if (results && results['brsrc']) {
360                         combined_brsrc = combined_brsrc.concat(results['brsrc']);
361                     }
362                 })
363             );
364         });
365
366         $q.all(promises).then(function() {
367             if (combined_brt.length > 0 || combined_brsrc.length > 0) {
368                 $uibModal.open({
369                     template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
370                     animation: true,
371                     size: 'md',
372                     controller:
373                            ['$scope','$location','egCore','$uibModalInstance',
374                     function($scope , $location , egCore , $uibModalInstance) {
375
376                         $scope.funcs = {
377                             ses : egCore.auth.token(),
378                             bresv_interface_opts : {
379                                 booking_results : {
380                                      brt : combined_brt
381                                     ,brsrc : combined_brsrc
382                                 }
383                             }
384                         }
385
386                         var booking_path = '/eg/booking/reservation';
387
388                         $scope.booking_admin_url =
389                             $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
390
391                     }]
392                 });
393             }
394         });
395     }
396
397     service.requestItems = function(copy_list) {
398         if (copy_list.length == 0) return;
399
400         return $uibModal.open({
401             templateUrl: './cat/catalog/t_request_items',
402             animation: true,
403             controller:
404                    ['$scope','$uibModalInstance','egUser',
405             function($scope , $uibModalInstance , egUser) {
406                 $scope.user = null;
407                 $scope.first_user_fetch = true;
408
409                 $scope.hold_data = {
410                     hold_type : 'C',
411                     copy_list : copy_list,
412                     pickup_lib: egCore.org.get(egCore.auth.user().ws_ou()),
413                     user      : egCore.auth.user().id()
414                 };
415
416                 egUser.get( $scope.hold_data.user ).then(function(u) {
417                     $scope.user = u;
418                     $scope.barcode = u.card().barcode();
419                     $scope.user_name = egUser.format_name(u);
420                     $scope.hold_data.user = u.id();
421                 });
422
423                 $scope.user_name = '';
424                 $scope.barcode = '';
425                 $scope.$watch('barcode', function (n) {
426                     if (!$scope.first_user_fetch) {
427                         egUser.getByBarcode(n).then(function(u) {
428                             $scope.user = u;
429                             $scope.user_name = egUser.format_name(u);
430                             $scope.hold_data.user = u.id();
431                         }, function() {
432                             $scope.user = null;
433                             $scope.user_name = '';
434                             delete $scope.hold_data.user;
435                         });
436                     }
437                     $scope.first_user_fetch = false;
438                 });
439
440                 $scope.ok = function(h) {
441                     var args = {
442                         patronid  : h.user,
443                         hold_type : h.hold_type,
444                         pickup_lib: h.pickup_lib.id(),
445                         depth     : 0
446                     };
447
448                     egCore.net.request(
449                         'open-ils.circ',
450                         'open-ils.circ.holds.test_and_create.batch.override',
451                         egCore.auth.token(), args, h.copy_list
452                     );
453
454                     $uibModalInstance.close();
455                 }
456
457                 $scope.cancel = function($event) {
458                     $uibModalInstance.dismiss();
459                     $event.preventDefault();
460                 }
461             }]
462         });
463     }
464
465     service.attach_to_peer_bib = function(items) {
466         if (items.length == 0) return;
467
468         egCore.hatch.getItem('eg.cat.marked_conjoined_record').then(function(target_record) {
469             if (!target_record) return;
470
471             return $uibModal.open({
472                 templateUrl: './cat/catalog/t_conjoined_selector',
473                 animation: true,
474                 controller:
475                        ['$scope','$uibModalInstance',
476                 function($scope , $uibModalInstance) {
477                     $scope.update = false;
478
479                     $scope.peer_type = null;
480                     $scope.peer_type_list = [];
481
482                     get_peer_types = function() {
483                         if (egCore.env.bpt)
484                             return $q.when(egCore.env.bpt.list);
485
486                         return egCore.pcrud.retrieveAll('bpt', null, {atomic : true})
487                         .then(function(list) {
488                             egCore.env.absorbList(list, 'bpt');
489                             return list;
490                         });
491                     }
492
493                     get_peer_types().then(function(list){
494                         $scope.peer_type_list = list;
495                     });
496
497                     $scope.ok = function(type) {
498                         var promises = [];
499
500                         angular.forEach(items, function (cp) {
501                             var n = new egCore.idl.bpbcm();
502                             n.isnew(true);
503                             n.peer_record(target_record);
504                             n.target_copy(cp.id);
505                             n.peer_type(type);
506                             promises.push(egCore.pcrud.create(n).then(function(){service.add_barcode_to_list(cp.barcode)}));
507                         });
508
509                         return $q.all(promises).then(function(){$uibModalInstance.close()});
510                     }
511
512                     $scope.cancel = function($event) {
513                         $uibModalInstance.dismiss();
514                         $event.preventDefault();
515                     }
516                 }]
517             });
518         });
519     }
520
521     service.selectedHoldingsCopyDelete = function (items) {
522         if (items.length == 0) return;
523
524         var copy_objects = [];
525         egCore.pcrud.search('acp',
526             {deleted : 'f', id : items.map(function(el){return el.id;}) },
527             { flesh : 1, flesh_fields : { acp : ['call_number'] } }
528         ).then(function(copy) {
529             copy_objects.push(copy);
530         }).then(function() {
531
532             var cnHash = {};
533             var perCnCopies = {};
534
535             var cn_count = 0;
536             var cp_count = 0;
537
538             angular.forEach(
539                 copy_objects,
540                 function (cp) {
541                     cp.isdeleted(1);
542                     cp_count++;
543                     var cn_id = cp.call_number().id();
544                     if (!cnHash[cn_id]) {
545                         cnHash[cn_id] = cp.call_number();
546                         perCnCopies[cn_id] = [cp];
547                     } else {
548                         perCnCopies[cn_id].push(cp);
549                     }
550                     cp.call_number(cn_id); // prevent loops in JSON-ification
551                 }
552             );
553
554             angular.forEach(perCnCopies, function (v, k) {
555                 cnHash[k].copies(v);
556             });
557
558             cnList = [];
559             angular.forEach(cnHash, function (v, k) {
560                 cnList.push(v);
561             });
562
563             if (cnList.length == 0) return;
564
565             var flags = {};
566
567             egConfirmDialog.open(
568                 egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES,
569                 egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES_MESSAGE,
570                 {copies : cp_count, volumes : cn_count}
571             ).result.then(function() {
572                 egCore.net.request(
573                     'open-ils.cat',
574                     'open-ils.cat.asset.volume.fleshed.batch.update.override',
575                     egCore.auth.token(), cnList, 1, flags
576                 ).then(function(){
577                     angular.forEach(items, function(cp){service.add_barcode_to_list(cp.barcode)});
578                 });
579             });
580         });
581     }
582
583     service.checkin = function (items) {
584         angular.forEach(items, function (cp) {
585             egCirc.checkin({copy_barcode:cp.barcode}).then(
586                 function() { service.add_barcode_to_list(cp.barcode) }
587             );
588         });
589     }
590
591     service.renew = function (items) {
592         angular.forEach(items, function (cp) {
593             egCirc.renew({copy_barcode:cp.barcode}).then(
594                 function() { service.add_barcode_to_list(cp.barcode) }
595             );
596         });
597     }
598
599     service.cancel_transit = function (items) {
600         angular.forEach(items, function(cp) {
601             egCirc.find_copy_transit(null, {copy_barcode:cp.barcode})
602                 .then(function(t) { return egCirc.abort_transit(t.id())    })
603                 .then(function()  { return service.add_barcode_to_list(cp.barcode) });
604         });
605     }
606
607     service.selectedHoldingsDamaged = function (items) {
608         egCirc.mark_damaged(items.map(function(el){return el.id;})).then(function(){
609             angular.forEach(items, function(cp){service.add_barcode_to_list(cp.barcode)});
610         });
611     }
612
613     service.selectedHoldingsMissing = function (items) {
614         egCirc.mark_missing(items.map(function(el){return el.id;})).then(function(){
615             angular.forEach(items, function(cp){service.add_barcode_to_list(cp.barcode)});
616         });
617     }
618
619     service.gatherSelectedRecordIds = function (items) {
620         var rid_list = [];
621         angular.forEach(
622             items,
623             function (item) {
624                 if (rid_list.indexOf(item['call_number.record.id']) == -1)
625                     rid_list.push(item['call_number.record.id'])
626             }
627         );
628         return rid_list;
629     }
630
631     service.gatherSelectedVolumeIds = function (items,rid) {
632         var cn_id_list = [];
633         angular.forEach(
634             items,
635             function (item) {
636                 if (rid && item['call_number.record.id'] != rid) return;
637                 if (cn_id_list.indexOf(item['call_number.id']) == -1)
638                     cn_id_list.push(item['call_number.id'])
639             }
640         );
641         return cn_id_list;
642     }
643
644     service.gatherSelectedHoldingsIds = function (items,rid) {
645         var cp_id_list = [];
646         angular.forEach(
647             items,
648             function (item) {
649                 if (rid && item['call_number.record.id'] != rid) return;
650                 cp_id_list.push(item.id)
651             }
652         );
653         return cp_id_list;
654     }
655
656     service.spawnHoldingsAdd = function (items,use_vols,use_copies){
657         angular.forEach(service.gatherSelectedRecordIds(items), function (r) {
658             var raw = [];
659             if (use_copies) { // just a copy on existing volumes
660                 angular.forEach(service.gatherSelectedVolumeIds(items,r), function (v) {
661                     raw.push( {callnumber : v} );
662                 });
663             } else if (use_vols) {
664                 angular.forEach(
665                     service.gatherSelectedHoldingsIds(items,r),
666                     function (i) {
667                         angular.forEach(items, function(item) {
668                             if (i == item.id) raw.push({owner : item['call_number.owning_lib']});
669                         });
670                     }
671                 );
672             }
673
674             if (raw.length == 0) raw.push({});
675
676             egCore.net.request(
677                 'open-ils.actor',
678                 'open-ils.actor.anon_cache.set_value',
679                 null, 'edit-these-copies', {
680                     record_id: r,
681                     raw: raw,
682                     hide_vols : false,
683                     hide_copies : false
684                 }
685             ).then(function(key) {
686                 if (key) {
687                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
688                     $timeout(function() { $window.open(url, '_blank') });
689                 } else {
690                     alert('Could not create anonymous cache key!');
691                 }
692             });
693         });
694     }
695
696     service.spawnHoldingsEdit = function (items,hide_vols,hide_copies){
697         angular.forEach(service.gatherSelectedRecordIds(items), function (r) {
698             egCore.net.request(
699                 'open-ils.actor',
700                 'open-ils.actor.anon_cache.set_value',
701                 null, 'edit-these-copies', {
702                     record_id: r,
703                     copies: service.gatherSelectedHoldingsIds(items,r),
704                     raw: {},
705                     hide_vols : hide_vols,
706                     hide_copies : hide_copies
707                 }
708             ).then(function(key) {
709                 if (key) {
710                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
711                     $timeout(function() { $window.open(url, '_blank') });
712                 } else {
713                     alert('Could not create anonymous cache key!');
714                 }
715             });
716         });
717     }
718
719     service.replaceBarcodes = function(items) {
720         angular.forEach(items, function (cp) {
721             $uibModal.open({
722                 templateUrl: './cat/share/t_replace_barcode',
723                 animation: true,
724                 controller:
725                            ['$scope','$uibModalInstance',
726                     function($scope , $uibModalInstance) {
727                         $scope.isModal = true;
728                         $scope.focusBarcode = false;
729                         $scope.focusBarcode2 = true;
730                         $scope.barcode1 = cp.barcode;
731
732                         $scope.updateBarcode = function() {
733                             $scope.copyNotFound = false;
734                             $scope.updateOK = false;
735
736                             egCore.pcrud.search('acp',
737                                 {deleted : 'f', barcode : $scope.barcode1})
738                             .then(function(copy) {
739
740                                 if (!copy) {
741                                     $scope.focusBarcode = true;
742                                     $scope.copyNotFound = true;
743                                     return;
744                                 }
745
746                                 $scope.copyId = copy.id();
747                                 copy.barcode($scope.barcode2);
748
749                                 egCore.pcrud.update(copy).then(function(stat) {
750                                     $scope.updateOK = stat;
751                                     $scope.focusBarcode = true;
752                                     if (stat) service.add_barcode_to_list(copy.barcode());
753                                 });
754
755                             });
756                             $uibModalInstance.close();
757                         }
758
759                         $scope.cancel = function($event) {
760                             $uibModalInstance.dismiss();
761                             $event.preventDefault();
762                         }
763                     }
764                 ]
765             });
766         });
767     }
768
769     // this "transfers" selected copies to a new owning library,
770     // auto-creating volumes and deleting unused volumes as required.
771     service.changeItemOwningLib = function(items) {
772         var xfer_target = egCore.hatch.getLocalItem('eg.cat.volume_transfer_target');
773         if (!xfer_target || !items.length) {
774             return;
775         }
776         var vols_to_move   = {};
777         var copies_to_move = {};
778         angular.forEach(items, function(item) {
779             if (item['call_number.owning_lib'] != xfer_target) {
780                 if (item['call_number.id'] in vols_to_move) {
781                     copies_to_move[item['call_number.id']].push(item.id);
782                 } else {
783                     vols_to_move[item['call_number.id']] = {
784                         label       : item['call_number.label'],
785                         label_class : item['call_number.label_class'],
786                         record      : item['call_number.record.id'],
787                         prefix      : item['call_number.prefix.id'],
788                         suffix      : item['call_number.suffix.id']
789                     };
790                     copies_to_move[item['call_number.id']] = new Array;
791                     copies_to_move[item['call_number.id']].push(item.id);
792                 }
793             }
794         });
795
796         var promises = [];
797         angular.forEach(vols_to_move, function(vol) {
798             promises.push(egCore.net.request(
799                 'open-ils.cat',
800                 'open-ils.cat.call_number.find_or_create',
801                 egCore.auth.token(),
802                 vol.label,
803                 vol.record,
804                 xfer_target,
805                 vol.prefix,
806                 vol.suffix,
807                 vol.label_class
808             ).then(function(resp) {
809                 var evt = egCore.evt.parse(resp);
810                 if (evt) return;
811                 return egCore.net.request(
812                     'open-ils.cat',
813                     'open-ils.cat.transfer_copies_to_volume',
814                     egCore.auth.token(),
815                     resp.acn_id,
816                     copies_to_move[vol.id]
817                 );
818             }));
819         });
820
821         angular.forEach(
822             items,
823             function(cp){
824                 promises.push(
825                     function(){ service.add_barcode_to_list(cp.barcode) }
826                 )
827             }
828         );
829         $q.all(promises);
830     }
831
832     service.transferItems = function (items){
833         var xfer_target = egCore.hatch.getLocalItem('eg.cat.item_transfer_target');
834         var copy_ids = service.gatherSelectedHoldingsIds(items);
835         if (xfer_target && copy_ids.length > 0) {
836             egCore.net.request(
837                 'open-ils.cat',
838                 'open-ils.cat.transfer_copies_to_volume',
839                 egCore.auth.token(),
840                 xfer_target,
841                 copy_ids
842             ).then(
843                 function(resp) { // oncomplete
844                     var evt = egCore.evt.parse(resp);
845                     if (evt) {
846                         egConfirmDialog.open(
847                             egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_TITLE,
848                             egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_BODY,
849                             {'evt_desc': evt}
850                         ).result.then(function() {
851                             egCore.net.request(
852                                 'open-ils.cat',
853                                 'open-ils.cat.transfer_copies_to_volume.override',
854                                 egCore.auth.token(),
855                                 xfer_target,
856                                 copy_ids,
857                                 { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
858                             );
859                         }).then(function() {
860                             angular.forEach(items, function(cp){service.add_barcode_to_list(cp.barcode)});
861                         });
862                     } else {
863                         angular.forEach(items, function(cp){service.add_barcode_to_list(cp.barcode)});
864                     }
865
866                 },
867                 null, // onerror
868                 null // onprogress
869             );
870         }
871     }
872
873     return service;
874 }])
875
876 /**
877  * Search bar along the top of the page.
878  * Parent scope for list and detail views
879  */
880 .controller('SearchCtrl', 
881        ['$scope','$location','$timeout','egCore','egGridDataProvider','itemSvc',
882 function($scope , $location , $timeout , egCore , egGridDataProvider , itemSvc) {
883     $scope.args = {}; // search args
884
885     // sub-scopes (search / detail-view) apply their version 
886     // of retrieval function to $scope.context.search
887     // and display toggling via $scope.context.toggleDisplay
888     $scope.context = {
889         selectBarcode : true
890     };
891
892     $scope.toggleView = function($event) {
893         $scope.context.toggleDisplay();
894         $event.preventDefault(); // avoid form submission
895     }
896
897     // The functions that follow in this controller are never called
898     // when the List View is active, only the Detail View.
899     
900     // In this context, we're only ever dealing with 1 item, so
901     // we can simply refresh the page.  These various itemSvc
902     // functions used to live in the ListCtrl, but they're now
903     // shared between SearchCtrl (for Actions for the Detail View)
904     // and ListCtrl (Actions in the egGrid)
905     itemSvc.add_barcode_to_list = function(b) {
906         //console.log('SearchCtrl: add_barcode_to_list',b);
907         // timeout so audible can happen upon checkin
908         $timeout(function() { location.href = location.href; }, 1000);
909     }
910
911     $scope.add_copies_to_bucket = function() {
912         itemSvc.add_copies_to_bucket([$scope.args.copyId]);
913     }
914
915     $scope.make_copies_bookable = function() {
916         itemSvc.make_copies_bookable([{
917             id : $scope.args.copyId,
918             'call_number.record.id' : $scope.args.recordId
919         }]);
920     }
921
922     $scope.book_copies_now = function() {
923         itemSvc.book_copies_now([{
924             id : $scope.args.copyId,
925             'call_number.record.id' : $scope.args.recordId
926         }]);
927     }
928
929     $scope.requestItems = function() {
930         itemSvc.requestItems([$scope.args.copyId]);
931     }
932
933     $scope.attach_to_peer_bib = function() {
934         itemSvc.attach_to_peer_bib([{
935             id : $scope.args.copyId,
936             barcode : $scope.args.copyBarcode
937         }]);
938     }
939
940     $scope.selectedHoldingsCopyDelete = function () {
941         itemSvc.selectedHoldingsCopyDelete([{
942             id : $scope.args.copyId,
943             barcode : $scope.args.copyBarcode
944         }]);
945     }
946
947     $scope.checkin = function () {
948         itemSvc.checkin([{
949             id : $scope.args.copyId,
950             barcode : $scope.args.copyBarcode
951         }]);
952     }
953
954     $scope.renew = function () {
955         itemSvc.renew([{
956             id : $scope.args.copyId,
957             barcode : $scope.args.copyBarcode
958         }]);
959     }
960
961     $scope.cancel_transit = function () {
962         itemSvc.cancel_transit([{
963             id : $scope.args.copyId,
964             barcode : $scope.args.copyBarcode
965         }]);
966     }
967
968     $scope.selectedHoldingsDamaged = function () {
969         itemSvc.selectedHoldingsDamaged([{
970             id : $scope.args.copyId,
971             barcode : $scope.args.copyBarcode
972         }]);
973     }
974
975     $scope.selectedHoldingsMissing = function () {
976         itemSvc.selectedHoldingsMissing([{
977             id : $scope.args.copyId,
978             barcode : $scope.args.copyBarcode
979         }]);
980     }
981
982     $scope.selectedHoldingsVolCopyAdd = function () {
983         itemSvc.spawnHoldingsAdd([{
984             id : $scope.args.copyId,
985             'call_number.owning_lib' : $scope.args.cnOwningLib,
986             'call_number.record.id' : $scope.args.recordId,
987             barcode : $scope.args.copyBarcode
988         }],true,false);
989     }
990     $scope.selectedHoldingsCopyAdd = function () {
991         itemSvc.spawnHoldingsAdd([{
992             id : $scope.args.copyId,
993             'call_number.id' : $scope.args.cnId,
994             'call_number.owning_lib' : $scope.args.cnOwningLib,
995             'call_number.record.id' : $scope.args.recordId,
996             barcode : $scope.args.copyBarcode
997         }],false,true);
998     }
999
1000     $scope.selectedHoldingsVolCopyEdit = function () {
1001         itemSvc.spawnHoldingsEdit([{
1002             id : $scope.args.copyId,
1003             'call_number.id' : $scope.args.cnId,
1004             'call_number.owning_lib' : $scope.args.cnOwningLib,
1005             'call_number.record.id' : $scope.args.recordId,
1006             barcode : $scope.args.copyBarcode
1007         }],false,false);
1008     }
1009     $scope.selectedHoldingsVolEdit = function () {
1010         itemSvc.spawnHoldingsEdit([{
1011             id : $scope.args.copyId,
1012             'call_number.id' : $scope.args.cnId,
1013             'call_number.owning_lib' : $scope.args.cnOwningLib,
1014             'call_number.record.id' : $scope.args.recordId,
1015             barcode : $scope.args.copyBarcode
1016         }],false,true);
1017     }
1018     $scope.selectedHoldingsCopyEdit = function () {
1019         itemSvc.spawnHoldingsEdit([{
1020             id : $scope.args.copyId,
1021             'call_number.id' : $scope.args.cnId,
1022             'call_number.owning_lib' : $scope.args.cnOwningLib,
1023             'call_number.record.id' : $scope.args.recordId,
1024             barcode : $scope.args.copyBarcode
1025         }],true,false);
1026     }
1027
1028     $scope.replaceBarcodes = function() {
1029         itemSvc.replaceBarcodes([{
1030             id : $scope.args.copyId,
1031             barcode : $scope.args.copyBarcode
1032         }]);
1033     }
1034
1035     $scope.changeItemOwningLib = function() {
1036         itemSvc.changeItemOwningLib([{
1037             id : $scope.args.copyId,
1038             'call_number.id' : $scope.args.cnId,
1039             'call_number.owning_lib' : $scope.args.cnOwningLib,
1040             'call_number.record.id' : $scope.args.recordId,
1041             'call_number.label' : $scope.args.cnLabel,
1042             'call_number.label_class' : $scope.args.cnLabelClass,
1043             'call_number.prefix.id' : $scope.args.cnPrefixId,
1044             'call_number.suffix.id' : $scope.args.cnSuffixId,
1045             barcode : $scope.args.copyBarcode
1046         }]);
1047     }
1048
1049     $scope.transferItems = function (){
1050         itemSvc.transferItems([{
1051             id : $scope.args.copyId,
1052             barcode : $scope.args.copyBarcode
1053         }]);
1054     }
1055
1056 }])
1057
1058 /**
1059  * List view - grid stuff
1060  */
1061 .controller('ListCtrl', 
1062        ['$scope','$q','$routeParams','$location','$timeout','$window','egCore','egGridDataProvider','itemSvc','egUser','$uibModal','egCirc','egConfirmDialog',
1063 function($scope , $q , $routeParams , $location , $timeout , $window , egCore , egGridDataProvider , itemSvc , egUser , $uibModal , egCirc , egConfirmDialog) {
1064     var copyId = [];
1065     var cp_list = $routeParams.idList;
1066     if (cp_list) {
1067         copyId = cp_list.split(',');
1068     }
1069
1070     $scope.context.page = 'list';
1071
1072     /*
1073     var provider = egGridDataProvider.instance();
1074     provider.get = function(offset, count) {
1075     }
1076     */
1077
1078     $scope.gridDataProvider = egGridDataProvider.instance({
1079         get : function(offset, count) {
1080             //return provider.arrayNotifier(itemSvc.copies, offset, count);
1081             return this.arrayNotifier(itemSvc.copies, offset, count);
1082         }
1083     });
1084
1085     // If a copy was just displayed in the detail view, ensure it's
1086     // focused in the list view.
1087     var selected = false;
1088     var copyGrid = $scope.gridControls = {
1089         itemRetrieved : function(item) {
1090             if (selected || !itemSvc.copy) return;
1091             if (itemSvc.copy.id() == item.id) {
1092                 copyGrid.selectItems([item.index]);
1093                 selected = true;
1094             }
1095         }
1096     };
1097
1098     $scope.$watch('barcodesFromFile', function(newVal, oldVal) {
1099         if (newVal && newVal != oldVal) {
1100             $scope.args.barcode = '';
1101             var barcodes = [];
1102
1103             angular.forEach(newVal.split(/\n/), function(line) {
1104                 if (!line) return;
1105                 // scrub any trailing spaces or commas from the barcode
1106                 line = line.replace(/(.*?)($|\s.*|,.*)/,'$1');
1107                 barcodes.push(line);
1108             });
1109
1110             itemSvc.fetch(barcodes).then(
1111                 function() {
1112                     copyGrid.refresh();
1113                     copyGrid.selectItems([itemSvc.copies[0].index]);
1114                 }
1115             );
1116         }
1117     });
1118
1119     $scope.context.search = function(args) {
1120         if (!args.barcode) return;
1121         $scope.context.itemNotFound = false;
1122         itemSvc.fetch(args.barcode).then(function(res) {
1123             if (res) {
1124                 copyGrid.refresh();
1125                 copyGrid.selectItems([res.index]);
1126                 $scope.args.barcode = '';
1127             } else {
1128                 $scope.context.itemNotFound = true;
1129                 egCore.audio.play('warning.item_status.itemNotFound');
1130             }
1131             $scope.context.selectBarcode = true;
1132         })
1133     }
1134
1135     var add_barcode_to_list = function (b) {
1136         //console.log('listCtrl: add_barcode_to_list',b);
1137         $scope.context.search({barcode:b});
1138     }
1139     itemSvc.add_barcode_to_list = add_barcode_to_list;
1140
1141     $scope.context.toggleDisplay = function() {
1142         var item = copyGrid.selectedItems()[0];
1143         if (item) 
1144             $location.path('/cat/item/' + item.id);
1145     }
1146
1147     $scope.context.show_triggered_events = function() {
1148         var item = copyGrid.selectedItems()[0];
1149         if (item) 
1150             $location.path('/cat/item/' + item.id + '/triggered_events');
1151     }
1152
1153     function gatherSelectedRecordIds () {
1154         var rid_list = [];
1155         angular.forEach(
1156             copyGrid.selectedItems(),
1157             function (item) {
1158                 if (rid_list.indexOf(item['call_number.record.id']) == -1)
1159                     rid_list.push(item['call_number.record.id'])
1160             }
1161         );
1162         return rid_list;
1163     }
1164
1165     function gatherSelectedVolumeIds (rid) {
1166         var cn_id_list = [];
1167         angular.forEach(
1168             copyGrid.selectedItems(),
1169             function (item) {
1170                 if (rid && item['call_number.record.id'] != rid) return;
1171                 if (cn_id_list.indexOf(item['call_number.id']) == -1)
1172                     cn_id_list.push(item['call_number.id'])
1173             }
1174         );
1175         return cn_id_list;
1176     }
1177
1178     function gatherSelectedHoldingsIds (rid) {
1179         var cp_id_list = [];
1180         angular.forEach(
1181             copyGrid.selectedItems(),
1182             function (item) {
1183                 if (rid && item['call_number.record.id'] != rid) return;
1184                 cp_id_list.push(item.id)
1185             }
1186         );
1187         return cp_id_list;
1188     }
1189
1190     $scope.add_copies_to_bucket = function() {
1191         var copy_list = gatherSelectedHoldingsIds();
1192         itemSvc.add_copies_to_bucket(copy_list);
1193     }
1194
1195     $scope.need_one_selected = function() {
1196         var items = $scope.gridControls.selectedItems();
1197         if (items.length == 1) return false;
1198         return true;
1199     };
1200
1201     $scope.make_copies_bookable = function() {
1202         itemSvc.make_copies_bookable(copyGrid.selectedItems());
1203     }
1204
1205     $scope.book_copies_now = function() {
1206         itemSvc.book_copies_now(copyGrid.selectedItems());
1207     }
1208
1209     $scope.requestItems = function() {
1210         var copy_list = gatherSelectedHoldingsIds();
1211         itemSvc.requestItems(copy_list);
1212     }
1213
1214     $scope.replaceBarcodes = function() {
1215         itemSvc.replaceBarcodes(copyGrid.selectedItems());
1216     }
1217
1218     $scope.attach_to_peer_bib = function() {
1219         itemSvc.attach_to_peer_bib(copyGrid.selectedItems());
1220     }
1221
1222     $scope.selectedHoldingsCopyDelete = function () {
1223         itemSvc.selectedHoldingsCopyDelete(copyGrid.selectedItems());
1224     }
1225
1226     $scope.selectedHoldingsItemStatusTgrEvt= function() {
1227         var item = copyGrid.selectedItems()[0];
1228         if (item)
1229             $location.path('/cat/item/' + item.id + '/triggered_events');
1230     }
1231
1232     $scope.selectedHoldingsItemStatusHolds= function() {
1233         var item = copyGrid.selectedItems()[0];
1234         if (item)
1235             $location.path('/cat/item/' + item.id + '/holds');
1236     }
1237
1238     $scope.cancel_transit = function () {
1239         itemSvc.cancel_transit(copyGrid.selectedItems());
1240     }
1241
1242     $scope.selectedHoldingsDamaged = function () {
1243         itemSvc.selectedHoldingsDamaged(copyGrid.selectedItems());
1244     }
1245
1246     $scope.selectedHoldingsMissing = function () {
1247         itemSvc.selectedHoldingsMissing(copyGrid.selectedItems());
1248     }
1249
1250     $scope.checkin = function () {
1251         itemSvc.checkin(copyGrid.selectedItems());
1252     }
1253
1254     $scope.renew = function () {
1255         itemSvc.renew(copyGrid.selectedItems());
1256     }
1257
1258     $scope.selectedHoldingsVolCopyAdd = function () {
1259         itemSvc.spawnHoldingsAdd(copyGrid.selectedItems(),true,false);
1260     }
1261     $scope.selectedHoldingsCopyAdd = function () {
1262         itemSvc.spawnHoldingsAdd(copyGrid.selectedItems(),false,true);
1263     }
1264
1265     $scope.showBibHolds = function () {
1266         angular.forEach(gatherSelectedRecordIds(), function (r) {
1267             var url = egCore.env.basePath + 'cat/catalog/record/' + r + '/holds';
1268             $timeout(function() { $window.open(url, '_blank') });
1269         });
1270     }
1271
1272     $scope.selectedHoldingsVolCopyEdit = function () {
1273         itemSvc.spawnHoldingsEdit(copyGrid.selectedItems(),false,false);
1274     }
1275     $scope.selectedHoldingsVolEdit = function () {
1276         itemSvc.spawnHoldingsEdit(copyGrid.selectedItems(),false,true);
1277     }
1278     $scope.selectedHoldingsCopyEdit = function () {
1279         itemSvc.spawnHoldingsEdit(copyGrid.selectedItems(),true,false);
1280     }
1281
1282     $scope.changeItemOwningLib = function() {
1283         itemSvc.changeItemOwningLib(copyGrid.selectedItems());
1284     }
1285
1286     $scope.transferItems = function (){
1287         itemSvc.transferItems(copyGrid.selectedItems());
1288     }
1289
1290     $scope.print_labels = function() {
1291         egCore.net.request(
1292             'open-ils.actor',
1293             'open-ils.actor.anon_cache.set_value',
1294             null, 'print-labels-these-copies', {
1295                 copies : gatherSelectedHoldingsIds()
1296             }
1297         ).then(function(key) {
1298             if (key) {
1299                 var url = egCore.env.basePath + 'cat/printlabels/' + key;
1300                 $timeout(function() { $window.open(url, '_blank') });
1301             } else {
1302                 alert('Could not create anonymous cache key!');
1303             }
1304         });
1305     }
1306
1307     $scope.print_list = function() {
1308         var print_data = { copies : copyGrid.allItems() };
1309
1310         if (print_data.copies.length == 0) return $q.when();
1311
1312         return egCore.print.print({
1313             template : 'item_status',
1314             scope : print_data
1315         });
1316     }
1317
1318     if (copyId.length > 0) {
1319         itemSvc.fetch(null,copyId).then(
1320             function() {
1321                 copyGrid.refresh();
1322             }
1323         );
1324     }
1325
1326 }])
1327
1328 /**
1329  * Detail view -- shows one copy
1330  */
1331 .controller('ViewCtrl', 
1332        ['$scope','$q','$location','$routeParams','$timeout','$window','egCore','itemSvc','egBilling',
1333 function($scope , $q , $location , $routeParams , $timeout , $window , egCore , itemSvc , egBilling) {
1334     var copyId = $routeParams.id;
1335     $scope.args.copyId = copyId;
1336     $scope.tab = $routeParams.tab || 'summary';
1337     $scope.context.page = 'detail';
1338     $scope.summaryRecord = null;
1339
1340     $scope.edit = false;
1341     if ($scope.tab == 'edit') {
1342         $scope.tab = 'summary';
1343         $scope.edit = true;
1344     }
1345
1346
1347     // use the cached record info
1348     if (itemSvc.copy) {
1349         $scope.recordId = itemSvc.copy.call_number().record().id();
1350         $scope.args.recordId = $scope.recordId;
1351         $scope.args.cnId = itemSvc.copy.call_number().id();
1352         $scope.args.cnOwningLib = itemSvc.copy.call_number().owning_lib();
1353         $scope.args.cnLabel = itemSvc.copy.call_number().label();
1354         $scope.args.cnLabelClass = itemSvc.copy.call_number().label_class();
1355         $scope.args.cnPrefixId = itemSvc.copy.call_number().prefix().id();
1356         $scope.args.cnSuffixId = itemSvc.copy.call_number().suffix().id();
1357         $scope.args.copyBarcode = itemSvc.copy.barcode();
1358     }
1359
1360     function loadCopy(barcode) {
1361         $scope.context.itemNotFound = false;
1362
1363         // Avoid re-fetching the same copy while jumping tabs.
1364         // In addition to being quicker, this helps to avoid flickering
1365         // of the top panel which is always visible in the detail view.
1366         //
1367         // 'barcode' represents the loading of a new item - refetch it
1368         // regardless of whether it matches the current item.
1369         if (!barcode && itemSvc.copy && itemSvc.copy.id() == copyId) {
1370             $scope.copy = itemSvc.copy;
1371             $scope.recordId = itemSvc.copy.call_number().record().id();
1372             $scope.args.recordId = $scope.recordId;
1373             $scope.args.cnId = itemSvc.copy.call_number().id();
1374             $scope.args.cnOwningLib = itemSvc.copy.call_number().owning_lib();
1375             $scope.args.cnLabel = itemSvc.copy.call_number().label();
1376             $scope.args.cnLabelClass = itemSvc.copy.call_number().label_class();
1377             $scope.args.cnPrefixId = itemSvc.copy.call_number().prefix().id();
1378             $scope.args.cnSuffixId = itemSvc.copy.call_number().suffix().id();
1379             $scope.args.copyBarcode = itemSvc.copy.barcode();
1380             return $q.when();
1381         }
1382
1383         delete $scope.copy;
1384         delete itemSvc.copy;
1385
1386         var deferred = $q.defer();
1387         itemSvc.fetch(barcode, copyId, true).then(function(res) {
1388             $scope.context.selectBarcode = true;
1389
1390             if (!res) {
1391                 copyId = null;
1392                 $scope.context.itemNotFound = true;
1393                 egCore.audio.play('warning.item_status.itemNotFound');
1394                 deferred.reject(); // avoid propagation of data fetch calls
1395                 return;
1396             }
1397
1398             var copy = res.copy;
1399             itemSvc.copy = copy;
1400
1401
1402             $scope.copy = copy;
1403             $scope.recordId = copy.call_number().record().id();
1404             $scope.args.recordId = $scope.recordId;
1405             $scope.args.cnId = itemSvc.copy.call_number().id();
1406             $scope.args.cnOwningLib = itemSvc.copy.call_number().owning_lib();
1407             $scope.args.cnLabel = itemSvc.copy.call_number().label();
1408             $scope.args.cnLabelClass = itemSvc.copy.call_number().label_class();
1409             $scope.args.cnPrefixId = itemSvc.copy.call_number().prefix().id();
1410             $scope.args.cnSuffixId = itemSvc.copy.call_number().suffix().id();
1411             $scope.args.copyBarcode = copy.barcode();
1412             $scope.args.barcode = '';
1413
1414             // locally flesh org units
1415             copy.circ_lib(egCore.org.get(copy.circ_lib()));
1416             copy.call_number().owning_lib(
1417                 egCore.org.get(copy.call_number().owning_lib()));
1418
1419             var r = copy.call_number().record();
1420             if (r.owner()) r.owner(egCore.org.get(r.owner())); 
1421
1422             // make boolean for auto-magic true/false display
1423             angular.forEach(
1424                 ['ref','opac_visible','holdable','circulate'],
1425                 function(field) { copy[field](Boolean(copy[field]() == 't')) }
1426             );
1427
1428             // finally, if this is a different copy, redirect.
1429             // Note that we flesh first since the copy we just
1430             // fetched will be used after the redirect.
1431             if (copyId && copyId != copy.id()) {
1432                 // if a new barcode is scanned in the detail view,
1433                 // update the url to match the ID of the new copy
1434                 $location.path('/cat/item/' + copy.id() + '/' + $scope.tab);
1435                 deferred.reject(); // avoid propagation of data fetch calls
1436                 return;
1437             }
1438             copyId = copy.id();
1439
1440             deferred.resolve();
1441         });
1442
1443         return deferred.promise;
1444     }
1445
1446     // if loadPrev load the two most recent circulations
1447     function loadCurrentCirc(loadPrev) {
1448         delete $scope.circ;
1449         delete $scope.circ_summary;
1450         delete $scope.prev_circ_summary;
1451         delete $scope.prev_circ_usr;
1452         if (!copyId) return;
1453         
1454         egCore.pcrud.search('aacs', 
1455             {target_copy : copyId},
1456             {   flesh : 2,
1457                 flesh_fields : {
1458                     aacs : [
1459                         'usr',
1460                         'workstation',                                         
1461                         'checkin_workstation',                                 
1462                         'duration_rule',                                       
1463                         'max_fine_rule',                                       
1464                         'recurring_fine_rule'   
1465                     ],
1466                     au : ['card']
1467                 },
1468                 order_by : {aacs : 'xact_start desc'}, 
1469                 limit :  1
1470             }
1471
1472         ).then(null, null, function(circ) {
1473             $scope.circ = circ;
1474
1475             // load the chain for this circ
1476             egCore.net.request(
1477                 'open-ils.circ',
1478                 'open-ils.circ.renewal_chain.retrieve_by_circ.summary',
1479                 egCore.auth.token(), $scope.circ.id()
1480             ).then(function(summary) {
1481                 $scope.circ_summary = summary;
1482             });
1483
1484             if (!loadPrev) return;
1485
1486             // load the chain for the previous circ, plus the user
1487             egCore.net.request(
1488                 'open-ils.circ',
1489                 'open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary',
1490                 egCore.auth.token(), $scope.circ.id()
1491
1492             ).then(null, null, function(summary) {
1493                 $scope.prev_circ_summary = summary.summary;
1494
1495                 if (summary.usr) { // aged circs have no 'usr'.
1496                     egCore.pcrud.retrieve('au', summary.usr,
1497                         {flesh : 1, flesh_fields : {au : ['card']}})
1498
1499                     .then(function(user) { $scope.prev_circ_usr = user });
1500                 }
1501             });
1502         });
1503     }
1504
1505     var maxHistory;
1506     function fetchMaxCircHistory() {
1507         if (maxHistory) return $q.when(maxHistory);
1508         return egCore.org.settings(
1509             'circ.item_checkout_history.max')
1510         .then(function(set) {
1511             maxHistory = set['circ.item_checkout_history.max'] || 4;
1512             return maxHistory;
1513         });
1514     }
1515
1516     $scope.addBilling = function(circ) {
1517         egBilling.showBillDialog({
1518             xact_id : circ.id(),
1519             patron : circ.usr()
1520         });
1521     }
1522
1523     $scope.retrieveAllPatrons = function() {
1524         var users = new Set();
1525         angular.forEach($scope.circ_list.map(function(circ) { return circ.usr(); }),function(usr) {
1526             // aged circs have no 'usr'.
1527             if (usr) users.add(usr);
1528         });
1529         users.forEach(function(usr) {
1530             $timeout(function() {
1531                 var url = $location.absUrl().replace(
1532                     /\/cat\/.*/,
1533                     '/circ/patron/' + usr.id() + '/checkout');
1534                 $window.open(url, '_blank')
1535             });
1536         });
1537     }
1538
1539     function loadCircHistory() {
1540         $scope.circ_list = [];
1541
1542         var copy_org = 
1543             itemSvc.copy.call_number().id() == -1 ?
1544             itemSvc.copy.circ_lib().id() :
1545             itemSvc.copy.call_number().owning_lib().id()
1546
1547         // there is an extra layer of permissibility over circ
1548         // history views
1549         egCore.perm.hasPermAt('VIEW_COPY_CHECKOUT_HISTORY', true)
1550         .then(function(orgIds) {
1551
1552             if (orgIds.indexOf(copy_org) == -1) {
1553                 console.log('User is not allowed to view circ history');
1554                 return $q.when(0);
1555             }
1556
1557             return fetchMaxCircHistory();
1558
1559         }).then(function(count) {
1560
1561             egCore.pcrud.search('aacs', 
1562                 {target_copy : copyId},
1563                 {   flesh : 2,
1564                     flesh_fields : {
1565                         aacs : [
1566                             'usr',
1567                             'workstation',                                         
1568                             'checkin_workstation',                                 
1569                             'recurring_fine_rule'   
1570                         ],
1571                         au : ['card']
1572                     },
1573                     order_by : {aacs : 'xact_start desc'}, 
1574                     limit :  count
1575                 }
1576
1577             ).then(null, null, function(circ) {
1578
1579                 // flesh circ_lib locally
1580                 circ.circ_lib(egCore.org.get(circ.circ_lib()));
1581                 circ.checkin_lib(egCore.org.get(circ.checkin_lib()));
1582                 $scope.circ_list.push(circ);
1583             });
1584         });
1585     }
1586
1587
1588     function loadCircCounts() {
1589
1590         delete $scope.circ_counts;
1591         $scope.total_circs = 0;
1592         $scope.total_circs_this_year = 0;
1593         $scope.total_circs_prev_year = 0;
1594         if (!copyId) return;
1595
1596         egCore.pcrud.search('circbyyr', 
1597             {copy : copyId}, null, {atomic : true})
1598
1599         .then(function(counts) {
1600             $scope.circ_counts = counts;
1601
1602             angular.forEach(counts, function(count) {
1603                 $scope.total_circs += Number(count.count());
1604             });
1605
1606             var this_year = counts.filter(function(c) {
1607                 return c.year() == new Date().getFullYear();
1608             });
1609
1610             $scope.total_circs_this_year = 
1611                 this_year.length ? this_year[0].count() : 0;
1612
1613             var prev_year = counts.filter(function(c) {
1614                 return c.year() == new Date().getFullYear() - 1;
1615             });
1616
1617             $scope.total_circs_prev_year = 
1618                 prev_year.length ? prev_year[0].count() : 0;
1619
1620         });
1621     }
1622
1623     function loadHolds() {
1624         delete $scope.hold;
1625         if (!copyId) return;
1626
1627         egCore.pcrud.search('ahr', 
1628             {   current_copy : copyId, 
1629                 cancel_time : null, 
1630                 fulfillment_time : null,
1631                 capture_time : {'<>' : null}
1632             }, {
1633                 flesh : 2,
1634                 flesh_fields : {
1635                     ahr : ['requestor', 'usr'],
1636                     au  : ['card']
1637                 }
1638             }
1639         ).then(null, null, function(hold) {
1640             $scope.hold = hold;
1641             hold.pickup_lib(egCore.org.get(hold.pickup_lib()));
1642             if (hold.current_shelf_lib()) {
1643                 hold.current_shelf_lib(
1644                     egCore.org.get(hold.current_shelf_lib()));
1645             }
1646             hold.behind_desk(Boolean(hold.behind_desk() == 't'));
1647         });
1648     }
1649
1650     function loadTransits() {
1651         delete $scope.transit;
1652         delete $scope.hold_transit;
1653         if (!copyId) return;
1654
1655         egCore.pcrud.search('atc', 
1656             {target_copy : copyId},
1657             {order_by : {atc : 'source_send_time DESC'}}
1658
1659         ).then(null, null, function(transit) {
1660             $scope.transit = transit;
1661             transit.source(egCore.org.get(transit.source()));
1662             transit.dest(egCore.org.get(transit.dest()));
1663         })
1664     }
1665
1666
1667     // we don't need all data on all tabs, so fetch what's needed when needed.
1668     function loadTabData() {
1669         switch($scope.tab) {
1670             case 'summary':
1671                 loadCurrentCirc();
1672                 loadCircCounts();
1673                 break;
1674
1675             case 'circs':
1676                 loadCurrentCirc(true);
1677                 break;
1678
1679             case 'circ_list':
1680                 loadCircHistory();
1681                 break;
1682
1683             case 'holds':
1684                 loadHolds()
1685                 loadTransits();
1686                 break;
1687
1688             case 'triggered_events':
1689                 var url = $location.absUrl().replace(/\/staff.*/, '/actor/user/event_log');
1690                 url += '?copy_id=' + encodeURIComponent(copyId);
1691                 $scope.triggered_events_url = url;
1692                 $scope.funcs = {};
1693         }
1694
1695         if ($scope.edit) {
1696             egCore.net.request(
1697                 'open-ils.actor',
1698                 'open-ils.actor.anon_cache.set_value',
1699                 null, 'edit-these-copies', {
1700                     record_id: $scope.recordId,
1701                     copies: [copyId],
1702                     hide_vols : true,
1703                     hide_copies : false
1704                 }
1705             ).then(function(key) {
1706                 if (key) {
1707                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
1708                     $window.location.href = url;
1709                 } else {
1710                     alert('Could not create anonymous cache key!');
1711                 }
1712             });
1713         }
1714
1715         return;
1716     }
1717
1718     $scope.context.toggleDisplay = function() {
1719         $location.path('/cat/item/search');
1720     }
1721
1722     // handle the barcode scan box, which will replace our current copy
1723     $scope.context.search = function(args) {
1724         loadCopy(args.barcode).then(loadTabData);
1725     }
1726
1727     $scope.context.show_triggered_events = function() {
1728         $location.path('/cat/item/' + copyId + '/triggered_events');
1729     }
1730
1731     loadCopy().then(loadTabData);
1732 }])