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