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