]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/cat/item/app.js
LP#1533326: follow-up to remove extra logging statement
[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                     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_list = function() {
1289         var print_data = { copies : copyGrid.allItems() };
1290
1291         if (print_data.copies.length == 0) return $q.when();
1292
1293         return egCore.print.print({
1294             template : 'item_status',
1295             scope : print_data
1296         });
1297     }
1298
1299     if (copyId.length > 0) {
1300         itemSvc.fetch(null,copyId).then(
1301             function() {
1302                 copyGrid.refresh();
1303             }
1304         );
1305     }
1306
1307 }])
1308
1309 /**
1310  * Detail view -- shows one copy
1311  */
1312 .controller('ViewCtrl', 
1313        ['$scope','$q','$location','$routeParams','$timeout','$window','egCore','itemSvc','egBilling',
1314 function($scope , $q , $location , $routeParams , $timeout , $window , egCore , itemSvc , egBilling) {
1315     var copyId = $routeParams.id;
1316     $scope.args.copyId = copyId;
1317     $scope.tab = $routeParams.tab || 'summary';
1318     $scope.context.page = 'detail';
1319     $scope.summaryRecord = null;
1320
1321     $scope.edit = false;
1322     if ($scope.tab == 'edit') {
1323         $scope.tab = 'summary';
1324         $scope.edit = true;
1325     }
1326
1327
1328     // use the cached record info
1329     if (itemSvc.copy) {
1330         $scope.recordId = itemSvc.copy.call_number().record().id();
1331         $scope.args.recordId = $scope.recordId;
1332         $scope.args.cnId = itemSvc.copy.call_number().id();
1333         $scope.args.cnOwningLib = itemSvc.copy.call_number().owning_lib();
1334         $scope.args.cnLabel = itemSvc.copy.call_number().label();
1335         $scope.args.cnLabelClass = itemSvc.copy.call_number().label_class();
1336         $scope.args.cnPrefixId = itemSvc.copy.call_number().prefix().id();
1337         $scope.args.cnSuffixId = itemSvc.copy.call_number().suffix().id();
1338         $scope.args.copyBarcode = itemSvc.copy.barcode();
1339     }
1340
1341     function loadCopy(barcode) {
1342         $scope.context.itemNotFound = false;
1343
1344         // Avoid re-fetching the same copy while jumping tabs.
1345         // In addition to being quicker, this helps to avoid flickering
1346         // of the top panel which is always visible in the detail view.
1347         //
1348         // 'barcode' represents the loading of a new item - refetch it
1349         // regardless of whether it matches the current item.
1350         if (!barcode && itemSvc.copy && itemSvc.copy.id() == copyId) {
1351             $scope.copy = itemSvc.copy;
1352             $scope.recordId = itemSvc.copy.call_number().record().id();
1353             $scope.args.recordId = $scope.recordId;
1354             $scope.args.cnId = itemSvc.copy.call_number().id();
1355             $scope.args.cnOwningLib = itemSvc.copy.call_number().owning_lib();
1356             $scope.args.cnLabel = itemSvc.copy.call_number().label();
1357             $scope.args.cnLabelClass = itemSvc.copy.call_number().label_class();
1358             $scope.args.cnPrefixId = itemSvc.copy.call_number().prefix().id();
1359             $scope.args.cnSuffixId = itemSvc.copy.call_number().suffix().id();
1360             $scope.args.copyBarcode = itemSvc.copy.barcode();
1361             return $q.when();
1362         }
1363
1364         delete $scope.copy;
1365         delete itemSvc.copy;
1366
1367         var deferred = $q.defer();
1368         itemSvc.fetch(barcode, copyId, true).then(function(res) {
1369             $scope.context.selectBarcode = true;
1370
1371             if (!res) {
1372                 copyId = null;
1373                 $scope.context.itemNotFound = true;
1374                 egCore.audio.play('warning.item_status.itemNotFound');
1375                 deferred.reject(); // avoid propagation of data fetch calls
1376                 return;
1377             }
1378
1379             var copy = res.copy;
1380             itemSvc.copy = copy;
1381
1382
1383             $scope.copy = copy;
1384             $scope.recordId = copy.call_number().record().id();
1385             $scope.args.recordId = $scope.recordId;
1386             $scope.args.cnId = itemSvc.copy.call_number().id();
1387             $scope.args.cnOwningLib = itemSvc.copy.call_number().owning_lib();
1388             $scope.args.cnLabel = itemSvc.copy.call_number().label();
1389             $scope.args.cnLabelClass = itemSvc.copy.call_number().label_class();
1390             $scope.args.cnPrefixId = itemSvc.copy.call_number().prefix().id();
1391             $scope.args.cnSuffixId = itemSvc.copy.call_number().suffix().id();
1392             $scope.args.copyBarcode = copy.barcode();
1393             $scope.args.barcode = '';
1394
1395             // locally flesh org units
1396             copy.circ_lib(egCore.org.get(copy.circ_lib()));
1397             copy.call_number().owning_lib(
1398                 egCore.org.get(copy.call_number().owning_lib()));
1399
1400             var r = copy.call_number().record();
1401             if (r.owner()) r.owner(egCore.org.get(r.owner())); 
1402
1403             // make boolean for auto-magic true/false display
1404             angular.forEach(
1405                 ['ref','opac_visible','holdable','circulate'],
1406                 function(field) { copy[field](Boolean(copy[field]() == 't')) }
1407             );
1408
1409             // finally, if this is a different copy, redirect.
1410             // Note that we flesh first since the copy we just
1411             // fetched will be used after the redirect.
1412             if (copyId && copyId != copy.id()) {
1413                 // if a new barcode is scanned in the detail view,
1414                 // update the url to match the ID of the new copy
1415                 $location.path('/cat/item/' + copy.id() + '/' + $scope.tab);
1416                 deferred.reject(); // avoid propagation of data fetch calls
1417                 return;
1418             }
1419             copyId = copy.id();
1420
1421             deferred.resolve();
1422         });
1423
1424         return deferred.promise;
1425     }
1426
1427     // if loadPrev load the two most recent circulations
1428     function loadCurrentCirc(loadPrev) {
1429         delete $scope.circ;
1430         delete $scope.circ_summary;
1431         delete $scope.prev_circ_summary;
1432         delete $scope.prev_circ_usr;
1433         if (!copyId) return;
1434         
1435         egCore.pcrud.search('circ', 
1436             {target_copy : copyId},
1437             {   flesh : 2,
1438                 flesh_fields : {
1439                     circ : [
1440                         'usr',
1441                         'workstation',                                         
1442                         'checkin_workstation',                                 
1443                         'duration_rule',                                       
1444                         'max_fine_rule',                                       
1445                         'recurring_fine_rule'   
1446                     ],
1447                     au : ['card']
1448                 },
1449                 order_by : {circ : 'xact_start desc'}, 
1450                 limit :  1
1451             }
1452
1453         ).then(null, null, function(circ) {
1454             $scope.circ = circ;
1455
1456             // load the chain for this circ
1457             egCore.net.request(
1458                 'open-ils.circ',
1459                 'open-ils.circ.renewal_chain.retrieve_by_circ.summary',
1460                 egCore.auth.token(), $scope.circ.id()
1461             ).then(function(summary) {
1462                 $scope.circ_summary = summary.summary;
1463             });
1464
1465             if (!loadPrev) return;
1466
1467             // load the chain for the previous circ, plus the user
1468             egCore.net.request(
1469                 'open-ils.circ',
1470                 'open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary',
1471                 egCore.auth.token(), $scope.circ.id()
1472
1473             ).then(null, null, function(summary) {
1474                 $scope.prev_circ_summary = summary.summary;
1475
1476                 if (summary.usr) { // aged circs have no 'usr'.
1477                     egCore.pcrud.retrieve('au', summary.usr,
1478                         {flesh : 1, flesh_fields : {au : ['card']}})
1479
1480                     .then(function(user) { $scope.prev_circ_usr = user });
1481                 }
1482             });
1483         });
1484     }
1485
1486     var maxHistory;
1487     function fetchMaxCircHistory() {
1488         if (maxHistory) return $q.when(maxHistory);
1489         return egCore.org.settings(
1490             'circ.item_checkout_history.max')
1491         .then(function(set) {
1492             maxHistory = set['circ.item_checkout_history.max'] || 4;
1493             return maxHistory;
1494         });
1495     }
1496
1497     $scope.addBilling = function(circ) {
1498         egBilling.showBillDialog({
1499             xact_id : circ.id(),
1500             patron : circ.usr()
1501         });
1502     }
1503
1504     $scope.retrieveAllPatrons = function() {
1505         var users = new Set();
1506         angular.forEach($scope.circ_list.map(function(circ) { return circ.usr(); }),function(usr) {
1507             // aged circs have no 'usr'.
1508             if (usr) users.add(usr);
1509         });
1510         users.forEach(function(usr) {
1511             $timeout(function() {
1512                 var url = $location.absUrl().replace(
1513                     /\/cat\/.*/,
1514                     '/circ/patron/' + usr.id() + '/checkout');
1515                 $window.open(url, '_blank')
1516             });
1517         });
1518     }
1519
1520     function loadCircHistory() {
1521         $scope.circ_list = [];
1522
1523         var copy_org = 
1524             itemSvc.copy.call_number().id() == -1 ?
1525             itemSvc.copy.circ_lib().id() :
1526             itemSvc.copy.call_number().owning_lib().id()
1527
1528         // there is an extra layer of permissibility over circ
1529         // history views
1530         egCore.perm.hasPermAt('VIEW_COPY_CHECKOUT_HISTORY', true)
1531         .then(function(orgIds) {
1532
1533             if (orgIds.indexOf(copy_org) == -1) {
1534                 console.log('User is not allowed to view circ history');
1535                 return $q.when(0);
1536             }
1537
1538             return fetchMaxCircHistory();
1539
1540         }).then(function(count) {
1541
1542             egCore.pcrud.search('combcirc', 
1543                 {target_copy : copyId},
1544                 {   flesh : 2,
1545                     flesh_fields : {
1546                         combcirc : [
1547                             'usr',
1548                             'workstation',                                         
1549                             'checkin_workstation',                                 
1550                             'recurring_fine_rule'   
1551                         ],
1552                         au : ['card']
1553                     },
1554                     order_by : {combcirc : 'xact_start desc'}, 
1555                     limit :  count
1556                 }
1557
1558             ).then(null, null, function(circ) {
1559
1560                 // flesh circ_lib locally
1561                 circ.circ_lib(egCore.org.get(circ.circ_lib()));
1562                 circ.checkin_lib(egCore.org.get(circ.checkin_lib()));
1563                 $scope.circ_list.push(circ);
1564             });
1565         });
1566     }
1567
1568
1569     function loadCircCounts() {
1570
1571         delete $scope.circ_counts;
1572         $scope.total_circs = 0;
1573         $scope.total_circs_this_year = 0;
1574         $scope.total_circs_prev_year = 0;
1575         if (!copyId) return;
1576
1577         egCore.pcrud.search('circbyyr', 
1578             {copy : copyId}, null, {atomic : true})
1579
1580         .then(function(counts) {
1581             $scope.circ_counts = counts;
1582
1583             angular.forEach(counts, function(count) {
1584                 $scope.total_circs += Number(count.count());
1585             });
1586
1587             var this_year = counts.filter(function(c) {
1588                 return c.year() == new Date().getFullYear();
1589             });
1590
1591             $scope.total_circs_this_year = 
1592                 this_year.length ? this_year[0].count() : 0;
1593
1594             var prev_year = counts.filter(function(c) {
1595                 return c.year() == new Date().getFullYear() - 1;
1596             });
1597
1598             $scope.total_circs_prev_year = 
1599                 prev_year.length ? prev_year[0].count() : 0;
1600
1601         });
1602     }
1603
1604     function loadHolds() {
1605         delete $scope.hold;
1606         if (!copyId) return;
1607
1608         egCore.pcrud.search('ahr', 
1609             {   current_copy : copyId, 
1610                 cancel_time : null, 
1611                 fulfillment_time : null,
1612                 capture_time : {'<>' : null}
1613             }, {
1614                 flesh : 2,
1615                 flesh_fields : {
1616                     ahr : ['requestor', 'usr'],
1617                     au  : ['card']
1618                 }
1619             }
1620         ).then(null, null, function(hold) {
1621             $scope.hold = hold;
1622             hold.pickup_lib(egCore.org.get(hold.pickup_lib()));
1623             if (hold.current_shelf_lib()) {
1624                 hold.current_shelf_lib(
1625                     egCore.org.get(hold.current_shelf_lib()));
1626             }
1627             hold.behind_desk(Boolean(hold.behind_desk() == 't'));
1628         });
1629     }
1630
1631     function loadTransits() {
1632         delete $scope.transit;
1633         delete $scope.hold_transit;
1634         if (!copyId) return;
1635
1636         egCore.pcrud.search('atc', 
1637             {target_copy : copyId},
1638             {order_by : {atc : 'source_send_time DESC'}}
1639
1640         ).then(null, null, function(transit) {
1641             $scope.transit = transit;
1642             transit.source(egCore.org.get(transit.source()));
1643             transit.dest(egCore.org.get(transit.dest()));
1644         })
1645     }
1646
1647
1648     // we don't need all data on all tabs, so fetch what's needed when needed.
1649     function loadTabData() {
1650         switch($scope.tab) {
1651             case 'summary':
1652                 loadCurrentCirc();
1653                 loadCircCounts();
1654                 break;
1655
1656             case 'circs':
1657                 loadCurrentCirc(true);
1658                 break;
1659
1660             case 'circ_list':
1661                 loadCircHistory();
1662                 break;
1663
1664             case 'holds':
1665                 loadHolds()
1666                 loadTransits();
1667                 break;
1668
1669             case 'triggered_events':
1670                 var url = $location.absUrl().replace(/\/staff.*/, '/actor/user/event_log');
1671                 url += '?copy_id=' + encodeURIComponent(copyId);
1672                 $scope.triggered_events_url = url;
1673                 $scope.funcs = {};
1674         }
1675
1676         if ($scope.edit) {
1677             egCore.net.request(
1678                 'open-ils.actor',
1679                 'open-ils.actor.anon_cache.set_value',
1680                 null, 'edit-these-copies', {
1681                     record_id: $scope.recordId,
1682                     copies: [copyId],
1683                     hide_vols : true,
1684                     hide_copies : false
1685                 }
1686             ).then(function(key) {
1687                 if (key) {
1688                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
1689                     $window.location.href = url;
1690                 } else {
1691                     alert('Could not create anonymous cache key!');
1692                 }
1693             });
1694         }
1695
1696         return;
1697     }
1698
1699     $scope.context.toggleDisplay = function() {
1700         $location.path('/cat/item/search');
1701     }
1702
1703     // handle the barcode scan box, which will replace our current copy
1704     $scope.context.search = function(args) {
1705         loadCopy(args.barcode).then(loadTabData);
1706     }
1707
1708     $scope.context.show_triggered_events = function() {
1709         $location.path('/cat/item/' + copyId + '/triggered_events');
1710     }
1711
1712     loadCopy().then(loadTabData);
1713 }])