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