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