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