]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/cat/item/app.js
LP#1570091: webstaff: adding item status actions
[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?|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 .factory('itemSvc', 
51        ['egCore',
52 function(egCore) {
53
54     var service = {
55         copies : [], // copy barcode search results
56         index : 0 // search grid index
57     };
58
59     service.flesh = {   
60         flesh : 3, 
61         flesh_fields : {
62             acp : ['call_number','location','status','location','floating'],
63             acn : ['record','prefix','suffix'],
64             bre : ['simple_record','creator','editor']
65         },
66         select : { 
67             // avoid fleshing MARC on the bre
68             // note: don't add simple_record.. not sure why
69             bre : ['id','tcn_value','creator','editor'],
70         } 
71     }
72
73     // resolved with the last received copy
74     service.fetch = function(barcode, id, noListDupes) {
75         var promise;
76
77         if (barcode) {
78             promise = egCore.pcrud.search('acp', 
79                 {barcode : barcode, deleted : 'f'}, service.flesh);
80         } else {
81             promise = egCore.pcrud.retrieve('acp', id, service.flesh);
82         }
83
84         var lastRes;
85         return promise.then(
86             function() {return lastRes},
87             null, // error
88
89             // notify reads the stream of copies, one at a time.
90             function(copy) {
91
92                 var flatCopy;
93                 if (noListDupes) {
94                     // use the existing copy if possible
95                     flatCopy = service.copies.filter(
96                         function(c) {return c.id == copy.id()})[0];
97                 }
98
99                 if (!flatCopy) {
100                     flatCopy = egCore.idl.toHash(copy, true);
101                     flatCopy.index = service.index++;
102                     service.copies.unshift(flatCopy);
103                 }
104
105                 return lastRes = {
106                     copy : copy, 
107                     index : flatCopy.index
108                 }
109             }
110         );
111     }
112
113     return service;
114 }])
115
116 /**
117  * Search bar along the top of the page.
118  * Parent scope for list and detail views
119  */
120 .controller('SearchCtrl', 
121        ['$scope','$location','egCore','egGridDataProvider','itemSvc',
122 function($scope , $location , egCore , egGridDataProvider , itemSvc) {
123     $scope.args = {}; // search args
124
125     // sub-scopes (search / detail-view) apply their version 
126     // of retrieval function to $scope.context.search
127     // and display toggling via $scope.context.toggleDisplay
128     $scope.context = {
129         selectBarcode : true
130     };
131
132     $scope.toggleView = function($event) {
133         $scope.context.toggleDisplay();
134         $event.preventDefault(); // avoid form submission
135     }
136 }])
137
138 /**
139  * List view - grid stuff
140  */
141 .controller('ListCtrl', 
142        ['$scope','$q','$routeParams','$location','$timeout','$window','egCore','egGridDataProvider','itemSvc','egUser','$modal','egCirc','egConfirmDialog',
143 function($scope , $q , $routeParams , $location , $timeout , $window , egCore , egGridDataProvider , itemSvc , egUser , $modal , egCirc , egConfirmDialog) {
144     var copyId = [];
145     var cp_list = $routeParams.idList;
146     if (cp_list) {
147         copyId = cp_list.split(',');
148     }
149
150     $scope.context.page = 'list';
151
152     /*
153     var provider = egGridDataProvider.instance();
154     provider.get = function(offset, count) {
155     }
156     */
157
158     $scope.gridDataProvider = egGridDataProvider.instance({
159         get : function(offset, count) {
160             //return provider.arrayNotifier(itemSvc.copies, offset, count);
161             return this.arrayNotifier(itemSvc.copies, offset, count);
162         }
163     });
164
165     // If a copy was just displayed in the detail view, ensure it's
166     // focused in the list view.
167     var selected = false;
168     var copyGrid = $scope.gridControls = {
169         itemRetrieved : function(item) {
170             if (selected || !itemSvc.copy) return;
171             if (itemSvc.copy.id() == item.id) {
172                 copyGrid.selectItems([item.index]);
173                 selected = true;
174             }
175         }
176     };
177
178     $scope.$watch('barcodesFromFile', function(newVal, oldVal) {
179         if (newVal && newVal != oldVal) {
180             $scope.args.barcode = '';
181             var barcodes = [];
182
183             angular.forEach(newVal.split(/\n/), function(line) {
184                 if (!line) return;
185                 // scrub any trailing spaces or commas from the barcode
186                 line = line.replace(/(.*?)($|\s.*|,.*)/,'$1');
187                 barcodes.push(line);
188             });
189
190             itemSvc.fetch(barcodes).then(
191                 function() {
192                     copyGrid.refresh();
193                     copyGrid.selectItems([itemSvc.copies[0].index]);
194                 }
195             );
196         }
197     });
198
199     $scope.context.search = function(args) {
200         if (!args.barcode) return;
201         $scope.context.itemNotFound = false;
202         itemSvc.fetch(args.barcode).then(function(res) {
203             if (res) {
204                 copyGrid.refresh();
205                 copyGrid.selectItems([res.index]);
206                 $scope.args.barcode = '';
207             } else {
208                 $scope.context.itemNotFound = true;
209             }
210             $scope.context.selectBarcode = true;
211         })
212     }
213
214     var add_barcode_to_list = function (b) {
215         $scope.context.search({barcode:b});
216     }
217
218     $scope.context.toggleDisplay = function() {
219         var item = copyGrid.selectedItems()[0];
220         if (item) 
221             $location.path('/cat/item/' + item.id);
222     }
223
224     $scope.context.show_triggered_events = function() {
225         var item = copyGrid.selectedItems()[0];
226         if (item) 
227             $location.path('/cat/item/' + item.id + '/triggered_events');
228     }
229
230     function gatherSelectedRecordIds () {
231         var rid_list = [];
232         angular.forEach(
233             copyGrid.selectedItems(),
234             function (item) {
235                 if (rid_list.indexOf(item['call_number.record.id']) == -1)
236                     rid_list.push(item['call_number.record.id'])
237             }
238         );
239         return rid_list;
240     }
241
242     function gatherSelectedVolumeIds (rid) {
243         var cn_id_list = [];
244         angular.forEach(
245             copyGrid.selectedItems(),
246             function (item) {
247                 if (rid && item['call_number.record.id'] != rid) return;
248                 if (cn_id_list.indexOf(item['call_number.id']) == -1)
249                     cn_id_list.push(item['call_number.id'])
250             }
251         );
252         return cn_id_list;
253     }
254
255     function gatherSelectedHoldingsIds (rid) {
256         var cp_id_list = [];
257         angular.forEach(
258             copyGrid.selectedItems(),
259             function (item) {
260                 if (rid && item['call_number.record.id'] != rid) return;
261                 cp_id_list.push(item.id)
262             }
263         );
264         return cp_id_list;
265     }
266
267     $scope.add_copies_to_bucket = function() {
268         var copy_list = gatherSelectedHoldingsIds();
269         if (copy_list.length == 0) return;
270
271         return $modal.open({
272             templateUrl: './cat/catalog/t_add_to_bucket',
273             animation: true,
274             size: 'md',
275             controller:
276                    ['$scope','$modalInstance',
277             function($scope , $modalInstance) {
278
279                 $scope.bucket_id = 0;
280                 $scope.newBucketName = '';
281                 $scope.allBuckets = [];
282
283                 egCore.net.request(
284                     'open-ils.actor',
285                     'open-ils.actor.container.retrieve_by_class.authoritative',
286                     egCore.auth.token(), egCore.auth.user().id(),
287                     'copy', 'staff_client'
288                 ).then(function(buckets) { $scope.allBuckets = buckets; });
289
290                 $scope.add_to_bucket = function() {
291                     var promises = [];
292                     angular.forEach(copy_list, function (cp) {
293                         var item = new egCore.idl.ccbi()
294                         item.bucket($scope.bucket_id);
295                         item.target_copy(cp);
296                         promises.push(
297                             egCore.net.request(
298                                 'open-ils.actor',
299                                 'open-ils.actor.container.item.create',
300                                 egCore.auth.token(), 'copy', item
301                             )
302                         );
303
304                         return $q.all(promises).then(function() {
305                             $modalInstance.close();
306                         });
307                     });
308                 }
309
310                 $scope.add_to_new_bucket = function() {
311                     var bucket = new egCore.idl.ccb();
312                     bucket.owner(egCore.auth.user().id());
313                     bucket.name($scope.newBucketName);
314                     bucket.description('');
315                     bucket.btype('staff_client');
316
317                     return egCore.net.request(
318                         'open-ils.actor',
319                         'open-ils.actor.container.create',
320                         egCore.auth.token(), 'copy', bucket
321                     ).then(function(bucket) {
322                         $scope.bucket_id = bucket;
323                         $scope.add_to_bucket();
324                     });
325                 }
326
327                 $scope.cancel = function() {
328                     $modalInstance.dismiss();
329                 }
330             }]
331         });
332     }
333
334     $scope.requestItems = function() {
335         var copy_list = gatherSelectedHoldingsIds();
336         if (copy_list.length == 0) return;
337
338         return $modal.open({
339             templateUrl: './cat/catalog/t_request_items',
340             animation: true,
341             controller:
342                    ['$scope','$modalInstance','egUser',
343             function($scope , $modalInstance , egUser) {
344                 $scope.user = null;
345                 $scope.first_user_fetch = true;
346
347                 $scope.hold_data = {
348                     hold_type : 'C',
349                     copy_list : copy_list,
350                     pickup_lib: egCore.org.get(egCore.auth.user().ws_ou()),
351                     user      : egCore.auth.user().id()
352                 };
353
354                 egUser.get( $scope.hold_data.user ).then(function(u) {
355                     $scope.user = u;
356                     $scope.barcode = u.card().barcode();
357                     $scope.user_name = egUser.format_name(u);
358                     $scope.hold_data.user = u.id();
359                 });
360
361                 $scope.user_name = '';
362                 $scope.barcode = '';
363                 $scope.$watch('barcode', function (n) {
364                     if (!$scope.first_user_fetch) {
365                         egUser.getByBarcode(n).then(function(u) {
366                             $scope.user = u;
367                             $scope.user_name = egUser.format_name(u);
368                             $scope.hold_data.user = u.id();
369                         }, function() {
370                             $scope.user = null;
371                             $scope.user_name = '';
372                             delete $scope.hold_data.user;
373                         });
374                     }
375                     $scope.first_user_fetch = false;
376                 });
377
378                 $scope.ok = function(h) {
379                     var args = {
380                         patronid  : h.user,
381                         hold_type : h.hold_type,
382                         pickup_lib: h.pickup_lib.id(),
383                         depth     : 0
384                     };
385
386                     egCore.net.request(
387                         'open-ils.circ',
388                         'open-ils.circ.holds.test_and_create.batch.override',
389                         egCore.auth.token(), args, h.copy_list
390                     );
391
392                     $modalInstance.close();
393                 }
394
395                 $scope.cancel = function($event) {
396                     $modalInstance.dismiss();
397                     $event.preventDefault();
398                 }
399             }]
400         });
401     }
402
403     $scope.replaceBarcodes = function() {
404         angular.forEach(copyGrid.selectedItems(), function (cp) {
405             $modal.open({
406                 templateUrl: './cat/share/t_replace_barcode',
407                 animation: true,
408                 controller:
409                            ['$scope','$modalInstance',
410                     function($scope , $modalInstance) {
411                         $scope.isModal = true;
412                         $scope.focusBarcode = false;
413                         $scope.focusBarcode2 = true;
414                         $scope.barcode1 = cp.barcode;
415
416                         $scope.updateBarcode = function() {
417                             $scope.copyNotFound = false;
418                             $scope.updateOK = false;
419
420                             egCore.pcrud.search('acp',
421                                 {deleted : 'f', barcode : $scope.barcode1})
422                             .then(function(copy) {
423
424                                 if (!copy) {
425                                     $scope.focusBarcode = true;
426                                     $scope.copyNotFound = true;
427                                     return;
428                                 }
429
430                                 $scope.copyId = copy.id();
431                                 copy.barcode($scope.barcode2);
432
433                                 egCore.pcrud.update(copy).then(function(stat) {
434                                     $scope.updateOK = stat;
435                                     $scope.focusBarcode = true;
436                                     if (stat) add_barcode_to_list(copy.barcode());
437                                 });
438
439                             });
440                             $modalInstance.close();
441                         }
442
443                         $scope.cancel = function($event) {
444                             $modalInstance.dismiss();
445                             $event.preventDefault();
446                         }
447                     }
448                 ]
449             });
450         });
451     }
452
453     $scope.attach_to_peer_bib = function() {
454         if (copyGrid.selectedItems().length == 0) return;
455
456         egCore.hatch.getItem('eg.cat.marked_conjoined_record').then(function(target_record) {
457             if (!target_record) return;
458
459             return $modal.open({
460                 templateUrl: './cat/catalog/t_conjoined_selector',
461                 animation: true,
462                 controller:
463                        ['$scope','$modalInstance',
464                 function($scope , $modalInstance) {
465                     $scope.update = false;
466
467                     $scope.peer_type = null;
468                     $scope.peer_type_list = [];
469
470                     get_peer_types = function() {
471                         if (egCore.env.bpt)
472                             return $q.when(egCore.env.bpt.list);
473
474                         return egCore.pcrud.retrieveAll('bpt', null, {atomic : true})
475                         .then(function(list) {
476                             egCore.env.absorbList(list, 'bpt');
477                             return list;
478                         });
479                     }
480
481                     get_peer_types().then(function(list){
482                         $scope.peer_type_list = list;
483                     });
484
485                     $scope.ok = function(type) {
486                         var promises = [];
487
488                         angular.forEach(copyGrid.selectedItems(), function (cp) {
489                             var n = new egCore.idl.bpbcm();
490                             n.isnew(true);
491                             n.peer_record(target_record);
492                             n.target_copy(cp.id);
493                             n.peer_type(type);
494                             promises.push(egCore.pcrud.create(n).then(function(){add_barcode_to_list(cp.barcode)}));
495                         });
496
497                         return $q.all(promises).then(function(){$modalInstance.close()});
498                     }
499
500                     $scope.cancel = function($event) {
501                         $modalInstance.dismiss();
502                         $event.preventDefault();
503                     }
504                 }]
505             });
506         });
507     }
508
509     $scope.selectedHoldingsCopyDelete = function () {
510         var copy_list = gatherSelectedHoldingsIds();
511         if (copy_list.length == 0) return;
512
513         var copy_objects = [];
514         egCore.pcrud.search('acp',
515             {deleted : 'f', id : copy_list},
516             { flesh : 1, flesh_fields : { acp : ['call_number'] } }
517         ).then(function(copy) {
518             copy_objects.push(copy);
519         }).then(function() {
520
521             var cnHash = {};
522             var perCnCopies = {};
523
524             var cn_count = 0;
525             var cp_count = 0;
526
527             angular.forEach(
528                 copy_objects,
529                 function (cp) {
530                     cp.isdeleted(1);
531                     cp_count++;
532                     var cn_id = cp.call_number().id();
533                     if (!cnHash[cn_id]) {
534                         cnHash[cn_id] = cp.call_number();
535                         perCnCopies[cn_id] = [cp];
536                     } else {
537                         perCnCopies[cn_id].push(cp);
538                     }
539                     cp.call_number(cn_id); // prevent loops in JSON-ification
540                 }
541             );
542
543             angular.forEach(perCnCopies, function (v, k) {
544                 cnHash[k].copies(v);
545             });
546
547             cnList = [];
548             angular.forEach(cnHash, function (v, k) {
549                 cnList.push(v);
550             });
551
552             if (cnList.length == 0) return;
553
554             var flags = {};
555
556             egConfirmDialog.open(
557                 egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES,
558                 egCore.strings.CONFIRM_DELETE_COPIES_VOLUMES_MESSAGE,
559                 {copies : cp_count, volumes : cn_count}
560             ).result.then(function() {
561                 egCore.net.request(
562                     'open-ils.cat',
563                     'open-ils.cat.asset.volume.fleshed.batch.update.override',
564                     egCore.auth.token(), cnList, 1, flags
565                 ).then(function(){
566                     angular.forEach(copyGrid.selectedItems(), function(cp){add_barcode_to_list(cp.barcode)});
567                 });
568             });
569         });
570     }
571
572     $scope.selectedHoldingsItemStatusTgrEvt= function() {
573         var item = copyGrid.selectedItems()[0];
574         if (item)
575             $location.path('/cat/item/' + item.id + '/triggered_events');
576     }
577
578     $scope.selectedHoldingsItemStatusHolds= function() {
579         var item = copyGrid.selectedItems()[0];
580         if (item)
581             $location.path('/cat/item/' + item.id + '/holds');
582     }
583
584     $scope.selectedHoldingsDamaged = function () {
585         var initial_list = copyGrid.selectedItems();
586         egCirc.mark_damaged(gatherSelectedHoldingsIds()).then(function(){
587             angular.forEach(initial_list, function(cp){add_barcode_to_list(cp.barcode)});
588         });
589     }
590
591     $scope.selectedHoldingsMissing = function () {
592         var initial_list = copyGrid.selectedItems();
593         egCirc.mark_missing(gatherSelectedHoldingsIds()).then(function(){
594             angular.forEach(initial_list, function(cp){add_barcode_to_list(cp.barcode)});
595         });
596     }
597
598     $scope.checkin = function () {
599         angular.forEach(copyGrid.selectedItems(), function (cp) {
600             egCirc.checkin({copy_barcode:cp.barcode}).then(
601                 function() { add_barcode_to_list(cp.barcode) }
602             );
603         });
604     }
605
606     $scope.renew = function () {
607         angular.forEach(copyGrid.selectedItems(), function (cp) {
608             egCirc.renew({copy_barcode:cp.barcode}).then(
609                 function() { add_barcode_to_list(cp.barcode) }
610             );
611         });
612     }
613
614
615     var spawnHoldingsAdd = function (vols,copies){
616         angular.forEach(gatherSelectedRecordIds(), function (r) {
617             var raw = [];
618             if (copies) { // just a copy on existing volumes
619                 angular.forEach(gatherSelectedVolumeIds(r), function (v) {
620                     raw.push( {callnumber : v} );
621                 });
622             } else if (vols) {
623                 angular.forEach(
624                     gatherSelectedHoldingsIds(r),
625                     function (i) {
626                         angular.forEach(copyGrid.selectedItems(), function(item) {
627                             if (i == item.id) raw.push({owner : item['call_number.owning_lib']});
628                         });
629                     }
630                 );
631             }
632
633             if (raw.length == 0) raw.push({});
634
635             egCore.net.request(
636                 'open-ils.actor',
637                 'open-ils.actor.anon_cache.set_value',
638                 null, 'edit-these-copies', {
639                     record_id: r,
640                     raw: raw,
641                     hide_vols : false,
642                     hide_copies : false
643                 }
644             ).then(function(key) {
645                 if (key) {
646                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
647                     $timeout(function() { $window.open(url, '_blank') });
648                 } else {
649                     alert('Could not create anonymous cache key!');
650                 }
651             });
652         });
653     }
654     $scope.selectedHoldingsVolCopyAdd = function () { spawnHoldingsAdd(true,false) }
655     $scope.selectedHoldingsCopyAdd = function () { spawnHoldingsAdd(false,true) }
656
657     $scope.showBibHolds = function () {
658         angular.forEach(gatherSelectedRecordIds(), function (r) {
659             var url = egCore.env.basePath + 'cat/catalog/record/' + r + '/holds';
660             $timeout(function() { $window.open(url, '_blank') });
661         });
662     }
663
664     var spawnHoldingsEdit = function (hide_vols,hide_copies){
665         angular.forEach(gatherSelectedRecordIds(), function (r) {
666             egCore.net.request(
667                 'open-ils.actor',
668                 'open-ils.actor.anon_cache.set_value',
669                 null, 'edit-these-copies', {
670                     record_id: r,
671                     copies: gatherSelectedHoldingsIds(r),
672                     raw: {},
673                     hide_vols : hide_vols,
674                     hide_copies : hide_copies
675                 }
676             ).then(function(key) {
677                 if (key) {
678                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
679                     $timeout(function() { $window.open(url, '_blank') });
680                 } else {
681                     alert('Could not create anonymous cache key!');
682                 }
683             });
684         });
685     }
686     $scope.selectedHoldingsVolCopyEdit = function () { spawnHoldingsEdit(false,false) }
687     $scope.selectedHoldingsVolEdit = function () { spawnHoldingsEdit(false,true) }
688     $scope.selectedHoldingsCopyEdit = function () { spawnHoldingsEdit(true,false) }
689
690     // this "transfers" selected copies to a new owning library,
691     // auto-creating volumes and deleting unused volumes as required.
692     $scope.changeItemOwningLib = function() {
693         var xfer_target = egCore.hatch.getLocalItem('eg.cat.volume_transfer_target');
694         var items = copyGrid.selectedItems();
695         if (!xfer_target || !items.length) {
696             return;
697         }
698         var vols_to_move   = {};
699         var copies_to_move = {};
700         angular.forEach(items, function(item) {
701             if (item['call_number.owning_lib'] != xfer_target) {
702                 if (item['call_number.id'] in vols_to_move) {
703                     copies_to_move[item['call_number.id']].push(item.id);
704                 } else {
705                     vols_to_move[item['call_number.id']] = {
706                         label       : item['call_number.label'],
707                         label_class : item['call_number.label_class'],
708                         record      : item['call_number.record.id'],
709                         prefix      : item['call_number.prefix.id'],
710                         suffix      : item['call_number.suffix.id']
711                     };
712                     copies_to_move[item['call_number.id']] = new Array;
713                     copies_to_move[item['call_number.id']].push(item.id);
714                 }
715             }
716         });
717
718         var promises = [];
719         angular.forEach(vols_to_move, function(vol) {
720             promises.push(egCore.net.request(
721                 'open-ils.cat',
722                 'open-ils.cat.call_number.find_or_create',
723                 egCore.auth.token(),
724                 vol.label,
725                 vol.record,
726                 xfer_target,
727                 vol.prefix,
728                 vol.suffix,
729                 vol.label_class
730             ).then(function(resp) {
731                 var evt = egCore.evt.parse(resp);
732                 if (evt) return;
733                 return egCore.net.request(
734                     'open-ils.cat',
735                     'open-ils.cat.transfer_copies_to_volume',
736                     egCore.auth.token(),
737                     resp.acn_id,
738                     copies_to_move[vol.id]
739                 );
740             }));
741         });
742
743         angular.forEach(
744             copyGrid.selectedItems(),
745             function(cp){
746                 promises.push(
747                     function(){ add_barcode_to_list(cp.barcode) }
748                 )
749             }
750         );
751         $q.all(promises);
752     }
753
754     $scope.transferItems = function (){
755         var xfer_target = egCore.hatch.getLocalItem('eg.cat.item_transfer_target');
756         var copy_ids = gatherSelectedHoldingsIds();
757         if (xfer_target && copy_ids.length > 0) {
758             egCore.net.request(
759                 'open-ils.cat',
760                 'open-ils.cat.transfer_copies_to_volume',
761                 egCore.auth.token(),
762                 xfer_target,
763                 copy_ids
764             ).then(
765                 function(resp) { // oncomplete
766                     var evt = egCore.evt.parse(resp);
767                     egConfirmDialog.open(
768                         egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_TITLE,
769                         egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_BODY,
770                         {'evt_desc': evt.desc}
771                     ).result.then(function() {
772                         egCore.net.request(
773                             'open-ils.cat',
774                             'open-ils.cat.transfer_copies_to_volume.override',
775                             egCore.auth.token(),
776                             xfer_target,
777                             copy_ids,
778                             { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
779                         );
780                     });
781                 },
782                 null, // onerror
783                 null // onprogress
784             ).then(function() {
785                     angular.forEach(copyGrid.selectedItems(), function(cp){add_barcode_to_list(cp.barcode)});
786             });
787         }
788     }
789
790     if (copyId.length > 0) {
791         itemSvc.fetch(null,copyId).then(
792             function() {
793                 copyGrid.refresh();
794             }
795         );
796     }
797
798 }])
799
800 /**
801  * Detail view -- shows one copy
802  */
803 .controller('ViewCtrl', 
804        ['$scope','$q','$location','$routeParams','$timeout','$window','egCore','itemSvc','egBilling',
805 function($scope , $q , $location , $routeParams , $timeout , $window , egCore , itemSvc , egBilling) {
806     var copyId = $routeParams.id;
807     $scope.tab = $routeParams.tab || 'summary';
808     $scope.context.page = 'detail';
809     $scope.summaryRecord = null;
810
811     $scope.edit = false;
812     if ($scope.tab == 'edit') {
813         $scope.tab = 'summary';
814         $scope.edit = true;
815     }
816
817
818     // use the cached record info
819     if (itemSvc.copy)
820         $scope.recordId = itemSvc.copy.call_number().record().id();
821
822     function loadCopy(barcode) {
823         $scope.context.itemNotFound = false;
824
825         // Avoid re-fetching the same copy while jumping tabs.
826         // In addition to being quicker, this helps to avoid flickering
827         // of the top panel which is always visible in the detail view.
828         //
829         // 'barcode' represents the loading of a new item - refetch it
830         // regardless of whether it matches the current item.
831         if (!barcode && itemSvc.copy && itemSvc.copy.id() == copyId) {
832             $scope.copy = itemSvc.copy;
833             $scope.recordId = itemSvc.copy.call_number().record().id();
834             return $q.when();
835         }
836
837         delete $scope.copy;
838         delete itemSvc.copy;
839
840         var deferred = $q.defer();
841         itemSvc.fetch(barcode, copyId, true).then(function(res) {
842             $scope.context.selectBarcode = true;
843
844             if (!res) {
845                 copyId = null;
846                 $scope.context.itemNotFound = true;
847                 deferred.reject(); // avoid propagation of data fetch calls
848                 return;
849             }
850
851             var copy = res.copy;
852             itemSvc.copy = copy;
853
854
855             $scope.copy = copy;
856             $scope.recordId = copy.call_number().record().id();
857             $scope.args.barcode = '';
858
859             // locally flesh org units
860             copy.circ_lib(egCore.org.get(copy.circ_lib()));
861             copy.call_number().owning_lib(
862                 egCore.org.get(copy.call_number().owning_lib()));
863
864             var r = copy.call_number().record();
865             if (r.owner()) r.owner(egCore.org.get(r.owner())); 
866
867             // make boolean for auto-magic true/false display
868             angular.forEach(
869                 ['ref','opac_visible','holdable','circulate'],
870                 function(field) { copy[field](Boolean(copy[field]() == 't')) }
871             );
872
873             // finally, if this is a different copy, redirect.
874             // Note that we flesh first since the copy we just
875             // fetched will be used after the redirect.
876             if (copyId && copyId != copy.id()) {
877                 // if a new barcode is scanned in the detail view,
878                 // update the url to match the ID of the new copy
879                 $location.path('/cat/item/' + copy.id() + '/' + $scope.tab);
880                 deferred.reject(); // avoid propagation of data fetch calls
881                 return;
882             }
883             copyId = copy.id();
884
885             deferred.resolve();
886         });
887
888         return deferred.promise;
889     }
890
891     // if loadPrev load the two most recent circulations
892     function loadCurrentCirc(loadPrev) {
893         delete $scope.circ;
894         delete $scope.circ_summary;
895         delete $scope.prev_circ_summary;
896         if (!copyId) return;
897         
898         egCore.pcrud.search('circ', 
899             {target_copy : copyId},
900             {   flesh : 2,
901                 flesh_fields : {
902                     circ : [
903                         'usr',
904                         'workstation',                                         
905                         'checkin_workstation',                                 
906                         'duration_rule',                                       
907                         'max_fine_rule',                                       
908                         'recurring_fine_rule'   
909                     ],
910                     au : ['card']
911                 },
912                 order_by : {circ : 'xact_start desc'}, 
913                 limit :  1
914             }
915
916         ).then(null, null, function(circ) {
917             $scope.circ = circ;
918
919             // load the chain for this circ
920             egCore.net.request(
921                 'open-ils.circ',
922                 'open-ils.circ.renewal_chain.retrieve_by_circ.summary',
923                 egCore.auth.token(), $scope.circ.id()
924             ).then(function(summary) {
925                 $scope.circ_summary = summary.summary;
926             });
927
928             if (!loadPrev) return;
929
930             // load the chain for the previous circ, plus the user
931             egCore.net.request(
932                 'open-ils.circ',
933                 'open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary',
934                 egCore.auth.token(), $scope.circ.id()
935
936             ).then(null, null, function(summary) {
937                 $scope.prev_circ_summary = summary.summary;
938
939                 egCore.pcrud.retrieve('au', summary.usr,
940                     {flesh : 1, flesh_fields : {au : ['card']}})
941
942                 .then(function(user) {
943                     $scope.prev_circ_usr = user;
944                 });
945             });
946         });
947     }
948
949     var maxHistory;
950     function fetchMaxCircHistory() {
951         if (maxHistory) return $q.when(maxHistory);
952         return egCore.org.settings(
953             'circ.item_checkout_history.max')
954         .then(function(set) {
955             maxHistory = set['circ.item_checkout_history.max'] || 4;
956             return maxHistory;
957         });
958     }
959
960     $scope.addBilling = function(circ) {
961         egBilling.showBillDialog({
962             xact_id : circ.id(),
963             patron : circ.usr()
964         });
965     }
966
967     $scope.retrieveAllPatrons = function() {
968         var users = new Set();
969         angular.forEach($scope.circ_list.map(function(circ) { return circ.usr(); }),function(usr) {
970             users.add(usr);
971         });
972         users.forEach(function(usr) {
973             $timeout(function() {
974                 var url = $location.absUrl().replace(
975                     /\/cat\/.*/,
976                     '/circ/patron/' + usr.id() + '/checkout');
977                 $window.open(url, '_blank')
978             });
979         });
980     }
981
982     function loadCircHistory() {
983         $scope.circ_list = [];
984
985         var copy_org = 
986             itemSvc.copy.call_number().id() == -1 ?
987             itemSvc.copy.circ_lib().id() :
988             itemSvc.copy.call_number().owning_lib().id()
989
990         // there is an extra layer of permissibility over circ
991         // history views
992         egCore.perm.hasPermAt('VIEW_COPY_CHECKOUT_HISTORY', true)
993         .then(function(orgIds) {
994
995             if (orgIds.indexOf(copy_org) == -1) {
996                 console.log('User is not allowed to view circ history');
997                 return $q.when(0);
998             }
999
1000             return fetchMaxCircHistory();
1001
1002         }).then(function(count) {
1003
1004             egCore.pcrud.search('circ', 
1005                 {target_copy : copyId},
1006                 {   flesh : 2,
1007                     flesh_fields : {
1008                         circ : [
1009                             'usr',
1010                             'workstation',                                         
1011                             'checkin_workstation',                                 
1012                             'recurring_fine_rule'   
1013                         ],
1014                         au : ['card']
1015                     },
1016                     order_by : {circ : 'xact_start desc'}, 
1017                     limit :  count
1018                 }
1019
1020             ).then(null, null, function(circ) {
1021
1022                 // flesh circ_lib locally
1023                 circ.circ_lib(egCore.org.get(circ.circ_lib()));
1024                 circ.checkin_lib(egCore.org.get(circ.checkin_lib()));
1025                 $scope.circ_list.push(circ);
1026             });
1027         });
1028     }
1029
1030
1031     function loadCircCounts() {
1032
1033         delete $scope.circ_counts;
1034         $scope.total_circs = 0;
1035         $scope.total_circs_this_year = 0;
1036         $scope.total_circs_prev_year = 0;
1037         if (!copyId) return;
1038
1039         egCore.pcrud.search('circbyyr', 
1040             {copy : copyId}, null, {atomic : true})
1041
1042         .then(function(counts) {
1043             $scope.circ_counts = counts;
1044
1045             angular.forEach(counts, function(count) {
1046                 $scope.total_circs += Number(count.count());
1047             });
1048
1049             var this_year = counts.filter(function(c) {
1050                 return c.year() == new Date().getFullYear();
1051             });
1052
1053             $scope.total_circs_this_year = 
1054                 this_year.length ? this_year[0].count() : 0;
1055
1056             var prev_year = counts.filter(function(c) {
1057                 return c.year() == new Date().getFullYear() - 1;
1058             });
1059
1060             $scope.total_circs_prev_year = 
1061                 prev_year.length ? prev_year[0].count() : 0;
1062
1063         });
1064     }
1065
1066     function loadHolds() {
1067         delete $scope.hold;
1068         if (!copyId) return;
1069
1070         egCore.pcrud.search('ahr', 
1071             {   current_copy : copyId, 
1072                 cancel_time : null, 
1073                 fulfillment_time : null,
1074                 capture_time : {'<>' : null}
1075             }, {
1076                 flesh : 2,
1077                 flesh_fields : {
1078                     ahr : ['requestor', 'usr'],
1079                     au  : ['card']
1080                 }
1081             }
1082         ).then(null, null, function(hold) {
1083             $scope.hold = hold;
1084             hold.pickup_lib(egCore.org.get(hold.pickup_lib()));
1085             if (hold.current_shelf_lib()) {
1086                 hold.current_shelf_lib(
1087                     egCore.org.get(hold.current_shelf_lib()));
1088             }
1089             hold.behind_desk(Boolean(hold.behind_desk() == 't'));
1090         });
1091     }
1092
1093     function loadTransits() {
1094         delete $scope.transit;
1095         delete $scope.hold_transit;
1096         if (!copyId) return;
1097
1098         egCore.pcrud.search('atc', 
1099             {target_copy : copyId},
1100             {order_by : {atc : 'source_send_time DESC'}}
1101
1102         ).then(null, null, function(transit) {
1103             $scope.transit = transit;
1104             transit.source(egCore.org.get(transit.source()));
1105             transit.dest(egCore.org.get(transit.dest()));
1106         })
1107     }
1108
1109
1110     // we don't need all data on all tabs, so fetch what's needed when needed.
1111     function loadTabData() {
1112         switch($scope.tab) {
1113             case 'summary':
1114                 loadCurrentCirc();
1115                 loadCircCounts();
1116                 break;
1117
1118             case 'circs':
1119                 loadCurrentCirc(true);
1120                 break;
1121
1122             case 'circ_list':
1123                 loadCircHistory();
1124                 break;
1125
1126             case 'holds':
1127                 loadHolds()
1128                 loadTransits();
1129                 break;
1130
1131             case 'triggered_events':
1132                 var url = $location.absUrl().replace(/\/staff.*/, '/actor/user/event_log');
1133                 url += '?copy_id=' + encodeURIComponent(copyId);
1134                 $scope.triggered_events_url = url;
1135                 $scope.funcs = {};
1136         }
1137
1138         if ($scope.edit) {
1139             egCore.net.request(
1140                 'open-ils.actor',
1141                 'open-ils.actor.anon_cache.set_value',
1142                 null, 'edit-these-copies', {
1143                     record_id: $scope.recordId,
1144                     copies: [copyId],
1145                     hide_vols : true,
1146                     hide_copies : false
1147                 }
1148             ).then(function(key) {
1149                 if (key) {
1150                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
1151                     $window.location.href = url;
1152                 } else {
1153                     alert('Could not create anonymous cache key!');
1154                 }
1155             });
1156         }
1157
1158         return;
1159     }
1160
1161     $scope.context.toggleDisplay = function() {
1162         $location.path('/cat/item/search');
1163     }
1164
1165     // handle the barcode scan box, which will replace our current copy
1166     $scope.context.search = function(args) {
1167         loadCopy(args.barcode).then(loadTabData);
1168     }
1169
1170     $scope.context.show_triggered_events = function() {
1171         $location.path('/cat/item/' + copyId + '/triggered_events');
1172     }
1173
1174     loadCopy().then(loadTabData);
1175 }])