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