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