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