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