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