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