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