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