LP1739288: report back on invalid barcodes in Item Status screen
[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('/eg/staff/cat/catalog/record/' + $scope.args.recordId + '/catalog', '_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('/eg/staff/cat/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) {
416         if (!args.barcode) return;
417         $scope.context.itemNotFound = false;
418         itemSvc.fetch(args.barcode).then(function(res) {
419             if (res) {
420                 copyGrid.refresh();
421                 copyGrid.selectItems([res.index]);
422                 $scope.args.barcode = '';
423             } else {
424                 $scope.context.itemNotFound = true;
425                 egCore.audio.play('warning.item_status.itemNotFound');
426             }
427             $scope.context.selectBarcode = true;
428         })
429     }
430
431     var add_barcode_to_list = function (b) {
432         //console.log('listCtrl: add_barcode_to_list',b);
433         $scope.context.search({barcode:b});
434     }
435     itemSvc.add_barcode_to_list = add_barcode_to_list;
436
437     $scope.context.toggleDisplay = function() {
438         var item = copyGrid.selectedItems()[0];
439         if (item) 
440             $location.path('/cat/item/' + item.id);
441     }
442
443     $scope.context.show_triggered_events = function() {
444         var item = copyGrid.selectedItems()[0];
445         if (item) 
446             $location.path('/cat/item/' + item.id + '/triggered_events');
447     }
448
449     function gatherSelectedRecordIds () {
450         var rid_list = [];
451         angular.forEach(
452             copyGrid.selectedItems(),
453             function (item) {
454                 if (rid_list.indexOf(item['call_number.record.id']) == -1)
455                     rid_list.push(item['call_number.record.id'])
456             }
457         );
458         return rid_list;
459     }
460
461     function gatherSelectedVolumeIds (rid) {
462         var cn_id_list = [];
463         angular.forEach(
464             copyGrid.selectedItems(),
465             function (item) {
466                 if (rid && item['call_number.record.id'] != rid) return;
467                 if (cn_id_list.indexOf(item['call_number.id']) == -1)
468                     cn_id_list.push(item['call_number.id'])
469             }
470         );
471         return cn_id_list;
472     }
473
474     function gatherSelectedHoldingsIds (rid) {
475         var cp_id_list = [];
476         angular.forEach(
477             copyGrid.selectedItems(),
478             function (item) {
479                 if (rid && item['call_number.record.id'] != rid) return;
480                 cp_id_list.push(item.id)
481             }
482         );
483         return cp_id_list;
484     }
485
486     function gatherSelectedHoldingsRecords() {
487         var record_id_list = [];
488         angular.forEach(
489             copyGrid.selectedItems(),
490             function (item) {
491                 record_id_list.push(item['call_number.record.id']);
492             }
493         )
494         return record_id_list;
495     }
496
497     $scope.refreshGridData = function() {
498         var chain = $q.when();
499         var all_items = itemSvc.copies.map(function(item) {
500             return item.id;
501         });
502         angular.forEach(all_items.reverse(), function(i) {
503             itemSvc.copies.shift();
504             chain = chain.then(function() {
505                 return itemSvc.fetch(null, i);
506             });
507         });
508         return chain.then(function() {
509             copyGrid.refresh();
510         });
511     }
512
513
514     $scope.add_copies_to_bucket = function() {
515         var copy_list = gatherSelectedHoldingsIds();
516         itemSvc.add_copies_to_bucket(copy_list);
517     }
518
519     $scope.add_records_to_bucket = function() {
520         var record_list = gatherSelectedHoldingsRecords();
521         itemSvc.add_copies_to_bucket(record_list, 'biblio');
522     }
523
524     $scope.locateAcquisition = function() {
525         if (gatherSelectedHoldingsIds) {
526             var cp_list = gatherSelectedHoldingsIds();
527             if (cp_list) {
528                 if (cp_list.length > 0) {
529                     $scope.openAcquisitionLineItem(cp_list);
530                 }
531             }
532         }
533     }
534
535     $scope.update_inventory = function() {
536         var copy_list = gatherSelectedHoldingsIds();
537         itemSvc.updateInventory(copy_list, $scope.gridControls.allItems()).then(function(res) {
538             if (res) {
539                 $scope.gridControls.allItems(res);
540                 ngToast.create(egCore.strings.SUCCESS_UPDATE_INVENTORY);
541             } else {
542                 ngToast.warning(egCore.strings.FAIL_UPDATE_INVENTORY);
543             }
544         });
545     }
546
547     $scope.need_one_selected = function() {
548         var items = $scope.gridControls.selectedItems();
549         if (items.length == 1) return false;
550         return true;
551     };
552
553     $scope.make_copies_bookable = function() {
554         itemSvc.make_copies_bookable(copyGrid.selectedItems());
555     }
556
557     $scope.book_copies_now = function() {
558         var item = copyGrid.selectedItems()[0];
559         if (item)
560             itemSvc.book_copies_now(item.barcode);
561     }
562
563     $scope.manage_reservations = function() {
564         var item = copyGrid.selectedItems()[0];
565         if (item)
566             itemSvc.manage_reservations(item.barcode);
567     }
568
569     $scope.requestItems = function() {
570         var copy_list = gatherSelectedHoldingsIds();
571         var record_list = gatherSelectedRecordIds();
572         itemSvc.requestItems(copy_list,record_list);
573     }
574
575     $scope.replaceBarcodes = function() {
576         itemSvc.replaceBarcodes(copyGrid.selectedItems());
577     }
578
579     $scope.attach_to_peer_bib = function() {
580         itemSvc.attach_to_peer_bib(copyGrid.selectedItems());
581     }
582
583     $scope.selectedHoldingsCopyDelete = function () {
584         itemSvc.selectedHoldingsCopyDelete(copyGrid.selectedItems());
585     }
586
587     $scope.selectedHoldingsItemStatusTgrEvt= function() {
588         var item = copyGrid.selectedItems()[0];
589         if (item)
590             $location.path('/cat/item/' + item.id + '/triggered_events');
591     }
592
593     $scope.selectedHoldingsItemStatusHolds= function() {
594         var item = copyGrid.selectedItems()[0];
595         if (item)
596             $location.path('/cat/item/' + item.id + '/holds');
597     }
598
599     $scope.cancel_transit = function () {
600         itemSvc.cancel_transit(copyGrid.selectedItems());
601     }
602
603     $scope.selectedHoldingsDamaged = function () {
604         itemSvc.selectedHoldingsDamaged(copyGrid.selectedItems());
605     }
606
607     $scope.selectedHoldingsDiscard = function () {
608         itemSvc.selectedHoldingsDiscard(copyGrid.selectedItems());
609     }
610
611     $scope.selectedHoldingsMissing = function () {
612         itemSvc.selectedHoldingsMissing(copyGrid.selectedItems());
613     }
614
615     $scope.checkin = function () {
616         itemSvc.checkin(copyGrid.selectedItems());
617     }
618
619     $scope.renew = function () {
620         itemSvc.renew(copyGrid.selectedItems());
621     }
622
623     $scope.selectedHoldingsVolCopyAdd = function () {
624         itemSvc.spawnHoldingsAdd(copyGrid.selectedItems(),true,false);
625     }
626     $scope.selectedHoldingsCopyAdd = function () {
627         itemSvc.spawnHoldingsAdd(copyGrid.selectedItems(),false,true);
628     }
629
630     $scope.selectedHoldingsCopyAlertsAdd = function(items) {
631         var copy_ids = [];
632         angular.forEach(items, function(item) {
633             if (item.id) copy_ids.push(item.id);
634         });
635         egCirc.add_copy_alerts(copy_ids).then(function() {
636             // update grid items?
637         });
638     }
639
640     $scope.selectedHoldingsCopyAlertsEdit = function(items) {
641         var copy_ids = [];
642         angular.forEach(items, function(item) {
643             if (item.id) copy_ids.push(item.id);
644         });
645         egCirc.manage_copy_alerts(copy_ids).then(function() {
646             // update grid items?
647         });
648     }
649
650     $scope.gridCellHandlers = {};
651     $scope.gridCellHandlers.copyAlertsEdit = function(id) {
652         egCirc.manage_copy_alerts([id]).then(function() {
653             // update grid items?
654         });
655     };
656
657     $scope.showBibHolds = function () {
658         angular.forEach(gatherSelectedRecordIds(), function (r) {
659             var url = egCore.env.basePath + 'cat/catalog/record/' + r + '/holds';
660             $timeout(function() { $window.open(url, '_blank') });
661         });
662     }
663
664     $scope.selectedHoldingsVolCopyEdit = function () {
665         itemSvc.spawnHoldingsEdit(copyGrid.selectedItems(),false,false);
666     }
667     $scope.selectedHoldingsVolEdit = function () {
668         itemSvc.spawnHoldingsEdit(copyGrid.selectedItems(),false,true);
669     }
670     $scope.selectedHoldingsCopyEdit = function () {
671         itemSvc.spawnHoldingsEdit(copyGrid.selectedItems(),true,false);
672     }
673
674     $scope.changeItemOwningLib = function() {
675         itemSvc.changeItemOwningLib(copyGrid.selectedItems());
676     }
677
678     $scope.transferItems = function (){
679         itemSvc.transferItems(copyGrid.selectedItems());
680     }
681
682     $scope.print_labels = function() {
683         egCore.net.request(
684             'open-ils.actor',
685             'open-ils.actor.anon_cache.set_value',
686             null, 'print-labels-these-copies', {
687                 copies : gatherSelectedHoldingsIds()
688             }
689         ).then(function(key) {
690             if (key) {
691                 var url = egCore.env.basePath + 'cat/printlabels/' + key;
692                 $timeout(function() { $window.open(url, '_blank') });
693             } else {
694                 alert('Could not create anonymous cache key!');
695             }
696         });
697     }
698
699     $scope.print_list = function() {
700         var print_data = { copies : copyGrid.allItems() };
701
702         if (print_data.copies.length == 0) return $q.when();
703
704         return egCore.print.print({
705             template : 'item_status',
706             scope : print_data
707         });
708     }
709
710     $scope.show_in_catalog = function(){
711         itemSvc.show_in_catalog(copyGrid.selectedItems());
712     }
713
714     if (copyId.length > 0) {
715         var fetch_list = [];
716         angular.forEach(copyId, function (c) {
717             fetch_list.push(itemSvc.fetch(null,c));
718         });
719
720         return $q.all(fetch_list).then(function (res) { copyGrid.refresh(); });
721     }
722
723     $scope.statusIconColumn = {
724         isEnabled: true,
725         template:  function(item) {
726             var icon = '';
727             if (modified_items.has(item['id'])) {
728                 icon = '<span class="glyphicon glyphicon-floppy-saved"' +
729                     'title="' + egCore.strings.ITEM_SUCCESSFULLY_MODIFIED + '" ' +
730                     'aria-label="' + egCore.strings.ITEM_SUCCESSFULLY_MODIFIED + '">' +
731                     '</span>';
732             }
733             return icon
734         }
735     }
736
737     if (typeof BroadcastChannel != 'undefined') {
738         var holdings_bChannel = new BroadcastChannel("eg.holdings.update");
739         holdings_bChannel.onmessage = function(e) {
740             angular.forEach(e.data.copies, function(i) {
741                 modified_items.add(i);
742             });
743             ngToast.create(egCore.strings.ITEMS_SUCCESSFULLY_MODIFIED);
744             $scope.refreshGridData();
745         }
746         $scope.$on('$destroy', function() {
747             holdings_bChannel.close();
748         });
749     }
750
751 }])
752
753 /**
754  * Detail view -- shows one copy
755  */
756 .controller('ViewCtrl', 
757        ['$scope','$q','$location','$routeParams','$timeout','$window','egCore','egItem','egBilling','egCirc',
758 function($scope , $q , $location , $routeParams , $timeout , $window , egCore , itemSvc , egBilling , egCirc) {
759     var copyId = $routeParams.id;
760     $scope.args.copyId = copyId;
761     $scope.tab = $routeParams.tab || 'summary';
762     $scope.context.page = 'detail';
763     $scope.summaryRecord = null;
764
765     $scope.edit = false;
766     if ($scope.tab == 'edit') {
767         $scope.tab = 'summary';
768         $scope.edit = true;
769     }
770
771
772     // use the cached record info
773     if (itemSvc.copy) {
774         $scope.copy_alert_count = itemSvc.copy.copy_alerts().filter(function(aca) {
775             return !aca.ack_time();
776         }).length;
777         $scope.recordId = itemSvc.copy.call_number().record().id();
778         $scope.args.recordId = $scope.recordId;
779         $scope.args.cnId = itemSvc.copy.call_number().id();
780         $scope.args.cnOwningLib = itemSvc.copy.call_number().owning_lib();
781         $scope.args.cnLabel = itemSvc.copy.call_number().label();
782         $scope.args.cnLabelClass = itemSvc.copy.call_number().label_class();
783         $scope.args.cnPrefixId = itemSvc.copy.call_number().prefix().id();
784         $scope.args.cnSuffixId = itemSvc.copy.call_number().suffix().id();
785         $scope.args.copyBarcode = itemSvc.copy.barcode();
786     }
787
788     function loadCopy(barcode) {
789         $scope.context.itemNotFound = false;
790
791         // Avoid re-fetching the same copy while jumping tabs.
792         // In addition to being quicker, this helps to avoid flickering
793         // of the top panel which is always visible in the detail view.
794         //
795         // 'barcode' represents the loading of a new item - refetch it
796         // regardless of whether it matches the current item.
797         if (!barcode && itemSvc.copy && itemSvc.copy.id() == copyId) {
798             $scope.copy = itemSvc.copy;
799             if (itemSvc.latest_inventory && itemSvc.latest_inventory.copy() == copyId) {
800                 $scope.latest_inventory = itemSvc.latest_inventory;
801             }
802             $scope.copy_alert_count = itemSvc.copy.copy_alerts().filter(function(aca) {
803                 return !aca.ack_time();
804             }).length;
805             $scope.recordId = itemSvc.copy.call_number().record().id();
806             $scope.args.recordId = $scope.recordId;
807             $scope.args.cnId = itemSvc.copy.call_number().id();
808             $scope.args.cnOwningLib = itemSvc.copy.call_number().owning_lib();
809             $scope.args.cnLabel = itemSvc.copy.call_number().label();
810             $scope.args.cnLabelClass = itemSvc.copy.call_number().label_class();
811             $scope.args.cnPrefixId = itemSvc.copy.call_number().prefix().id();
812             $scope.args.cnSuffixId = itemSvc.copy.call_number().suffix().id();
813             $scope.args.copyBarcode = itemSvc.copy.barcode();
814             return $q.when();
815         }
816
817         delete $scope.copy;
818         delete itemSvc.copy;
819
820         var deferred = $q.defer();
821         itemSvc.fetch(barcode, copyId, true).then(function(res) {
822             $scope.context.selectBarcode = true;
823
824             if (!res) {
825                 copyId = null;
826                 $scope.context.itemNotFound = true;
827                 egCore.audio.play('warning.item_status.itemNotFound');
828                 deferred.reject(); // avoid propagation of data fetch calls
829                 return;
830             }
831
832             var copy = res.copy;
833             itemSvc.copy = copy;
834             if (res.latest_inventory) itemSvc.latest_inventory = res.latest_inventory;
835
836
837             $scope.copy = copy;
838             $scope.latest_inventory = res.latest_inventory;
839             $scope.copy_alert_count = copy.copy_alerts().filter(function(aca) {
840                 return !aca.ack_time();
841             }).length;
842 console.debug($scope.copy_alert_count);
843             $scope.recordId = copy.call_number().record().id();
844             $scope.args.recordId = $scope.recordId;
845             $scope.args.cnId = itemSvc.copy.call_number().id();
846             $scope.args.cnOwningLib = itemSvc.copy.call_number().owning_lib();
847             $scope.args.cnLabel = itemSvc.copy.call_number().label();
848             $scope.args.cnLabelClass = itemSvc.copy.call_number().label_class();
849             $scope.args.cnPrefixId = itemSvc.copy.call_number().prefix().id();
850             $scope.args.cnSuffixId = itemSvc.copy.call_number().suffix().id();
851             $scope.args.copyBarcode = copy.barcode();
852             $scope.args.barcode = '';
853
854             // locally flesh org units
855             copy.circ_lib(egCore.org.get(copy.circ_lib()));
856             copy.call_number().owning_lib(
857                 egCore.org.get(copy.call_number().owning_lib()));
858
859             var r = copy.call_number().record();
860             if (r.owner()) r.owner(egCore.org.get(r.owner())); 
861
862             // make boolean for auto-magic true/false display
863             angular.forEach(
864                 ['ref','opac_visible','holdable','circulate'],
865                 function(field) { copy[field](Boolean(copy[field]() == 't')) }
866             );
867
868             // finally, if this is a different copy, redirect.
869             // Note that we flesh first since the copy we just
870             // fetched will be used after the redirect.
871             if (copyId && copyId != copy.id()) {
872                 // if a new barcode is scanned in the detail view,
873                 // update the url to match the ID of the new copy
874                 $location.path('/cat/item/' + copy.id() + '/' + $scope.tab);
875                 deferred.reject(); // avoid propagation of data fetch calls
876                 return;
877             }
878             copyId = copy.id();
879
880             deferred.resolve();
881         });
882
883         return deferred.promise;
884     }
885
886     // load the two most recent circulations in /circs tab
887     function loadCurrentCirc() {
888         delete $scope.circ;
889         delete $scope.circ_summary;
890         delete $scope.prev_circ_summary;
891         delete $scope.prev_circ_usr;
892         if (!copyId) return;
893         
894         var copy_org =
895             itemSvc.copy.call_number().id() == -1 ?
896             itemSvc.copy.circ_lib().id() :
897             itemSvc.copy.call_number().owning_lib().id();
898
899         // since a user can still view patron checkout history here, check perms
900         egCore.perm.hasPermAt('VIEW_COPY_CHECKOUT_HISTORY', true)
901         .then(function(orgIds){
902             if(orgIds.indexOf(copy_org) == -1){
903                 console.warn('User is not allowed to view circ history!');
904                 $q.when(0);
905             }
906
907             return fetchMaxCircHistory();
908         })
909         .then(function(maxHistCount){
910
911             if (!maxHistCount) $scope.isMaxCircHistoryZero = true;
912
913             egCore.pcrud.search('aacs',
914                 {target_copy : copyId},
915                 {   flesh : 2,
916                     flesh_fields : {
917                         aacs : [
918                             'usr',
919                             'workstation',
920                             'checkin_workstation',
921                             'duration_rule',
922                             'max_fine_rule',
923                             'recurring_fine_rule'
924                         ],
925                         au : ['card']
926                     },
927                     order_by : {aacs : 'xact_start desc'},
928                     limit :  1
929                 }
930
931             ).then(null, null, function(circ) {
932                 $scope.circ = circ;
933
934                 if (!circ) return $q.when();
935
936                 // load the chain for this circ
937                 egCore.net.request(
938                     'open-ils.circ',
939                     'open-ils.circ.renewal_chain.retrieve_by_circ.summary',
940                     egCore.auth.token(), $scope.circ.id()
941                 ).then(function(summary) {
942                     $scope.circ_summary = summary;
943                 });
944
945                 if (maxHistCount <= 1) return;
946
947                 // load the chain for the previous circ, plus the user
948                 egCore.net.request(
949                     'open-ils.circ',
950                     'open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary',
951                     egCore.auth.token(), $scope.circ.id()
952
953                 ).then(null, null, function(summary) {
954                     $scope.prev_circ_summary = summary.summary;
955
956                     if (summary.usr) { // aged circs have no 'usr'.
957                         egCore.pcrud.retrieve('au', summary.usr,
958                             {flesh : 1, flesh_fields : {au : ['card']}})
959
960                         .then(function(user) { $scope.prev_circ_usr = user });
961                     }
962                 });
963             });
964         })
965     }
966
967     var maxHistory;
968     function fetchMaxCircHistory() {
969         if (maxHistory) return $q.when(maxHistory);
970         return egCore.org.settings(
971             'circ.item_checkout_history.max')
972         .then(function(set) {
973             maxHistory = set['circ.item_checkout_history.max'] || 4;
974             return Number(maxHistory);
975         });
976     }
977
978     $scope.addBilling = function(circ) {
979         egBilling.showBillDialog({
980             xact_id : circ.id(),
981             patron : circ.usr()
982         });
983     }
984
985     $scope.retrieveAllPatrons = function() {
986         var users = new Set();
987         angular.forEach($scope.circ_list.map(function(circ) { return circ.usr(); }),function(usr) {
988             // aged circs have no 'usr'.
989             if (usr) users.add(usr);
990         });
991         users.forEach(function(usr) {
992             $timeout(function() {
993                 var url = $location.absUrl().replace(
994                     /\/cat\/.*/,
995                     '/circ/patron/' + usr.id() + '/checkout');
996                 $window.open(url, '_blank')
997             });
998         });
999     }
1000
1001     // load data for /circ_list tab
1002     function loadCircHistory() {
1003         $scope.circ_list = [];
1004
1005         var copy_org = 
1006             itemSvc.copy.call_number().id() == -1 ?
1007             itemSvc.copy.circ_lib().id() :
1008             itemSvc.copy.call_number().owning_lib().id();
1009
1010         // there is an extra layer of permissibility over circ
1011         // history views
1012         egCore.perm.hasPermAt('VIEW_COPY_CHECKOUT_HISTORY', true)
1013         .then(function(orgIds) {
1014
1015             if (orgIds.indexOf(copy_org) == -1) {
1016                 console.log('User is not allowed to view circ history');
1017                 return $q.when(0);
1018             }
1019
1020             return fetchMaxCircHistory();
1021
1022         }).then(function(maxHistCount) {
1023
1024             if(!maxHistCount) $scope.isMaxCircHistoryZero = true;
1025
1026             egCore.pcrud.search('aacs',
1027                 {target_copy : copyId},
1028                 {   flesh : 2,
1029                     flesh_fields : {
1030                         aacs : [
1031                             'usr',
1032                             'workstation',
1033                             'checkin_workstation',
1034                             'recurring_fine_rule'
1035                         ],
1036                         au : ['card']
1037                     },
1038                     order_by : {aacs : 'xact_start desc'},
1039                     // fetch at least one to see if copy ever circulated
1040                     limit : $scope.isMaxCircHistoryZero ? 1 : maxHistCount
1041                 }
1042
1043             ).then(null, null, function(circ) {
1044
1045                 $scope.circ = circ;
1046
1047                 // flesh circ_lib locally
1048                 circ.circ_lib(egCore.org.get(circ.circ_lib()));
1049                 circ.checkin_lib(egCore.org.get(circ.checkin_lib()));
1050                 $scope.circ_list.push(circ);
1051             });
1052         });
1053     }
1054
1055
1056     function loadCircCounts() {
1057
1058         delete $scope.circ_counts;
1059         $scope.total_circs = 0;
1060         $scope.total_circs_this_year = 0;
1061         $scope.total_circs_prev_year = 0;
1062         if (!copyId) return;
1063
1064         egCore.pcrud.search('circbyyr', 
1065             {copy : copyId}, null, {atomic : true})
1066
1067         .then(function(counts) {
1068             $scope.circ_counts = counts;
1069
1070             angular.forEach(counts, function(count) {
1071                 $scope.total_circs += Number(count.count());
1072             });
1073
1074             var this_year = counts.filter(function(c) {
1075                 return c.year() == new Date().getFullYear();
1076             });
1077
1078             $scope.total_circs_this_year = (function() {
1079                 total = 0;
1080                 if (this_year.length == 2) {
1081                     total = (Number(this_year[0].count()) + Number(this_year[1].count()));
1082                 } else if (this_year.length == 1) {
1083                     total = Number(this_year[0].count());
1084                 }
1085                 return total;
1086             })();
1087
1088             var prev_year = counts.filter(function(c) {
1089                 return c.year() == new Date().getFullYear() - 1;
1090             });
1091
1092             $scope.total_circs_prev_year = (function() {
1093                 total = 0;
1094                 if (prev_year.length == 2) {
1095                     total = (Number(prev_year[0].count()) + Number(prev_year[1].count()));
1096                 } else if (prev_year.length == 1) {
1097                     total = Number(prev_year[0].count());
1098                 }
1099                 return total;
1100             })();
1101
1102         });
1103     }
1104
1105     function loadHolds() {
1106         delete $scope.hold;
1107         if (!copyId) return;
1108
1109         egCore.pcrud.search('ahr', 
1110             {   current_copy : copyId, 
1111                 cancel_time : null, 
1112                 fulfillment_time : null,
1113                 capture_time : {'<>' : null}
1114             }, {
1115                 flesh : 2,
1116                 flesh_fields : {
1117                     ahr : ['requestor', 'usr'],
1118                     au  : ['card']
1119                 }
1120             }
1121         ).then(null, null, function(hold) {
1122             $scope.hold = hold;
1123             hold.pickup_lib(egCore.org.get(hold.pickup_lib()));
1124             if (hold.current_shelf_lib()) {
1125                 hold.current_shelf_lib(
1126                     egCore.org.get(hold.current_shelf_lib()));
1127             }
1128             hold.behind_desk(Boolean(hold.behind_desk() == 't'));
1129         });
1130     }
1131
1132     function loadMostRecentTransit() {
1133         delete $scope.transit;
1134         delete $scope.hold_transit;
1135         if (!copyId) return;
1136
1137         egCore.pcrud.search('atc', 
1138             {target_copy : copyId},
1139             {
1140                 order_by : {atc : 'source_send_time DESC'},
1141                 limit : 1
1142             }
1143
1144         ).then(null, null, function(transit) {
1145             // use progress callback since we'll get up to one result
1146             $scope.transit = transit;
1147             transit.source(egCore.org.get(transit.source()));
1148             transit.dest(egCore.org.get(transit.dest()));
1149         })
1150     }
1151
1152
1153     // we don't need all data on all tabs, so fetch what's needed when needed.
1154     function loadTabData() {
1155         switch($scope.tab) {
1156             case 'summary':
1157                 loadCurrentCirc();
1158                 loadCircCounts();
1159                 break;
1160
1161             case 'circs':
1162                 loadCurrentCirc();
1163                 break;
1164
1165             case 'circ_list':
1166                 loadCircHistory();
1167                 break;
1168
1169             case 'holds':
1170                 loadHolds()
1171                 loadMostRecentTransit();
1172                 break;
1173
1174             case 'triggered_events':
1175                 var url = $location.absUrl().replace(/\/staff.*/, '/actor/user/event_log');
1176                 url += '?copy_id=' + encodeURIComponent(copyId);
1177                 $scope.triggered_events_url = url;
1178                 $scope.funcs = {};
1179         }
1180
1181         if ($scope.edit) {
1182             egCore.net.request(
1183                 'open-ils.actor',
1184                 'open-ils.actor.anon_cache.set_value',
1185                 null, 'edit-these-copies', {
1186                     record_id: $scope.recordId,
1187                     copies: [copyId],
1188                     hide_vols : true,
1189                     hide_copies : false
1190                 }
1191             ).then(function(key) {
1192                 if (key) {
1193                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
1194                     $window.location.href = url;
1195                 } else {
1196                     alert('Could not create anonymous cache key!');
1197                 }
1198             });
1199         }
1200
1201         return;
1202     }
1203
1204     $scope.addCopyAlerts = function(copy_id) {
1205         egCirc.add_copy_alerts([copy_id]).then(function() {
1206             // force a refresh
1207             loadCopy($scope.copy.barcode()).then(loadTabData);
1208         });
1209     }
1210     $scope.manageCopyAlerts = function(copy_id) {
1211         egCirc.manage_copy_alerts([copy_id]).then(function() {
1212             // force a refresh
1213             loadCopy($scope.copy.barcode()).then(loadTabData);
1214         });
1215     }
1216
1217     $scope.context.toggleDisplay = function() {
1218         $location.path('/cat/item/search');
1219     }
1220
1221     // handle the barcode scan box, which will replace our current copy
1222     $scope.context.search = function(args) {
1223         loadCopy(args.barcode).then(loadTabData);
1224     }
1225
1226     $scope.context.show_triggered_events = function() {
1227         $location.path('/cat/item/' + copyId + '/triggered_events');
1228     }
1229
1230     loadCopy().then(loadTabData);
1231 }])