]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/cat/item/app.js
dba013ca790f7fcda9670d2d92f98a4092295c61
[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',
52 function(egCore) {
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     return service;
205 }])
206
207 /**
208  * Search bar along the top of the page.
209  * Parent scope for list and detail views
210  */
211 .controller('SearchCtrl', 
212        ['$scope','$location','egCore','egGridDataProvider','itemSvc',
213 function($scope , $location , egCore , egGridDataProvider , itemSvc) {
214     $scope.args = {}; // search args
215
216     // sub-scopes (search / detail-view) apply their version 
217     // of retrieval function to $scope.context.search
218     // and display toggling via $scope.context.toggleDisplay
219     $scope.context = {
220         selectBarcode : true
221     };
222
223     $scope.toggleView = function($event) {
224         $scope.context.toggleDisplay();
225         $event.preventDefault(); // avoid form submission
226     }
227 }])
228
229 /**
230  * List view - grid stuff
231  */
232 .controller('ListCtrl', 
233        ['$scope','$q','$routeParams','$location','$timeout','$window','egCore','egGridDataProvider','itemSvc','egUser','$uibModal','egCirc','egConfirmDialog',
234 function($scope , $q , $routeParams , $location , $timeout , $window , egCore , egGridDataProvider , itemSvc , egUser , $uibModal , egCirc , egConfirmDialog) {
235     var copyId = [];
236     var cp_list = $routeParams.idList;
237     if (cp_list) {
238         copyId = cp_list.split(',');
239     }
240
241     $scope.context.page = 'list';
242
243     /*
244     var provider = egGridDataProvider.instance();
245     provider.get = function(offset, count) {
246     }
247     */
248
249     $scope.gridDataProvider = egGridDataProvider.instance({
250         get : function(offset, count) {
251             //return provider.arrayNotifier(itemSvc.copies, offset, count);
252             return this.arrayNotifier(itemSvc.copies, offset, count);
253         }
254     });
255
256     // If a copy was just displayed in the detail view, ensure it's
257     // focused in the list view.
258     var selected = false;
259     var copyGrid = $scope.gridControls = {
260         itemRetrieved : function(item) {
261             if (selected || !itemSvc.copy) return;
262             if (itemSvc.copy.id() == item.id) {
263                 copyGrid.selectItems([item.index]);
264                 selected = true;
265             }
266         }
267     };
268
269     $scope.$watch('barcodesFromFile', function(newVal, oldVal) {
270         if (newVal && newVal != oldVal) {
271             $scope.args.barcode = '';
272             var barcodes = [];
273
274             angular.forEach(newVal.split(/\n/), function(line) {
275                 if (!line) return;
276                 // scrub any trailing spaces or commas from the barcode
277                 line = line.replace(/(.*?)($|\s.*|,.*)/,'$1');
278                 barcodes.push(line);
279             });
280
281             itemSvc.fetch(barcodes).then(
282                 function() {
283                     copyGrid.refresh();
284                     copyGrid.selectItems([itemSvc.copies[0].index]);
285                 }
286             );
287         }
288     });
289
290     $scope.context.search = function(args) {
291         if (!args.barcode) return;
292         $scope.context.itemNotFound = false;
293         itemSvc.fetch(args.barcode).then(function(res) {
294             if (res) {
295                 copyGrid.refresh();
296                 copyGrid.selectItems([res.index]);
297                 $scope.args.barcode = '';
298             } else {
299                 $scope.context.itemNotFound = true;
300                 egCore.audio.play('warning.item_status.itemNotFound');
301             }
302             $scope.context.selectBarcode = true;
303         })
304     }
305
306     var add_barcode_to_list = function (b) {
307         $scope.context.search({barcode:b});
308     }
309
310     $scope.context.toggleDisplay = function() {
311         var item = copyGrid.selectedItems()[0];
312         if (item) 
313             $location.path('/cat/item/' + item.id);
314     }
315
316     $scope.context.show_triggered_events = function() {
317         var item = copyGrid.selectedItems()[0];
318         if (item) 
319             $location.path('/cat/item/' + item.id + '/triggered_events');
320     }
321
322     function gatherSelectedRecordIds () {
323         var rid_list = [];
324         angular.forEach(
325             copyGrid.selectedItems(),
326             function (item) {
327                 if (rid_list.indexOf(item['call_number.record.id']) == -1)
328                     rid_list.push(item['call_number.record.id'])
329             }
330         );
331         return rid_list;
332     }
333
334     function gatherSelectedVolumeIds (rid) {
335         var cn_id_list = [];
336         angular.forEach(
337             copyGrid.selectedItems(),
338             function (item) {
339                 if (rid && item['call_number.record.id'] != rid) return;
340                 if (cn_id_list.indexOf(item['call_number.id']) == -1)
341                     cn_id_list.push(item['call_number.id'])
342             }
343         );
344         return cn_id_list;
345     }
346
347     function gatherSelectedHoldingsIds (rid) {
348         var cp_id_list = [];
349         angular.forEach(
350             copyGrid.selectedItems(),
351             function (item) {
352                 if (rid && item['call_number.record.id'] != rid) return;
353                 cp_id_list.push(item.id)
354             }
355         );
356         return cp_id_list;
357     }
358
359     $scope.add_copies_to_bucket = function() {
360         var copy_list = gatherSelectedHoldingsIds();
361         if (copy_list.length == 0) return;
362
363         return $uibModal.open({
364             templateUrl: './cat/catalog/t_add_to_bucket',
365             animation: true,
366             size: 'md',
367             controller:
368                    ['$scope','$uibModalInstance',
369             function($scope , $uibModalInstance) {
370
371                 $scope.bucket_id = 0;
372                 $scope.newBucketName = '';
373                 $scope.allBuckets = [];
374
375                 egCore.net.request(
376                     'open-ils.actor',
377                     'open-ils.actor.container.retrieve_by_class.authoritative',
378                     egCore.auth.token(), egCore.auth.user().id(),
379                     'copy', 'staff_client'
380                 ).then(function(buckets) { $scope.allBuckets = buckets; });
381
382                 $scope.add_to_bucket = function() {
383                     var promises = [];
384                     angular.forEach(copy_list, function (cp) {
385                         var item = new egCore.idl.ccbi()
386                         item.bucket($scope.bucket_id);
387                         item.target_copy(cp);
388                         promises.push(
389                             egCore.net.request(
390                                 'open-ils.actor',
391                                 'open-ils.actor.container.item.create',
392                                 egCore.auth.token(), 'copy', item
393                             )
394                         );
395
396                         return $q.all(promises).then(function() {
397                             $uibModalInstance.close();
398                         });
399                     });
400                 }
401
402                 $scope.add_to_new_bucket = function() {
403                     var bucket = new egCore.idl.ccb();
404                     bucket.owner(egCore.auth.user().id());
405                     bucket.name($scope.newBucketName);
406                     bucket.description('');
407                     bucket.btype('staff_client');
408
409                     return egCore.net.request(
410                         'open-ils.actor',
411                         'open-ils.actor.container.create',
412                         egCore.auth.token(), 'copy', bucket
413                     ).then(function(bucket) {
414                         $scope.bucket_id = bucket;
415                         $scope.add_to_bucket();
416                     });
417                 }
418
419                 $scope.cancel = function() {
420                     $uibModalInstance.dismiss();
421                 }
422             }]
423         });
424     }
425
426     $scope.need_one_selected = function() {
427         var items = $scope.gridControls.selectedItems();
428         if (items.length == 1) return false;
429         return true;
430     };
431
432     $scope.make_copies_bookable = function() {
433
434         var copies_by_record = {};
435         var record_list = [];
436         angular.forEach(
437             copyGrid.selectedItems(),
438             function (item) {
439                 var record_id = item['call_number.record.id'];
440                 if (typeof copies_by_record[ record_id ] == 'undefined') {
441                     copies_by_record[ record_id ] = [];
442                     record_list.push( record_id );
443                 }
444                 copies_by_record[ record_id ].push(item.id);
445             }
446         );
447
448         var promises = [];
449         var combined_results = [];
450         angular.forEach(record_list, function(record_id) {
451             promises.push(
452                 egCore.net.request(
453                     'open-ils.booking',
454                     'open-ils.booking.resources.create_from_copies',
455                     egCore.auth.token(),
456                     copies_by_record[record_id]
457                 ).then(function(results) {
458                     if (results && results['brsrc']) {
459                         combined_results = combined_results.concat(results['brsrc']);
460                     }
461                 })
462             );
463         });
464
465         $q.all(promises).then(function() {
466             if (combined_results.length > 0) {
467                 $uibModal.open({
468                     template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
469                     animation: true,
470                     size: 'md',
471                     controller:
472                            ['$scope','$location','egCore','$uibModalInstance',
473                     function($scope , $location , egCore , $uibModalInstance) {
474
475                         $scope.funcs = {
476                             ses : egCore.auth.token(),
477                             resultant_brsrc : combined_results.map(function(o) { return o[0]; })
478                         }
479
480                         var booking_path = '/eg/conify/global/booking/resource';
481
482                         $scope.booking_admin_url =
483                             $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
484                     }]
485                 });
486             }
487         });
488     }
489
490     $scope.book_copies_now = function() {
491         var copies_by_record = {};
492         var record_list = [];
493         angular.forEach(
494             copyGrid.selectedItems(),
495             function (item) {
496                 var record_id = item['call_number.record.id'];
497                 if (typeof copies_by_record[ record_id ] == 'undefined') {
498                     copies_by_record[ record_id ] = [];
499                     record_list.push( record_id );
500                 }
501                 copies_by_record[ record_id ].push(item.id);
502             }
503         );
504
505         var promises = [];
506         var combined_brt = [];
507         var combined_brsrc = [];
508         angular.forEach(record_list, function(record_id) {
509             promises.push(
510                 egCore.net.request(
511                     'open-ils.booking',
512                     'open-ils.booking.resources.create_from_copies',
513                     egCore.auth.token(),
514                     copies_by_record[record_id]
515                 ).then(function(results) {
516                     if (results && results['brt']) {
517                         combined_brt = combined_brt.concat(results['brt']);
518                     }
519                     if (results && results['brsrc']) {
520                         combined_brsrc = combined_brsrc.concat(results['brsrc']);
521                     }
522                 })
523             );
524         });
525
526         $q.all(promises).then(function() {
527             if (combined_brt.length > 0 || combined_brsrc.length > 0) {
528                 $uibModal.open({
529                     template: '<eg-embed-frame url="booking_admin_url" handlers="funcs"></eg-embed-frame>',
530                     animation: true,
531                     size: 'md',
532                     controller:
533                            ['$scope','$location','egCore','$uibModalInstance',
534                     function($scope , $location , egCore , $uibModalInstance) {
535
536                         $scope.funcs = {
537                             ses : egCore.auth.token(),
538                             bresv_interface_opts : {
539                                 booking_results : {
540                                      brt : combined_brt
541                                     ,brsrc : combined_brsrc
542                                 }
543                             }
544                         }
545
546                         var booking_path = '/eg/booking/reservation';
547
548                         $scope.booking_admin_url =
549                             $location.absUrl().replace(/\/eg\/staff.*/, booking_path);
550
551                     }]
552                 });
553             }
554         });
555     }
556
557     $scope.requestItems = function() {
558         var copy_list = gatherSelectedHoldingsIds();
559         if (copy_list.length == 0) return;
560
561         return $uibModal.open({
562             templateUrl: './cat/catalog/t_request_items',
563             animation: true,
564             controller:
565                    ['$scope','$uibModalInstance','egUser',
566             function($scope , $uibModalInstance , egUser) {
567                 $scope.user = null;
568                 $scope.first_user_fetch = true;
569
570                 $scope.hold_data = {
571                     hold_type : 'C',
572                     copy_list : copy_list,
573                     pickup_lib: egCore.org.get(egCore.auth.user().ws_ou()),
574                     user      : egCore.auth.user().id()
575                 };
576
577                 egUser.get( $scope.hold_data.user ).then(function(u) {
578                     $scope.user = u;
579                     $scope.barcode = u.card().barcode();
580                     $scope.user_name = egUser.format_name(u);
581                     $scope.hold_data.user = u.id();
582                 });
583
584                 $scope.user_name = '';
585                 $scope.barcode = '';
586                 $scope.$watch('barcode', function (n) {
587                     if (!$scope.first_user_fetch) {
588                         egUser.getByBarcode(n).then(function(u) {
589                             $scope.user = u;
590                             $scope.user_name = egUser.format_name(u);
591                             $scope.hold_data.user = u.id();
592                         }, function() {
593                             $scope.user = null;
594                             $scope.user_name = '';
595                             delete $scope.hold_data.user;
596                         });
597                     }
598                     $scope.first_user_fetch = false;
599                 });
600
601                 $scope.ok = function(h) {
602                     var args = {
603                         patronid  : h.user,
604                         hold_type : h.hold_type,
605                         pickup_lib: h.pickup_lib.id(),
606                         depth     : 0
607                     };
608
609                     egCore.net.request(
610                         'open-ils.circ',
611                         'open-ils.circ.holds.test_and_create.batch.override',
612                         egCore.auth.token(), args, h.copy_list
613                     );
614
615                     $uibModalInstance.close();
616                 }
617
618                 $scope.cancel = function($event) {
619                     $uibModalInstance.dismiss();
620                     $event.preventDefault();
621                 }
622             }]
623         });
624     }
625
626     $scope.replaceBarcodes = function() {
627         angular.forEach(copyGrid.selectedItems(), function (cp) {
628             $uibModal.open({
629                 templateUrl: './cat/share/t_replace_barcode',
630                 animation: true,
631                 controller:
632                            ['$scope','$uibModalInstance',
633                     function($scope , $uibModalInstance) {
634                         $scope.isModal = true;
635                         $scope.focusBarcode = false;
636                         $scope.focusBarcode2 = true;
637                         $scope.barcode1 = cp.barcode;
638
639                         $scope.updateBarcode = function() {
640                             $scope.copyNotFound = false;
641                             $scope.updateOK = false;
642
643                             egCore.pcrud.search('acp',
644                                 {deleted : 'f', barcode : $scope.barcode1})
645                             .then(function(copy) {
646
647                                 if (!copy) {
648                                     $scope.focusBarcode = true;
649                                     $scope.copyNotFound = true;
650                                     return;
651                                 }
652
653                                 $scope.copyId = copy.id();
654                                 copy.barcode($scope.barcode2);
655
656                                 egCore.pcrud.update(copy).then(function(stat) {
657                                     $scope.updateOK = stat;
658                                     $scope.focusBarcode = true;
659                                     if (stat) add_barcode_to_list(copy.barcode());
660                                 });
661
662                             });
663                             $uibModalInstance.close();
664                         }
665
666                         $scope.cancel = function($event) {
667                             $uibModalInstance.dismiss();
668                             $event.preventDefault();
669                         }
670                     }
671                 ]
672             });
673         });
674     }
675
676     $scope.attach_to_peer_bib = function() {
677         if (copyGrid.selectedItems().length == 0) return;
678
679         egCore.hatch.getItem('eg.cat.marked_conjoined_record').then(function(target_record) {
680             if (!target_record) return;
681
682             return $uibModal.open({
683                 templateUrl: './cat/catalog/t_conjoined_selector',
684                 animation: true,
685                 controller:
686                        ['$scope','$uibModalInstance',
687                 function($scope , $uibModalInstance) {
688                     $scope.update = false;
689
690                     $scope.peer_type = null;
691                     $scope.peer_type_list = [];
692
693                     get_peer_types = function() {
694                         if (egCore.env.bpt)
695                             return $q.when(egCore.env.bpt.list);
696
697                         return egCore.pcrud.retrieveAll('bpt', null, {atomic : true})
698                         .then(function(list) {
699                             egCore.env.absorbList(list, 'bpt');
700                             return list;
701                         });
702                     }
703
704                     get_peer_types().then(function(list){
705                         $scope.peer_type_list = list;
706                     });
707
708                     $scope.ok = function(type) {
709                         var promises = [];
710
711                         angular.forEach(copyGrid.selectedItems(), function (cp) {
712                             var n = new egCore.idl.bpbcm();
713                             n.isnew(true);
714                             n.peer_record(target_record);
715                             n.target_copy(cp.id);
716                             n.peer_type(type);
717                             promises.push(egCore.pcrud.create(n).then(function(){add_barcode_to_list(cp.barcode)}));
718                         });
719
720                         return $q.all(promises).then(function(){$uibModalInstance.close()});
721                     }
722
723                     $scope.cancel = function($event) {
724                         $uibModalInstance.dismiss();
725                         $event.preventDefault();
726                     }
727                 }]
728             });
729         });
730     }
731
732     $scope.selectedHoldingsCopyDelete = function () {
733         var copy_list = gatherSelectedHoldingsIds();
734         if (copy_list.length == 0) return;
735
736         var copy_objects = [];
737         egCore.pcrud.search('acp',
738             {deleted : 'f', id : copy_list},
739             { flesh : 1, flesh_fields : { acp : ['call_number'] } }
740         ).then(function(copy) {
741             copy_objects.push(copy);
742         }).then(function() {
743
744             var cnHash = {};
745             var perCnCopies = {};
746
747             var cn_count = 0;
748             var cp_count = 0;
749
750             angular.forEach(
751                 copy_objects,
752                 function (cp) {
753                     cp.isdeleted(1);
754                     cp_count++;
755                     var cn_id = cp.call_number().id();
756                     if (!cnHash[cn_id]) {
757                         cnHash[cn_id] = cp.call_number();
758                         perCnCopies[cn_id] = [cp];
759                     } else {
760                         perCnCopies[cn_id].push(cp);
761                     }
762                     cp.call_number(cn_id); // prevent loops in JSON-ification
763                 }
764             );
765
766             angular.forEach(perCnCopies, function (v, k) {
767                 cnHash[k].copies(v);
768             });
769
770             cnList = [];
771             angular.forEach(cnHash, function (v, k) {
772                 cnList.push(v);
773             });
774
775             if (cnList.length == 0) return;
776
777             var flags = {};
778
779             egConfirmDialog.open(
780                 egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES,
781                 egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES_MESSAGE,
782                 {copies : cp_count, volumes : cn_count}
783             ).result.then(function() {
784                 egCore.net.request(
785                     'open-ils.cat',
786                     'open-ils.cat.asset.volume.fleshed.batch.update.override',
787                     egCore.auth.token(), cnList, 1, flags
788                 ).then(function(){
789                     angular.forEach(copyGrid.selectedItems(), function(cp){add_barcode_to_list(cp.barcode)});
790                 });
791             });
792         });
793     }
794
795     $scope.selectedHoldingsItemStatusTgrEvt= function() {
796         var item = copyGrid.selectedItems()[0];
797         if (item)
798             $location.path('/cat/item/' + item.id + '/triggered_events');
799     }
800
801     $scope.selectedHoldingsItemStatusHolds= function() {
802         var item = copyGrid.selectedItems()[0];
803         if (item)
804             $location.path('/cat/item/' + item.id + '/holds');
805     }
806
807     $scope.cancel_transit = function () {
808         var initial_list = copyGrid.selectedItems();
809         angular.forEach(copyGrid.selectedItems(), function(cp) {
810             egCirc.find_copy_transit(null, {copy_barcode:cp.barcode})
811                 .then(function(t) { return egCirc.abort_transit(t.id())    })
812                 .then(function()  { return add_barcode_to_list(cp.barcode) });
813         });
814     }
815
816     $scope.selectedHoldingsDamaged = function () {
817         var initial_list = copyGrid.selectedItems();
818         egCirc.mark_damaged(gatherSelectedHoldingsIds()).then(function(){
819             angular.forEach(initial_list, function(cp){add_barcode_to_list(cp.barcode)});
820         });
821     }
822
823     $scope.selectedHoldingsMissing = function () {
824         var initial_list = copyGrid.selectedItems();
825         egCirc.mark_missing(gatherSelectedHoldingsIds()).then(function(){
826             angular.forEach(initial_list, function(cp){add_barcode_to_list(cp.barcode)});
827         });
828     }
829
830     $scope.checkin = function () {
831         angular.forEach(copyGrid.selectedItems(), function (cp) {
832             egCirc.checkin({copy_barcode:cp.barcode}).then(
833                 function() { add_barcode_to_list(cp.barcode) }
834             );
835         });
836     }
837
838     $scope.renew = function () {
839         angular.forEach(copyGrid.selectedItems(), function (cp) {
840             egCirc.renew({copy_barcode:cp.barcode}).then(
841                 function() { add_barcode_to_list(cp.barcode) }
842             );
843         });
844     }
845
846
847     var spawnHoldingsAdd = function (vols,copies){
848         angular.forEach(gatherSelectedRecordIds(), function (r) {
849             var raw = [];
850             if (copies) { // just a copy on existing volumes
851                 angular.forEach(gatherSelectedVolumeIds(r), function (v) {
852                     raw.push( {callnumber : v} );
853                 });
854             } else if (vols) {
855                 angular.forEach(
856                     gatherSelectedHoldingsIds(r),
857                     function (i) {
858                         angular.forEach(copyGrid.selectedItems(), function(item) {
859                             if (i == item.id) raw.push({owner : item['call_number.owning_lib']});
860                         });
861                     }
862                 );
863             }
864
865             if (raw.length == 0) raw.push({});
866
867             egCore.net.request(
868                 'open-ils.actor',
869                 'open-ils.actor.anon_cache.set_value',
870                 null, 'edit-these-copies', {
871                     record_id: r,
872                     raw: raw,
873                     hide_vols : false,
874                     hide_copies : false
875                 }
876             ).then(function(key) {
877                 if (key) {
878                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
879                     $timeout(function() { $window.open(url, '_blank') });
880                 } else {
881                     alert('Could not create anonymous cache key!');
882                 }
883             });
884         });
885     }
886     $scope.selectedHoldingsVolCopyAdd = function () { spawnHoldingsAdd(true,false) }
887     $scope.selectedHoldingsCopyAdd = function () { spawnHoldingsAdd(false,true) }
888
889     $scope.showBibHolds = function () {
890         angular.forEach(gatherSelectedRecordIds(), function (r) {
891             var url = egCore.env.basePath + 'cat/catalog/record/' + r + '/holds';
892             $timeout(function() { $window.open(url, '_blank') });
893         });
894     }
895
896     var spawnHoldingsEdit = function (hide_vols,hide_copies){
897         angular.forEach(gatherSelectedRecordIds(), function (r) {
898             egCore.net.request(
899                 'open-ils.actor',
900                 'open-ils.actor.anon_cache.set_value',
901                 null, 'edit-these-copies', {
902                     record_id: r,
903                     copies: gatherSelectedHoldingsIds(r),
904                     raw: {},
905                     hide_vols : hide_vols,
906                     hide_copies : hide_copies
907                 }
908             ).then(function(key) {
909                 if (key) {
910                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
911                     $timeout(function() { $window.open(url, '_blank') });
912                 } else {
913                     alert('Could not create anonymous cache key!');
914                 }
915             });
916         });
917     }
918     $scope.selectedHoldingsVolCopyEdit = function () { spawnHoldingsEdit(false,false) }
919     $scope.selectedHoldingsVolEdit = function () { spawnHoldingsEdit(false,true) }
920     $scope.selectedHoldingsCopyEdit = function () { spawnHoldingsEdit(true,false) }
921
922     // this "transfers" selected copies to a new owning library,
923     // auto-creating volumes and deleting unused volumes as required.
924     $scope.changeItemOwningLib = function() {
925         var xfer_target = egCore.hatch.getLocalItem('eg.cat.volume_transfer_target');
926         var items = copyGrid.selectedItems();
927         if (!xfer_target || !items.length) {
928             return;
929         }
930         var vols_to_move   = {};
931         var copies_to_move = {};
932         angular.forEach(items, function(item) {
933             if (item['call_number.owning_lib'] != xfer_target) {
934                 if (item['call_number.id'] in vols_to_move) {
935                     copies_to_move[item['call_number.id']].push(item.id);
936                 } else {
937                     vols_to_move[item['call_number.id']] = {
938                         label       : item['call_number.label'],
939                         label_class : item['call_number.label_class'],
940                         record      : item['call_number.record.id'],
941                         prefix      : item['call_number.prefix.id'],
942                         suffix      : item['call_number.suffix.id']
943                     };
944                     copies_to_move[item['call_number.id']] = new Array;
945                     copies_to_move[item['call_number.id']].push(item.id);
946                 }
947             }
948         });
949
950         var promises = [];
951         angular.forEach(vols_to_move, function(vol) {
952             promises.push(egCore.net.request(
953                 'open-ils.cat',
954                 'open-ils.cat.call_number.find_or_create',
955                 egCore.auth.token(),
956                 vol.label,
957                 vol.record,
958                 xfer_target,
959                 vol.prefix,
960                 vol.suffix,
961                 vol.label_class
962             ).then(function(resp) {
963                 var evt = egCore.evt.parse(resp);
964                 if (evt) return;
965                 return egCore.net.request(
966                     'open-ils.cat',
967                     'open-ils.cat.transfer_copies_to_volume',
968                     egCore.auth.token(),
969                     resp.acn_id,
970                     copies_to_move[vol.id]
971                 );
972             }));
973         });
974
975         angular.forEach(
976             copyGrid.selectedItems(),
977             function(cp){
978                 promises.push(
979                     function(){ add_barcode_to_list(cp.barcode) }
980                 )
981             }
982         );
983         $q.all(promises);
984     }
985
986     $scope.transferItems = function (){
987         var xfer_target = egCore.hatch.getLocalItem('eg.cat.item_transfer_target');
988         var copy_ids = gatherSelectedHoldingsIds();
989         if (xfer_target && copy_ids.length > 0) {
990             egCore.net.request(
991                 'open-ils.cat',
992                 'open-ils.cat.transfer_copies_to_volume',
993                 egCore.auth.token(),
994                 xfer_target,
995                 copy_ids
996             ).then(
997                 function(resp) { // oncomplete
998                     var evt = egCore.evt.parse(resp);
999                     egConfirmDialog.open(
1000                         egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_TITLE,
1001                         egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_BODY,
1002                         {'evt_desc': evt.desc}
1003                     ).result.then(function() {
1004                         egCore.net.request(
1005                             'open-ils.cat',
1006                             'open-ils.cat.transfer_copies_to_volume.override',
1007                             egCore.auth.token(),
1008                             xfer_target,
1009                             copy_ids,
1010                             { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
1011                         );
1012                     });
1013                 },
1014                 null, // onerror
1015                 null // onprogress
1016             ).then(function() {
1017                     angular.forEach(copyGrid.selectedItems(), function(cp){add_barcode_to_list(cp.barcode)});
1018             });
1019         }
1020     }
1021
1022     $scope.print_list = function() {
1023         var print_data = { copies : copyGrid.allItems() };
1024
1025         if (print_data.copies.length == 0) return $q.when();
1026
1027         return egCore.print.print({
1028             template : 'item_status',
1029             scope : print_data
1030         });
1031     }
1032
1033     if (copyId.length > 0) {
1034         itemSvc.fetch(null,copyId).then(
1035             function() {
1036                 copyGrid.refresh();
1037             }
1038         );
1039     }
1040
1041 }])
1042
1043 /**
1044  * Detail view -- shows one copy
1045  */
1046 .controller('ViewCtrl', 
1047        ['$scope','$q','$location','$routeParams','$timeout','$window','egCore','itemSvc','egBilling',
1048 function($scope , $q , $location , $routeParams , $timeout , $window , egCore , itemSvc , egBilling) {
1049     var copyId = $routeParams.id;
1050     $scope.tab = $routeParams.tab || 'summary';
1051     $scope.context.page = 'detail';
1052     $scope.summaryRecord = null;
1053
1054     $scope.edit = false;
1055     if ($scope.tab == 'edit') {
1056         $scope.tab = 'summary';
1057         $scope.edit = true;
1058     }
1059
1060
1061     // use the cached record info
1062     if (itemSvc.copy)
1063         $scope.recordId = itemSvc.copy.call_number().record().id();
1064
1065     function loadCopy(barcode) {
1066         $scope.context.itemNotFound = false;
1067
1068         // Avoid re-fetching the same copy while jumping tabs.
1069         // In addition to being quicker, this helps to avoid flickering
1070         // of the top panel which is always visible in the detail view.
1071         //
1072         // 'barcode' represents the loading of a new item - refetch it
1073         // regardless of whether it matches the current item.
1074         if (!barcode && itemSvc.copy && itemSvc.copy.id() == copyId) {
1075             $scope.copy = itemSvc.copy;
1076             $scope.recordId = itemSvc.copy.call_number().record().id();
1077             return $q.when();
1078         }
1079
1080         delete $scope.copy;
1081         delete itemSvc.copy;
1082
1083         var deferred = $q.defer();
1084         itemSvc.fetch(barcode, copyId, true).then(function(res) {
1085             $scope.context.selectBarcode = true;
1086
1087             if (!res) {
1088                 copyId = null;
1089                 $scope.context.itemNotFound = true;
1090                 egCore.audio.play('warning.item_status.itemNotFound');
1091                 deferred.reject(); // avoid propagation of data fetch calls
1092                 return;
1093             }
1094
1095             var copy = res.copy;
1096             itemSvc.copy = copy;
1097
1098
1099             $scope.copy = copy;
1100             $scope.recordId = copy.call_number().record().id();
1101             $scope.args.barcode = '';
1102
1103             // locally flesh org units
1104             copy.circ_lib(egCore.org.get(copy.circ_lib()));
1105             copy.call_number().owning_lib(
1106                 egCore.org.get(copy.call_number().owning_lib()));
1107
1108             var r = copy.call_number().record();
1109             if (r.owner()) r.owner(egCore.org.get(r.owner())); 
1110
1111             // make boolean for auto-magic true/false display
1112             angular.forEach(
1113                 ['ref','opac_visible','holdable','circulate'],
1114                 function(field) { copy[field](Boolean(copy[field]() == 't')) }
1115             );
1116
1117             // finally, if this is a different copy, redirect.
1118             // Note that we flesh first since the copy we just
1119             // fetched will be used after the redirect.
1120             if (copyId && copyId != copy.id()) {
1121                 // if a new barcode is scanned in the detail view,
1122                 // update the url to match the ID of the new copy
1123                 $location.path('/cat/item/' + copy.id() + '/' + $scope.tab);
1124                 deferred.reject(); // avoid propagation of data fetch calls
1125                 return;
1126             }
1127             copyId = copy.id();
1128
1129             deferred.resolve();
1130         });
1131
1132         return deferred.promise;
1133     }
1134
1135     // if loadPrev load the two most recent circulations
1136     function loadCurrentCirc(loadPrev) {
1137         delete $scope.circ;
1138         delete $scope.circ_summary;
1139         delete $scope.prev_circ_summary;
1140         delete $scope.prev_circ_usr;
1141         if (!copyId) return;
1142         
1143         egCore.pcrud.search('circ', 
1144             {target_copy : copyId},
1145             {   flesh : 2,
1146                 flesh_fields : {
1147                     circ : [
1148                         'usr',
1149                         'workstation',                                         
1150                         'checkin_workstation',                                 
1151                         'duration_rule',                                       
1152                         'max_fine_rule',                                       
1153                         'recurring_fine_rule'   
1154                     ],
1155                     au : ['card']
1156                 },
1157                 order_by : {circ : 'xact_start desc'}, 
1158                 limit :  1
1159             }
1160
1161         ).then(null, null, function(circ) {
1162             $scope.circ = circ;
1163
1164             // load the chain for this circ
1165             egCore.net.request(
1166                 'open-ils.circ',
1167                 'open-ils.circ.renewal_chain.retrieve_by_circ.summary',
1168                 egCore.auth.token(), $scope.circ.id()
1169             ).then(function(summary) {
1170                 $scope.circ_summary = summary.summary;
1171             });
1172
1173             if (!loadPrev) return;
1174
1175             // load the chain for the previous circ, plus the user
1176             egCore.net.request(
1177                 'open-ils.circ',
1178                 'open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary',
1179                 egCore.auth.token(), $scope.circ.id()
1180
1181             ).then(null, null, function(summary) {
1182                 $scope.prev_circ_summary = summary.summary;
1183
1184                 if (summary.usr) { // aged circs have no 'usr'.
1185                     egCore.pcrud.retrieve('au', summary.usr,
1186                         {flesh : 1, flesh_fields : {au : ['card']}})
1187
1188                     .then(function(user) { $scope.prev_circ_usr = user });
1189                 }
1190             });
1191         });
1192     }
1193
1194     var maxHistory;
1195     function fetchMaxCircHistory() {
1196         if (maxHistory) return $q.when(maxHistory);
1197         return egCore.org.settings(
1198             'circ.item_checkout_history.max')
1199         .then(function(set) {
1200             maxHistory = set['circ.item_checkout_history.max'] || 4;
1201             return maxHistory;
1202         });
1203     }
1204
1205     $scope.addBilling = function(circ) {
1206         egBilling.showBillDialog({
1207             xact_id : circ.id(),
1208             patron : circ.usr()
1209         });
1210     }
1211
1212     $scope.retrieveAllPatrons = function() {
1213         var users = new Set();
1214         angular.forEach($scope.circ_list.map(function(circ) { return circ.usr(); }),function(usr) {
1215             // aged circs have no 'usr'.
1216             if (usr) users.add(usr);
1217         });
1218         users.forEach(function(usr) {
1219             $timeout(function() {
1220                 var url = $location.absUrl().replace(
1221                     /\/cat\/.*/,
1222                     '/circ/patron/' + usr.id() + '/checkout');
1223                 $window.open(url, '_blank')
1224             });
1225         });
1226     }
1227
1228     function loadCircHistory() {
1229         $scope.circ_list = [];
1230
1231         var copy_org = 
1232             itemSvc.copy.call_number().id() == -1 ?
1233             itemSvc.copy.circ_lib().id() :
1234             itemSvc.copy.call_number().owning_lib().id()
1235
1236         // there is an extra layer of permissibility over circ
1237         // history views
1238         egCore.perm.hasPermAt('VIEW_COPY_CHECKOUT_HISTORY', true)
1239         .then(function(orgIds) {
1240
1241             if (orgIds.indexOf(copy_org) == -1) {
1242                 console.log('User is not allowed to view circ history');
1243                 return $q.when(0);
1244             }
1245
1246             return fetchMaxCircHistory();
1247
1248         }).then(function(count) {
1249
1250             egCore.pcrud.search('combcirc', 
1251                 {target_copy : copyId},
1252                 {   flesh : 2,
1253                     flesh_fields : {
1254                         combcirc : [
1255                             'usr',
1256                             'workstation',                                         
1257                             'checkin_workstation',                                 
1258                             'recurring_fine_rule'   
1259                         ],
1260                         au : ['card']
1261                     },
1262                     order_by : {combcirc : 'xact_start desc'}, 
1263                     limit :  count
1264                 }
1265
1266             ).then(null, null, function(circ) {
1267
1268                 // flesh circ_lib locally
1269                 circ.circ_lib(egCore.org.get(circ.circ_lib()));
1270                 circ.checkin_lib(egCore.org.get(circ.checkin_lib()));
1271                 $scope.circ_list.push(circ);
1272             });
1273         });
1274     }
1275
1276
1277     function loadCircCounts() {
1278
1279         delete $scope.circ_counts;
1280         $scope.total_circs = 0;
1281         $scope.total_circs_this_year = 0;
1282         $scope.total_circs_prev_year = 0;
1283         if (!copyId) return;
1284
1285         egCore.pcrud.search('circbyyr', 
1286             {copy : copyId}, null, {atomic : true})
1287
1288         .then(function(counts) {
1289             $scope.circ_counts = counts;
1290
1291             angular.forEach(counts, function(count) {
1292                 $scope.total_circs += Number(count.count());
1293             });
1294
1295             var this_year = counts.filter(function(c) {
1296                 return c.year() == new Date().getFullYear();
1297             });
1298
1299             $scope.total_circs_this_year = 
1300                 this_year.length ? this_year[0].count() : 0;
1301
1302             var prev_year = counts.filter(function(c) {
1303                 return c.year() == new Date().getFullYear() - 1;
1304             });
1305
1306             $scope.total_circs_prev_year = 
1307                 prev_year.length ? prev_year[0].count() : 0;
1308
1309         });
1310     }
1311
1312     function loadHolds() {
1313         delete $scope.hold;
1314         if (!copyId) return;
1315
1316         egCore.pcrud.search('ahr', 
1317             {   current_copy : copyId, 
1318                 cancel_time : null, 
1319                 fulfillment_time : null,
1320                 capture_time : {'<>' : null}
1321             }, {
1322                 flesh : 2,
1323                 flesh_fields : {
1324                     ahr : ['requestor', 'usr'],
1325                     au  : ['card']
1326                 }
1327             }
1328         ).then(null, null, function(hold) {
1329             $scope.hold = hold;
1330             hold.pickup_lib(egCore.org.get(hold.pickup_lib()));
1331             if (hold.current_shelf_lib()) {
1332                 hold.current_shelf_lib(
1333                     egCore.org.get(hold.current_shelf_lib()));
1334             }
1335             hold.behind_desk(Boolean(hold.behind_desk() == 't'));
1336         });
1337     }
1338
1339     function loadTransits() {
1340         delete $scope.transit;
1341         delete $scope.hold_transit;
1342         if (!copyId) return;
1343
1344         egCore.pcrud.search('atc', 
1345             {target_copy : copyId},
1346             {order_by : {atc : 'source_send_time DESC'}}
1347
1348         ).then(null, null, function(transit) {
1349             $scope.transit = transit;
1350             transit.source(egCore.org.get(transit.source()));
1351             transit.dest(egCore.org.get(transit.dest()));
1352         })
1353     }
1354
1355
1356     // we don't need all data on all tabs, so fetch what's needed when needed.
1357     function loadTabData() {
1358         switch($scope.tab) {
1359             case 'summary':
1360                 loadCurrentCirc();
1361                 loadCircCounts();
1362                 break;
1363
1364             case 'circs':
1365                 loadCurrentCirc(true);
1366                 break;
1367
1368             case 'circ_list':
1369                 loadCircHistory();
1370                 break;
1371
1372             case 'holds':
1373                 loadHolds()
1374                 loadTransits();
1375                 break;
1376
1377             case 'triggered_events':
1378                 var url = $location.absUrl().replace(/\/staff.*/, '/actor/user/event_log');
1379                 url += '?copy_id=' + encodeURIComponent(copyId);
1380                 $scope.triggered_events_url = url;
1381                 $scope.funcs = {};
1382         }
1383
1384         if ($scope.edit) {
1385             egCore.net.request(
1386                 'open-ils.actor',
1387                 'open-ils.actor.anon_cache.set_value',
1388                 null, 'edit-these-copies', {
1389                     record_id: $scope.recordId,
1390                     copies: [copyId],
1391                     hide_vols : true,
1392                     hide_copies : false
1393                 }
1394             ).then(function(key) {
1395                 if (key) {
1396                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
1397                     $window.location.href = url;
1398                 } else {
1399                     alert('Could not create anonymous cache key!');
1400                 }
1401             });
1402         }
1403
1404         return;
1405     }
1406
1407     $scope.context.toggleDisplay = function() {
1408         $location.path('/cat/item/search');
1409     }
1410
1411     // handle the barcode scan box, which will replace our current copy
1412     $scope.context.search = function(args) {
1413         loadCopy(args.barcode).then(loadTabData);
1414     }
1415
1416     $scope.context.show_triggered_events = function() {
1417         $location.path('/cat/item/' + copyId + '/triggered_events');
1418     }
1419
1420     loadCopy().then(loadTabData);
1421 }])