48d2d8270f338f53e8a04f3447230ecf0e8bebad
[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, combcirc, 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('combcirc', { 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                     console.log('evt',evt);
844                     if (evt) {
845                         egConfirmDialog.open(
846                             egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_TITLE,
847                             egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_BODY,
848                             {'evt_desc': evt}
849                         ).result.then(function() {
850                             egCore.net.request(
851                                 'open-ils.cat',
852                                 'open-ils.cat.transfer_copies_to_volume.override',
853                                 egCore.auth.token(),
854                                 xfer_target,
855                                 copy_ids,
856                                 { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
857                             );
858                         }).then(function() {
859                             angular.forEach(items, function(cp){service.add_barcode_to_list(cp.barcode)});
860                         });
861                     } else {
862                         angular.forEach(items, function(cp){service.add_barcode_to_list(cp.barcode)});
863                     }
864
865                 },
866                 null, // onerror
867                 null // onprogress
868             );
869         }
870     }
871
872     return service;
873 }])
874
875 /**
876  * Search bar along the top of the page.
877  * Parent scope for list and detail views
878  */
879 .controller('SearchCtrl', 
880        ['$scope','$location','$timeout','egCore','egGridDataProvider','itemSvc',
881 function($scope , $location , $timeout , egCore , egGridDataProvider , itemSvc) {
882     $scope.args = {}; // search args
883
884     // sub-scopes (search / detail-view) apply their version 
885     // of retrieval function to $scope.context.search
886     // and display toggling via $scope.context.toggleDisplay
887     $scope.context = {
888         selectBarcode : true
889     };
890
891     $scope.toggleView = function($event) {
892         $scope.context.toggleDisplay();
893         $event.preventDefault(); // avoid form submission
894     }
895
896     // The functions that follow in this controller are never called
897     // when the List View is active, only the Detail View.
898     
899     // In this context, we're only ever dealing with 1 item, so
900     // we can simply refresh the page.  These various itemSvc
901     // functions used to live in the ListCtrl, but they're now
902     // shared between SearchCtrl (for Actions for the Detail View)
903     // and ListCtrl (Actions in the egGrid)
904     itemSvc.add_barcode_to_list = function(b) {
905         //console.log('SearchCtrl: add_barcode_to_list',b);
906         // timeout so audible can happen upon checkin
907         $timeout(function() { location.href = location.href; }, 1000);
908     }
909
910     $scope.add_copies_to_bucket = function() {
911         itemSvc.add_copies_to_bucket([$scope.args.copyId]);
912     }
913
914     $scope.make_copies_bookable = function() {
915         itemSvc.make_copies_bookable([{
916             id : $scope.args.copyId,
917             'call_number.record.id' : $scope.args.recordId
918         }]);
919     }
920
921     $scope.book_copies_now = function() {
922         itemSvc.book_copies_now([{
923             id : $scope.args.copyId,
924             'call_number.record.id' : $scope.args.recordId
925         }]);
926     }
927
928     $scope.requestItems = function() {
929         itemSvc.requestItems([$scope.args.copyId]);
930     }
931
932     $scope.attach_to_peer_bib = function() {
933         itemSvc.attach_to_peer_bib([{
934             id : $scope.args.copyId,
935             barcode : $scope.args.copyBarcode
936         }]);
937     }
938
939     $scope.selectedHoldingsCopyDelete = function () {
940         itemSvc.selectedHoldingsCopyDelete([{
941             id : $scope.args.copyId,
942             barcode : $scope.args.copyBarcode
943         }]);
944     }
945
946     $scope.checkin = function () {
947         itemSvc.checkin([{
948             id : $scope.args.copyId,
949             barcode : $scope.args.copyBarcode
950         }]);
951     }
952
953     $scope.renew = function () {
954         itemSvc.renew([{
955             id : $scope.args.copyId,
956             barcode : $scope.args.copyBarcode
957         }]);
958     }
959
960     $scope.cancel_transit = function () {
961         itemSvc.cancel_transit([{
962             id : $scope.args.copyId,
963             barcode : $scope.args.copyBarcode
964         }]);
965     }
966
967     $scope.selectedHoldingsDamaged = function () {
968         itemSvc.selectedHoldingsDamaged([{
969             id : $scope.args.copyId,
970             barcode : $scope.args.copyBarcode
971         }]);
972     }
973
974     $scope.selectedHoldingsMissing = function () {
975         itemSvc.selectedHoldingsMissing([{
976             id : $scope.args.copyId,
977             barcode : $scope.args.copyBarcode
978         }]);
979     }
980
981     $scope.selectedHoldingsVolCopyAdd = function () {
982         itemSvc.spawnHoldingsAdd([{
983             id : $scope.args.copyId,
984             'call_number.owning_lib' : $scope.args.cnOwningLib,
985             'call_number.record.id' : $scope.args.recordId,
986             barcode : $scope.args.copyBarcode
987         }],true,false);
988     }
989     $scope.selectedHoldingsCopyAdd = function () {
990         itemSvc.spawnHoldingsAdd([{
991             id : $scope.args.copyId,
992             'call_number.id' : $scope.args.cnId,
993             'call_number.owning_lib' : $scope.args.cnOwningLib,
994             'call_number.record.id' : $scope.args.recordId,
995             barcode : $scope.args.copyBarcode
996         }],false,true);
997     }
998
999     $scope.selectedHoldingsVolCopyEdit = function () {
1000         itemSvc.spawnHoldingsEdit([{
1001             id : $scope.args.copyId,
1002             'call_number.id' : $scope.args.cnId,
1003             'call_number.owning_lib' : $scope.args.cnOwningLib,
1004             'call_number.record.id' : $scope.args.recordId,
1005             barcode : $scope.args.copyBarcode
1006         }],false,false);
1007     }
1008     $scope.selectedHoldingsVolEdit = function () {
1009         itemSvc.spawnHoldingsEdit([{
1010             id : $scope.args.copyId,
1011             'call_number.id' : $scope.args.cnId,
1012             'call_number.owning_lib' : $scope.args.cnOwningLib,
1013             'call_number.record.id' : $scope.args.recordId,
1014             barcode : $scope.args.copyBarcode
1015         }],false,true);
1016     }
1017     $scope.selectedHoldingsCopyEdit = function () {
1018         itemSvc.spawnHoldingsEdit([{
1019             id : $scope.args.copyId,
1020             'call_number.id' : $scope.args.cnId,
1021             'call_number.owning_lib' : $scope.args.cnOwningLib,
1022             'call_number.record.id' : $scope.args.recordId,
1023             barcode : $scope.args.copyBarcode
1024         }],true,false);
1025     }
1026
1027     $scope.replaceBarcodes = function() {
1028         itemSvc.replaceBarcodes([{
1029             id : $scope.args.copyId,
1030             barcode : $scope.args.copyBarcode
1031         }]);
1032     }
1033
1034     $scope.changeItemOwningLib = function() {
1035         itemSvc.changeItemOwningLib([{
1036             id : $scope.args.copyId,
1037             'call_number.id' : $scope.args.cnId,
1038             'call_number.owning_lib' : $scope.args.cnOwningLib,
1039             'call_number.record.id' : $scope.args.recordId,
1040             'call_number.label' : $scope.args.cnLabel,
1041             'call_number.label_class' : $scope.args.cnLabelClass,
1042             'call_number.prefix.id' : $scope.args.cnPrefixId,
1043             'call_number.suffix.id' : $scope.args.cnSuffixId,
1044             barcode : $scope.args.copyBarcode
1045         }]);
1046     }
1047
1048     $scope.transferItems = function (){
1049         itemSvc.transferItems([{
1050             id : $scope.args.copyId,
1051             barcode : $scope.args.copyBarcode
1052         }]);
1053     }
1054
1055 }])
1056
1057 /**
1058  * List view - grid stuff
1059  */
1060 .controller('ListCtrl', 
1061        ['$scope','$q','$routeParams','$location','$timeout','$window','egCore','egGridDataProvider','itemSvc','egUser','$uibModal','egCirc','egConfirmDialog',
1062 function($scope , $q , $routeParams , $location , $timeout , $window , egCore , egGridDataProvider , itemSvc , egUser , $uibModal , egCirc , egConfirmDialog) {
1063     var copyId = [];
1064     var cp_list = $routeParams.idList;
1065     if (cp_list) {
1066         copyId = cp_list.split(',');
1067     }
1068
1069     $scope.context.page = 'list';
1070
1071     /*
1072     var provider = egGridDataProvider.instance();
1073     provider.get = function(offset, count) {
1074     }
1075     */
1076
1077     $scope.gridDataProvider = egGridDataProvider.instance({
1078         get : function(offset, count) {
1079             //return provider.arrayNotifier(itemSvc.copies, offset, count);
1080             return this.arrayNotifier(itemSvc.copies, offset, count);
1081         }
1082     });
1083
1084     // If a copy was just displayed in the detail view, ensure it's
1085     // focused in the list view.
1086     var selected = false;
1087     var copyGrid = $scope.gridControls = {
1088         itemRetrieved : function(item) {
1089             if (selected || !itemSvc.copy) return;
1090             if (itemSvc.copy.id() == item.id) {
1091                 copyGrid.selectItems([item.index]);
1092                 selected = true;
1093             }
1094         }
1095     };
1096
1097     $scope.$watch('barcodesFromFile', function(newVal, oldVal) {
1098         if (newVal && newVal != oldVal) {
1099             $scope.args.barcode = '';
1100             var barcodes = [];
1101
1102             angular.forEach(newVal.split(/\n/), function(line) {
1103                 if (!line) return;
1104                 // scrub any trailing spaces or commas from the barcode
1105                 line = line.replace(/(.*?)($|\s.*|,.*)/,'$1');
1106                 barcodes.push(line);
1107             });
1108
1109             itemSvc.fetch(barcodes).then(
1110                 function() {
1111                     copyGrid.refresh();
1112                     copyGrid.selectItems([itemSvc.copies[0].index]);
1113                 }
1114             );
1115         }
1116     });
1117
1118     $scope.context.search = function(args) {
1119         if (!args.barcode) return;
1120         $scope.context.itemNotFound = false;
1121         itemSvc.fetch(args.barcode).then(function(res) {
1122             if (res) {
1123                 copyGrid.refresh();
1124                 copyGrid.selectItems([res.index]);
1125                 $scope.args.barcode = '';
1126             } else {
1127                 $scope.context.itemNotFound = true;
1128                 egCore.audio.play('warning.item_status.itemNotFound');
1129             }
1130             $scope.context.selectBarcode = true;
1131         })
1132     }
1133
1134     var add_barcode_to_list = function (b) {
1135         //console.log('listCtrl: add_barcode_to_list',b);
1136         $scope.context.search({barcode:b});
1137     }
1138     itemSvc.add_barcode_to_list = add_barcode_to_list;
1139
1140     $scope.context.toggleDisplay = function() {
1141         var item = copyGrid.selectedItems()[0];
1142         if (item) 
1143             $location.path('/cat/item/' + item.id);
1144     }
1145
1146     $scope.context.show_triggered_events = function() {
1147         var item = copyGrid.selectedItems()[0];
1148         if (item) 
1149             $location.path('/cat/item/' + item.id + '/triggered_events');
1150     }
1151
1152     function gatherSelectedRecordIds () {
1153         var rid_list = [];
1154         angular.forEach(
1155             copyGrid.selectedItems(),
1156             function (item) {
1157                 if (rid_list.indexOf(item['call_number.record.id']) == -1)
1158                     rid_list.push(item['call_number.record.id'])
1159             }
1160         );
1161         return rid_list;
1162     }
1163
1164     function gatherSelectedVolumeIds (rid) {
1165         var cn_id_list = [];
1166         angular.forEach(
1167             copyGrid.selectedItems(),
1168             function (item) {
1169                 if (rid && item['call_number.record.id'] != rid) return;
1170                 if (cn_id_list.indexOf(item['call_number.id']) == -1)
1171                     cn_id_list.push(item['call_number.id'])
1172             }
1173         );
1174         return cn_id_list;
1175     }
1176
1177     function gatherSelectedHoldingsIds (rid) {
1178         var cp_id_list = [];
1179         angular.forEach(
1180             copyGrid.selectedItems(),
1181             function (item) {
1182                 if (rid && item['call_number.record.id'] != rid) return;
1183                 cp_id_list.push(item.id)
1184             }
1185         );
1186         return cp_id_list;
1187     }
1188
1189     $scope.add_copies_to_bucket = function() {
1190         var copy_list = gatherSelectedHoldingsIds();
1191         itemSvc.add_copies_to_bucket(copy_list);
1192     }
1193
1194     $scope.need_one_selected = function() {
1195         var items = $scope.gridControls.selectedItems();
1196         if (items.length == 1) return false;
1197         return true;
1198     };
1199
1200     $scope.make_copies_bookable = function() {
1201         itemSvc.make_copies_bookable(copyGrid.selectedItems());
1202     }
1203
1204     $scope.book_copies_now = function() {
1205         itemSvc.book_copies_now(copyGrid.selectedItems());
1206     }
1207
1208     $scope.requestItems = function() {
1209         var copy_list = gatherSelectedHoldingsIds();
1210         itemSvc.requestItems(copy_list);
1211     }
1212
1213     $scope.replaceBarcodes = function() {
1214         itemSvc.replaceBarcodes(copyGrid.selectedItems());
1215     }
1216
1217     $scope.attach_to_peer_bib = function() {
1218         itemSvc.attach_to_peer_bib(copyGrid.selectedItems());
1219     }
1220
1221     $scope.selectedHoldingsCopyDelete = function () {
1222         itemSvc.selectedHoldingsCopyDelete(copyGrid.selectedItems());
1223     }
1224
1225     $scope.selectedHoldingsItemStatusTgrEvt= function() {
1226         var item = copyGrid.selectedItems()[0];
1227         if (item)
1228             $location.path('/cat/item/' + item.id + '/triggered_events');
1229     }
1230
1231     $scope.selectedHoldingsItemStatusHolds= function() {
1232         var item = copyGrid.selectedItems()[0];
1233         if (item)
1234             $location.path('/cat/item/' + item.id + '/holds');
1235     }
1236
1237     $scope.cancel_transit = function () {
1238         itemSvc.cancel_transit(copyGrid.selectedItems());
1239     }
1240
1241     $scope.selectedHoldingsDamaged = function () {
1242         itemSvc.selectedHoldingsDamaged(copyGrid.selectedItems());
1243     }
1244
1245     $scope.selectedHoldingsMissing = function () {
1246         itemSvc.selectedHoldingsMissing(copyGrid.selectedItems());
1247     }
1248
1249     $scope.checkin = function () {
1250         itemSvc.checkin(copyGrid.selectedItems());
1251     }
1252
1253     $scope.renew = function () {
1254         itemSvc.renew(copyGrid.selectedItems());
1255     }
1256
1257     $scope.selectedHoldingsVolCopyAdd = function () {
1258         itemSvc.spawnHoldingsAdd(copyGrid.selectedItems(),true,false);
1259     }
1260     $scope.selectedHoldingsCopyAdd = function () {
1261         itemSvc.spawnHoldingsAdd(copyGrid.selectedItems(),false,true);
1262     }
1263
1264     $scope.showBibHolds = function () {
1265         angular.forEach(gatherSelectedRecordIds(), function (r) {
1266             var url = egCore.env.basePath + 'cat/catalog/record/' + r + '/holds';
1267             $timeout(function() { $window.open(url, '_blank') });
1268         });
1269     }
1270
1271     $scope.selectedHoldingsVolCopyEdit = function () {
1272         itemSvc.spawnHoldingsEdit(copyGrid.selectedItems(),false,false);
1273     }
1274     $scope.selectedHoldingsVolEdit = function () {
1275         itemSvc.spawnHoldingsEdit(copyGrid.selectedItems(),false,true);
1276     }
1277     $scope.selectedHoldingsCopyEdit = function () {
1278         itemSvc.spawnHoldingsEdit(copyGrid.selectedItems(),true,false);
1279     }
1280
1281     $scope.changeItemOwningLib = function() {
1282         itemSvc.changeItemOwningLib(copyGrid.selectedItems());
1283     }
1284
1285     $scope.transferItems = function (){
1286         itemSvc.transferItems(copyGrid.selectedItems());
1287     }
1288
1289     $scope.print_list = function() {
1290         var print_data = { copies : copyGrid.allItems() };
1291
1292         if (print_data.copies.length == 0) return $q.when();
1293
1294         return egCore.print.print({
1295             template : 'item_status',
1296             scope : print_data
1297         });
1298     }
1299
1300     if (copyId.length > 0) {
1301         itemSvc.fetch(null,copyId).then(
1302             function() {
1303                 copyGrid.refresh();
1304             }
1305         );
1306     }
1307
1308 }])
1309
1310 /**
1311  * Detail view -- shows one copy
1312  */
1313 .controller('ViewCtrl', 
1314        ['$scope','$q','$location','$routeParams','$timeout','$window','egCore','itemSvc','egBilling',
1315 function($scope , $q , $location , $routeParams , $timeout , $window , egCore , itemSvc , egBilling) {
1316     var copyId = $routeParams.id;
1317     $scope.args.copyId = copyId;
1318     $scope.tab = $routeParams.tab || 'summary';
1319     $scope.context.page = 'detail';
1320     $scope.summaryRecord = null;
1321
1322     $scope.edit = false;
1323     if ($scope.tab == 'edit') {
1324         $scope.tab = 'summary';
1325         $scope.edit = true;
1326     }
1327
1328
1329     // use the cached record info
1330     if (itemSvc.copy) {
1331         $scope.recordId = itemSvc.copy.call_number().record().id();
1332         $scope.args.recordId = $scope.recordId;
1333         $scope.args.cnId = itemSvc.copy.call_number().id();
1334         $scope.args.cnOwningLib = itemSvc.copy.call_number().owning_lib();
1335         $scope.args.cnLabel = itemSvc.copy.call_number().label();
1336         $scope.args.cnLabelClass = itemSvc.copy.call_number().label_class();
1337         $scope.args.cnPrefixId = itemSvc.copy.call_number().prefix().id();
1338         $scope.args.cnSuffixId = itemSvc.copy.call_number().suffix().id();
1339         $scope.args.copyBarcode = itemSvc.copy.barcode();
1340     }
1341
1342     function loadCopy(barcode) {
1343         $scope.context.itemNotFound = false;
1344
1345         // Avoid re-fetching the same copy while jumping tabs.
1346         // In addition to being quicker, this helps to avoid flickering
1347         // of the top panel which is always visible in the detail view.
1348         //
1349         // 'barcode' represents the loading of a new item - refetch it
1350         // regardless of whether it matches the current item.
1351         if (!barcode && itemSvc.copy && itemSvc.copy.id() == copyId) {
1352             $scope.copy = itemSvc.copy;
1353             $scope.recordId = itemSvc.copy.call_number().record().id();
1354             $scope.args.recordId = $scope.recordId;
1355             $scope.args.cnId = itemSvc.copy.call_number().id();
1356             $scope.args.cnOwningLib = itemSvc.copy.call_number().owning_lib();
1357             $scope.args.cnLabel = itemSvc.copy.call_number().label();
1358             $scope.args.cnLabelClass = itemSvc.copy.call_number().label_class();
1359             $scope.args.cnPrefixId = itemSvc.copy.call_number().prefix().id();
1360             $scope.args.cnSuffixId = itemSvc.copy.call_number().suffix().id();
1361             $scope.args.copyBarcode = itemSvc.copy.barcode();
1362             return $q.when();
1363         }
1364
1365         delete $scope.copy;
1366         delete itemSvc.copy;
1367
1368         var deferred = $q.defer();
1369         itemSvc.fetch(barcode, copyId, true).then(function(res) {
1370             $scope.context.selectBarcode = true;
1371
1372             if (!res) {
1373                 copyId = null;
1374                 $scope.context.itemNotFound = true;
1375                 egCore.audio.play('warning.item_status.itemNotFound');
1376                 deferred.reject(); // avoid propagation of data fetch calls
1377                 return;
1378             }
1379
1380             var copy = res.copy;
1381             itemSvc.copy = copy;
1382
1383
1384             $scope.copy = copy;
1385             $scope.recordId = copy.call_number().record().id();
1386             $scope.args.recordId = $scope.recordId;
1387             $scope.args.cnId = itemSvc.copy.call_number().id();
1388             $scope.args.cnOwningLib = itemSvc.copy.call_number().owning_lib();
1389             $scope.args.cnLabel = itemSvc.copy.call_number().label();
1390             $scope.args.cnLabelClass = itemSvc.copy.call_number().label_class();
1391             $scope.args.cnPrefixId = itemSvc.copy.call_number().prefix().id();
1392             $scope.args.cnSuffixId = itemSvc.copy.call_number().suffix().id();
1393             $scope.args.copyBarcode = copy.barcode();
1394             $scope.args.barcode = '';
1395
1396             // locally flesh org units
1397             copy.circ_lib(egCore.org.get(copy.circ_lib()));
1398             copy.call_number().owning_lib(
1399                 egCore.org.get(copy.call_number().owning_lib()));
1400
1401             var r = copy.call_number().record();
1402             if (r.owner()) r.owner(egCore.org.get(r.owner())); 
1403
1404             // make boolean for auto-magic true/false display
1405             angular.forEach(
1406                 ['ref','opac_visible','holdable','circulate'],
1407                 function(field) { copy[field](Boolean(copy[field]() == 't')) }
1408             );
1409
1410             // finally, if this is a different copy, redirect.
1411             // Note that we flesh first since the copy we just
1412             // fetched will be used after the redirect.
1413             if (copyId && copyId != copy.id()) {
1414                 // if a new barcode is scanned in the detail view,
1415                 // update the url to match the ID of the new copy
1416                 $location.path('/cat/item/' + copy.id() + '/' + $scope.tab);
1417                 deferred.reject(); // avoid propagation of data fetch calls
1418                 return;
1419             }
1420             copyId = copy.id();
1421
1422             deferred.resolve();
1423         });
1424
1425         return deferred.promise;
1426     }
1427
1428     // if loadPrev load the two most recent circulations
1429     function loadCurrentCirc(loadPrev) {
1430         delete $scope.circ;
1431         delete $scope.circ_summary;
1432         delete $scope.prev_circ_summary;
1433         delete $scope.prev_circ_usr;
1434         if (!copyId) return;
1435         
1436         egCore.pcrud.search('circ', 
1437             {target_copy : copyId},
1438             {   flesh : 2,
1439                 flesh_fields : {
1440                     circ : [
1441                         'usr',
1442                         'workstation',                                         
1443                         'checkin_workstation',                                 
1444                         'duration_rule',                                       
1445                         'max_fine_rule',                                       
1446                         'recurring_fine_rule'   
1447                     ],
1448                     au : ['card']
1449                 },
1450                 order_by : {circ : 'xact_start desc'}, 
1451                 limit :  1
1452             }
1453
1454         ).then(null, null, function(circ) {
1455             $scope.circ = circ;
1456
1457             // load the chain for this circ
1458             egCore.net.request(
1459                 'open-ils.circ',
1460                 'open-ils.circ.renewal_chain.retrieve_by_circ.summary',
1461                 egCore.auth.token(), $scope.circ.id()
1462             ).then(function(summary) {
1463                 $scope.circ_summary = summary.summary;
1464             });
1465
1466             if (!loadPrev) return;
1467
1468             // load the chain for the previous circ, plus the user
1469             egCore.net.request(
1470                 'open-ils.circ',
1471                 'open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary',
1472                 egCore.auth.token(), $scope.circ.id()
1473
1474             ).then(null, null, function(summary) {
1475                 $scope.prev_circ_summary = summary.summary;
1476
1477                 if (summary.usr) { // aged circs have no 'usr'.
1478                     egCore.pcrud.retrieve('au', summary.usr,
1479                         {flesh : 1, flesh_fields : {au : ['card']}})
1480
1481                     .then(function(user) { $scope.prev_circ_usr = user });
1482                 }
1483             });
1484         });
1485     }
1486
1487     var maxHistory;
1488     function fetchMaxCircHistory() {
1489         if (maxHistory) return $q.when(maxHistory);
1490         return egCore.org.settings(
1491             'circ.item_checkout_history.max')
1492         .then(function(set) {
1493             maxHistory = set['circ.item_checkout_history.max'] || 4;
1494             return maxHistory;
1495         });
1496     }
1497
1498     $scope.addBilling = function(circ) {
1499         egBilling.showBillDialog({
1500             xact_id : circ.id(),
1501             patron : circ.usr()
1502         });
1503     }
1504
1505     $scope.retrieveAllPatrons = function() {
1506         var users = new Set();
1507         angular.forEach($scope.circ_list.map(function(circ) { return circ.usr(); }),function(usr) {
1508             // aged circs have no 'usr'.
1509             if (usr) users.add(usr);
1510         });
1511         users.forEach(function(usr) {
1512             $timeout(function() {
1513                 var url = $location.absUrl().replace(
1514                     /\/cat\/.*/,
1515                     '/circ/patron/' + usr.id() + '/checkout');
1516                 $window.open(url, '_blank')
1517             });
1518         });
1519     }
1520
1521     function loadCircHistory() {
1522         $scope.circ_list = [];
1523
1524         var copy_org = 
1525             itemSvc.copy.call_number().id() == -1 ?
1526             itemSvc.copy.circ_lib().id() :
1527             itemSvc.copy.call_number().owning_lib().id()
1528
1529         // there is an extra layer of permissibility over circ
1530         // history views
1531         egCore.perm.hasPermAt('VIEW_COPY_CHECKOUT_HISTORY', true)
1532         .then(function(orgIds) {
1533
1534             if (orgIds.indexOf(copy_org) == -1) {
1535                 console.log('User is not allowed to view circ history');
1536                 return $q.when(0);
1537             }
1538
1539             return fetchMaxCircHistory();
1540
1541         }).then(function(count) {
1542
1543             egCore.pcrud.search('combcirc', 
1544                 {target_copy : copyId},
1545                 {   flesh : 2,
1546                     flesh_fields : {
1547                         combcirc : [
1548                             'usr',
1549                             'workstation',                                         
1550                             'checkin_workstation',                                 
1551                             'recurring_fine_rule'   
1552                         ],
1553                         au : ['card']
1554                     },
1555                     order_by : {combcirc : 'xact_start desc'}, 
1556                     limit :  count
1557                 }
1558
1559             ).then(null, null, function(circ) {
1560
1561                 // flesh circ_lib locally
1562                 circ.circ_lib(egCore.org.get(circ.circ_lib()));
1563                 circ.checkin_lib(egCore.org.get(circ.checkin_lib()));
1564                 $scope.circ_list.push(circ);
1565             });
1566         });
1567     }
1568
1569
1570     function loadCircCounts() {
1571
1572         delete $scope.circ_counts;
1573         $scope.total_circs = 0;
1574         $scope.total_circs_this_year = 0;
1575         $scope.total_circs_prev_year = 0;
1576         if (!copyId) return;
1577
1578         egCore.pcrud.search('circbyyr', 
1579             {copy : copyId}, null, {atomic : true})
1580
1581         .then(function(counts) {
1582             $scope.circ_counts = counts;
1583
1584             angular.forEach(counts, function(count) {
1585                 $scope.total_circs += Number(count.count());
1586             });
1587
1588             var this_year = counts.filter(function(c) {
1589                 return c.year() == new Date().getFullYear();
1590             });
1591
1592             $scope.total_circs_this_year = 
1593                 this_year.length ? this_year[0].count() : 0;
1594
1595             var prev_year = counts.filter(function(c) {
1596                 return c.year() == new Date().getFullYear() - 1;
1597             });
1598
1599             $scope.total_circs_prev_year = 
1600                 prev_year.length ? prev_year[0].count() : 0;
1601
1602         });
1603     }
1604
1605     function loadHolds() {
1606         delete $scope.hold;
1607         if (!copyId) return;
1608
1609         egCore.pcrud.search('ahr', 
1610             {   current_copy : copyId, 
1611                 cancel_time : null, 
1612                 fulfillment_time : null,
1613                 capture_time : {'<>' : null}
1614             }, {
1615                 flesh : 2,
1616                 flesh_fields : {
1617                     ahr : ['requestor', 'usr'],
1618                     au  : ['card']
1619                 }
1620             }
1621         ).then(null, null, function(hold) {
1622             $scope.hold = hold;
1623             hold.pickup_lib(egCore.org.get(hold.pickup_lib()));
1624             if (hold.current_shelf_lib()) {
1625                 hold.current_shelf_lib(
1626                     egCore.org.get(hold.current_shelf_lib()));
1627             }
1628             hold.behind_desk(Boolean(hold.behind_desk() == 't'));
1629         });
1630     }
1631
1632     function loadTransits() {
1633         delete $scope.transit;
1634         delete $scope.hold_transit;
1635         if (!copyId) return;
1636
1637         egCore.pcrud.search('atc', 
1638             {target_copy : copyId},
1639             {order_by : {atc : 'source_send_time DESC'}}
1640
1641         ).then(null, null, function(transit) {
1642             $scope.transit = transit;
1643             transit.source(egCore.org.get(transit.source()));
1644             transit.dest(egCore.org.get(transit.dest()));
1645         })
1646     }
1647
1648
1649     // we don't need all data on all tabs, so fetch what's needed when needed.
1650     function loadTabData() {
1651         switch($scope.tab) {
1652             case 'summary':
1653                 loadCurrentCirc();
1654                 loadCircCounts();
1655                 break;
1656
1657             case 'circs':
1658                 loadCurrentCirc(true);
1659                 break;
1660
1661             case 'circ_list':
1662                 loadCircHistory();
1663                 break;
1664
1665             case 'holds':
1666                 loadHolds()
1667                 loadTransits();
1668                 break;
1669
1670             case 'triggered_events':
1671                 var url = $location.absUrl().replace(/\/staff.*/, '/actor/user/event_log');
1672                 url += '?copy_id=' + encodeURIComponent(copyId);
1673                 $scope.triggered_events_url = url;
1674                 $scope.funcs = {};
1675         }
1676
1677         if ($scope.edit) {
1678             egCore.net.request(
1679                 'open-ils.actor',
1680                 'open-ils.actor.anon_cache.set_value',
1681                 null, 'edit-these-copies', {
1682                     record_id: $scope.recordId,
1683                     copies: [copyId],
1684                     hide_vols : true,
1685                     hide_copies : false
1686                 }
1687             ).then(function(key) {
1688                 if (key) {
1689                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
1690                     $window.location.href = url;
1691                 } else {
1692                     alert('Could not create anonymous cache key!');
1693                 }
1694             });
1695         }
1696
1697         return;
1698     }
1699
1700     $scope.context.toggleDisplay = function() {
1701         $location.path('/cat/item/search');
1702     }
1703
1704     // handle the barcode scan box, which will replace our current copy
1705     $scope.context.search = function(args) {
1706         loadCopy(args.barcode).then(loadTabData);
1707     }
1708
1709     $scope.context.show_triggered_events = function() {
1710         $location.path('/cat/item/' + copyId + '/triggered_events');
1711     }
1712
1713     loadCopy().then(loadTabData);
1714 }])