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