]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/cat/item/app.js
LP1819367 - Allow paste of list of barcodes in csv format
[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','$location','$routeParams','$timeout','$window','egCore','egItem','egBilling','egCirc',
781 function($scope , $q , $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
788     $scope.edit = false;
789     if ($scope.tab == 'edit') {
790         $scope.tab = 'summary';
791         $scope.edit = true;
792     }
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     $scope.addBilling = function(circ) {
1002         egBilling.showBillDialog({
1003             xact_id : circ.id(),
1004             patron : circ.usr()
1005         });
1006     }
1007
1008     $scope.retrieveAllPatrons = function() {
1009         var users = new Set();
1010         angular.forEach($scope.circ_list.map(function(circ) { return circ.usr(); }),function(usr) {
1011             // aged circs have no 'usr'.
1012             if (usr) users.add(usr);
1013         });
1014         users.forEach(function(usr) {
1015             $timeout(function() {
1016                 var url = $location.absUrl().replace(
1017                     /\/cat\/.*/,
1018                     '/circ/patron/' + usr.id() + '/checkout');
1019                 $window.open(url, '_blank')
1020             });
1021         });
1022     }
1023
1024     // load data for /circ_list tab
1025     function loadCircHistory() {
1026         $scope.circ_list = [];
1027
1028         var copy_org = 
1029             itemSvc.copy.call_number().id() == -1 ?
1030             itemSvc.copy.circ_lib().id() :
1031             itemSvc.copy.call_number().owning_lib().id();
1032
1033         // there is an extra layer of permissibility over circ
1034         // history views
1035         egCore.perm.hasPermAt('VIEW_COPY_CHECKOUT_HISTORY', true)
1036         .then(function(orgIds) {
1037
1038             if (orgIds.indexOf(copy_org) == -1) {
1039                 console.log('User is not allowed to view circ history');
1040                 return $q.when(0);
1041             }
1042
1043             return fetchMaxCircHistory();
1044
1045         }).then(function(maxHistCount) {
1046
1047             if(!maxHistCount) $scope.isMaxCircHistoryZero = true;
1048
1049             egCore.pcrud.search('aacs',
1050                 {target_copy : copyId},
1051                 {   flesh : 2,
1052                     flesh_fields : {
1053                         aacs : [
1054                             'usr',
1055                             'workstation',
1056                             'checkin_workstation',
1057                             'recurring_fine_rule'
1058                         ],
1059                         au : ['card']
1060                     },
1061                     order_by : {aacs : 'xact_start desc'},
1062                     // fetch at least one to see if copy ever circulated
1063                     limit : $scope.isMaxCircHistoryZero ? 1 : maxHistCount
1064                 }
1065
1066             ).then(null, null, function(circ) {
1067
1068                 $scope.circ = circ;
1069
1070                 // flesh circ_lib locally
1071                 circ.circ_lib(egCore.org.get(circ.circ_lib()));
1072                 circ.checkin_lib(egCore.org.get(circ.checkin_lib()));
1073                 $scope.circ_list.push(circ);
1074             });
1075         });
1076     }
1077
1078
1079     function loadCircCounts() {
1080
1081         delete $scope.circ_counts;
1082         $scope.total_circs = 0;
1083         $scope.total_circs_this_year = 0;
1084         $scope.total_circs_prev_year = 0;
1085         if (!copyId) return;
1086
1087         egCore.pcrud.search('circbyyr', 
1088             {copy : copyId}, null, {atomic : true})
1089
1090         .then(function(counts) {
1091             $scope.circ_counts = counts;
1092
1093             angular.forEach(counts, function(count) {
1094                 $scope.total_circs += Number(count.count());
1095             });
1096
1097             var this_year = counts.filter(function(c) {
1098                 return c.year() == new Date().getFullYear();
1099             });
1100
1101             $scope.total_circs_this_year = (function() {
1102                 total = 0;
1103                 if (this_year.length == 2) {
1104                     total = (Number(this_year[0].count()) + Number(this_year[1].count()));
1105                 } else if (this_year.length == 1) {
1106                     total = Number(this_year[0].count());
1107                 }
1108                 return total;
1109             })();
1110
1111             var prev_year = counts.filter(function(c) {
1112                 return c.year() == new Date().getFullYear() - 1;
1113             });
1114
1115             $scope.total_circs_prev_year = (function() {
1116                 total = 0;
1117                 if (prev_year.length == 2) {
1118                     total = (Number(prev_year[0].count()) + Number(prev_year[1].count()));
1119                 } else if (prev_year.length == 1) {
1120                     total = Number(prev_year[0].count());
1121                 }
1122                 return total;
1123             })();
1124
1125         });
1126     }
1127
1128     function loadHolds() {
1129         delete $scope.hold;
1130         if (!copyId) return;
1131
1132         egCore.pcrud.search('ahr', 
1133             {   current_copy : copyId, 
1134                 cancel_time : null, 
1135                 fulfillment_time : null,
1136                 capture_time : {'<>' : null}
1137             }, {
1138                 flesh : 2,
1139                 flesh_fields : {
1140                     ahr : ['requestor', 'usr'],
1141                     au  : ['card']
1142                 }
1143             }
1144         ).then(null, null, function(hold) {
1145             $scope.hold = hold;
1146             hold.pickup_lib(egCore.org.get(hold.pickup_lib()));
1147             if (hold.current_shelf_lib()) {
1148                 hold.current_shelf_lib(
1149                     egCore.org.get(hold.current_shelf_lib()));
1150             }
1151             hold.behind_desk(Boolean(hold.behind_desk() == 't'));
1152         });
1153     }
1154
1155     function loadMostRecentTransit() {
1156         delete $scope.transit;
1157         delete $scope.hold_transit;
1158         if (!copyId) return;
1159
1160         egCore.pcrud.search('atc', 
1161             {target_copy : copyId},
1162             {
1163                 order_by : {atc : 'source_send_time DESC'},
1164                 limit : 1
1165             }
1166
1167         ).then(null, null, function(transit) {
1168             // use progress callback since we'll get up to one result
1169             $scope.transit = transit;
1170             transit.source(egCore.org.get(transit.source()));
1171             transit.dest(egCore.org.get(transit.dest()));
1172         })
1173     }
1174
1175
1176     // we don't need all data on all tabs, so fetch what's needed when needed.
1177     function loadTabData() {
1178         switch($scope.tab) {
1179             case 'summary':
1180                 loadCurrentCirc();
1181                 loadCircCounts();
1182                 break;
1183
1184             case 'circs':
1185                 loadCurrentCirc();
1186                 break;
1187
1188             case 'circ_list':
1189                 loadCircHistory();
1190                 break;
1191
1192             case 'holds':
1193                 loadHolds()
1194                 loadMostRecentTransit();
1195                 break;
1196
1197             case 'triggered_events':
1198                 var url = $location.absUrl().replace(/\/staff.*/, '/actor/user/event_log');
1199                 url += '?copy_id=' + encodeURIComponent(copyId);
1200                 $scope.triggered_events_url = url;
1201                 $scope.funcs = {};
1202         }
1203
1204         if ($scope.edit) {
1205             egCore.net.request(
1206                 'open-ils.actor',
1207                 'open-ils.actor.anon_cache.set_value',
1208                 null, 'edit-these-copies', {
1209                     record_id: $scope.recordId,
1210                     copies: [copyId],
1211                     hide_vols : true,
1212                     hide_copies : false
1213                 }
1214             ).then(function(key) {
1215                 if (key) {
1216                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
1217                     $window.location.href = url;
1218                 } else {
1219                     alert('Could not create anonymous cache key!');
1220                 }
1221             });
1222         }
1223
1224         return;
1225     }
1226
1227     $scope.addCopyAlerts = function(copy_id) {
1228         egCirc.add_copy_alerts([copy_id]).then(function() {
1229             // force a refresh
1230             loadCopy($scope.copy.barcode()).then(loadTabData);
1231         });
1232     }
1233     $scope.manageCopyAlerts = function(copy_id) {
1234         egCirc.manage_copy_alerts([copy_id]).then(function() {
1235             // force a refresh
1236             loadCopy($scope.copy.barcode()).then(loadTabData);
1237         });
1238     }
1239
1240     $scope.context.toggleDisplay = function() {
1241         $location.path('/cat/item/search');
1242     }
1243
1244     // handle the barcode scan box, which will replace our current copy
1245     $scope.context.search = function(args) {
1246         loadCopy(args.barcode).then(loadTabData);
1247     }
1248
1249     $scope.context.show_triggered_events = function() {
1250         $location.path('/cat/item/' + copyId + '/triggered_events');
1251     }
1252
1253     loadCopy().then(loadTabData);
1254 }])