]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/cat/item/app.js
a8781dc9bed5270b3e06ec999c686052df0ed226
[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','$uibModal','egCirc','egConfirmDialog',
143 function($scope , $q , $routeParams , $location , $timeout , $window , egCore , egGridDataProvider , itemSvc , egUser , $uibModal , 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 $uibModal.open({
272             templateUrl: './cat/catalog/t_add_to_bucket',
273             animation: true,
274             size: 'md',
275             controller:
276                    ['$scope','$uibModalInstance',
277             function($scope , $uibModalInstance) {
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                             $uibModalInstance.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                     $uibModalInstance.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 $uibModal.open({
339             templateUrl: './cat/catalog/t_request_items',
340             animation: true,
341             controller:
342                    ['$scope','$uibModalInstance','egUser',
343             function($scope , $uibModalInstance , 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                     $uibModalInstance.close();
393                 }
394
395                 $scope.cancel = function($event) {
396                     $uibModalInstance.dismiss();
397                     $event.preventDefault();
398                 }
399             }]
400         });
401     }
402
403     $scope.replaceBarcodes = function() {
404         angular.forEach(copyGrid.selectedItems(), function (cp) {
405             $uibModal.open({
406                 templateUrl: './cat/share/t_replace_barcode',
407                 animation: true,
408                 controller:
409                            ['$scope','$uibModalInstance',
410                     function($scope , $uibModalInstance) {
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                             $uibModalInstance.close();
441                         }
442
443                         $scope.cancel = function($event) {
444                             $uibModalInstance.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 $uibModal.open({
460                 templateUrl: './cat/catalog/t_conjoined_selector',
461                 animation: true,
462                 controller:
463                        ['$scope','$uibModalInstance',
464                 function($scope , $uibModalInstance) {
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(){$uibModalInstance.close()});
498                     }
499
500                     $scope.cancel = function($event) {
501                         $uibModalInstance.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.cancel_transit = function () {
585         var initial_list = copyGrid.selectedItems();
586         angular.forEach(copyGrid.selectedItems(), function(cp) {
587             egCirc.find_copy_transit(null, {copy_barcode:cp.barcode})
588                 .then(function(t) { return egCirc.abort_transit(t.id())    })
589                 .then(function()  { return add_barcode_to_list(cp.barcode) });
590         });
591     }
592
593     $scope.selectedHoldingsDamaged = function () {
594         var initial_list = copyGrid.selectedItems();
595         egCirc.mark_damaged(gatherSelectedHoldingsIds()).then(function(){
596             angular.forEach(initial_list, function(cp){add_barcode_to_list(cp.barcode)});
597         });
598     }
599
600     $scope.selectedHoldingsMissing = function () {
601         var initial_list = copyGrid.selectedItems();
602         egCirc.mark_missing(gatherSelectedHoldingsIds()).then(function(){
603             angular.forEach(initial_list, function(cp){add_barcode_to_list(cp.barcode)});
604         });
605     }
606
607     $scope.checkin = function () {
608         angular.forEach(copyGrid.selectedItems(), function (cp) {
609             egCirc.checkin({copy_barcode:cp.barcode}).then(
610                 function() { add_barcode_to_list(cp.barcode) }
611             );
612         });
613     }
614
615     $scope.renew = function () {
616         angular.forEach(copyGrid.selectedItems(), function (cp) {
617             egCirc.renew({copy_barcode:cp.barcode}).then(
618                 function() { add_barcode_to_list(cp.barcode) }
619             );
620         });
621     }
622
623
624     var spawnHoldingsAdd = function (vols,copies){
625         angular.forEach(gatherSelectedRecordIds(), function (r) {
626             var raw = [];
627             if (copies) { // just a copy on existing volumes
628                 angular.forEach(gatherSelectedVolumeIds(r), function (v) {
629                     raw.push( {callnumber : v} );
630                 });
631             } else if (vols) {
632                 angular.forEach(
633                     gatherSelectedHoldingsIds(r),
634                     function (i) {
635                         angular.forEach(copyGrid.selectedItems(), function(item) {
636                             if (i == item.id) raw.push({owner : item['call_number.owning_lib']});
637                         });
638                     }
639                 );
640             }
641
642             if (raw.length == 0) raw.push({});
643
644             egCore.net.request(
645                 'open-ils.actor',
646                 'open-ils.actor.anon_cache.set_value',
647                 null, 'edit-these-copies', {
648                     record_id: r,
649                     raw: raw,
650                     hide_vols : false,
651                     hide_copies : false
652                 }
653             ).then(function(key) {
654                 if (key) {
655                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
656                     $timeout(function() { $window.open(url, '_blank') });
657                 } else {
658                     alert('Could not create anonymous cache key!');
659                 }
660             });
661         });
662     }
663     $scope.selectedHoldingsVolCopyAdd = function () { spawnHoldingsAdd(true,false) }
664     $scope.selectedHoldingsCopyAdd = function () { spawnHoldingsAdd(false,true) }
665
666     $scope.showBibHolds = function () {
667         angular.forEach(gatherSelectedRecordIds(), function (r) {
668             var url = egCore.env.basePath + 'cat/catalog/record/' + r + '/holds';
669             $timeout(function() { $window.open(url, '_blank') });
670         });
671     }
672
673     var spawnHoldingsEdit = function (hide_vols,hide_copies){
674         angular.forEach(gatherSelectedRecordIds(), function (r) {
675             egCore.net.request(
676                 'open-ils.actor',
677                 'open-ils.actor.anon_cache.set_value',
678                 null, 'edit-these-copies', {
679                     record_id: r,
680                     copies: gatherSelectedHoldingsIds(r),
681                     raw: {},
682                     hide_vols : hide_vols,
683                     hide_copies : hide_copies
684                 }
685             ).then(function(key) {
686                 if (key) {
687                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
688                     $timeout(function() { $window.open(url, '_blank') });
689                 } else {
690                     alert('Could not create anonymous cache key!');
691                 }
692             });
693         });
694     }
695     $scope.selectedHoldingsVolCopyEdit = function () { spawnHoldingsEdit(false,false) }
696     $scope.selectedHoldingsVolEdit = function () { spawnHoldingsEdit(false,true) }
697     $scope.selectedHoldingsCopyEdit = function () { spawnHoldingsEdit(true,false) }
698
699     // this "transfers" selected copies to a new owning library,
700     // auto-creating volumes and deleting unused volumes as required.
701     $scope.changeItemOwningLib = function() {
702         var xfer_target = egCore.hatch.getLocalItem('eg.cat.volume_transfer_target');
703         var items = copyGrid.selectedItems();
704         if (!xfer_target || !items.length) {
705             return;
706         }
707         var vols_to_move   = {};
708         var copies_to_move = {};
709         angular.forEach(items, function(item) {
710             if (item['call_number.owning_lib'] != xfer_target) {
711                 if (item['call_number.id'] in vols_to_move) {
712                     copies_to_move[item['call_number.id']].push(item.id);
713                 } else {
714                     vols_to_move[item['call_number.id']] = {
715                         label       : item['call_number.label'],
716                         label_class : item['call_number.label_class'],
717                         record      : item['call_number.record.id'],
718                         prefix      : item['call_number.prefix.id'],
719                         suffix      : item['call_number.suffix.id']
720                     };
721                     copies_to_move[item['call_number.id']] = new Array;
722                     copies_to_move[item['call_number.id']].push(item.id);
723                 }
724             }
725         });
726
727         var promises = [];
728         angular.forEach(vols_to_move, function(vol) {
729             promises.push(egCore.net.request(
730                 'open-ils.cat',
731                 'open-ils.cat.call_number.find_or_create',
732                 egCore.auth.token(),
733                 vol.label,
734                 vol.record,
735                 xfer_target,
736                 vol.prefix,
737                 vol.suffix,
738                 vol.label_class
739             ).then(function(resp) {
740                 var evt = egCore.evt.parse(resp);
741                 if (evt) return;
742                 return egCore.net.request(
743                     'open-ils.cat',
744                     'open-ils.cat.transfer_copies_to_volume',
745                     egCore.auth.token(),
746                     resp.acn_id,
747                     copies_to_move[vol.id]
748                 );
749             }));
750         });
751
752         angular.forEach(
753             copyGrid.selectedItems(),
754             function(cp){
755                 promises.push(
756                     function(){ add_barcode_to_list(cp.barcode) }
757                 )
758             }
759         );
760         $q.all(promises);
761     }
762
763     $scope.transferItems = function (){
764         var xfer_target = egCore.hatch.getLocalItem('eg.cat.item_transfer_target');
765         var copy_ids = gatherSelectedHoldingsIds();
766         if (xfer_target && copy_ids.length > 0) {
767             egCore.net.request(
768                 'open-ils.cat',
769                 'open-ils.cat.transfer_copies_to_volume',
770                 egCore.auth.token(),
771                 xfer_target,
772                 copy_ids
773             ).then(
774                 function(resp) { // oncomplete
775                     var evt = egCore.evt.parse(resp);
776                     egConfirmDialog.open(
777                         egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_TITLE,
778                         egCore.strings.OVERRIDE_TRANSFER_COPIES_TO_MARKED_VOLUME_BODY,
779                         {'evt_desc': evt.desc}
780                     ).result.then(function() {
781                         egCore.net.request(
782                             'open-ils.cat',
783                             'open-ils.cat.transfer_copies_to_volume.override',
784                             egCore.auth.token(),
785                             xfer_target,
786                             copy_ids,
787                             { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
788                         );
789                     });
790                 },
791                 null, // onerror
792                 null // onprogress
793             ).then(function() {
794                     angular.forEach(copyGrid.selectedItems(), function(cp){add_barcode_to_list(cp.barcode)});
795             });
796         }
797     }
798
799     if (copyId.length > 0) {
800         itemSvc.fetch(null,copyId).then(
801             function() {
802                 copyGrid.refresh();
803             }
804         );
805     }
806
807 }])
808
809 /**
810  * Detail view -- shows one copy
811  */
812 .controller('ViewCtrl', 
813        ['$scope','$q','$location','$routeParams','$timeout','$window','egCore','itemSvc','egBilling',
814 function($scope , $q , $location , $routeParams , $timeout , $window , egCore , itemSvc , egBilling) {
815     var copyId = $routeParams.id;
816     $scope.tab = $routeParams.tab || 'summary';
817     $scope.context.page = 'detail';
818     $scope.summaryRecord = null;
819
820     $scope.edit = false;
821     if ($scope.tab == 'edit') {
822         $scope.tab = 'summary';
823         $scope.edit = true;
824     }
825
826
827     // use the cached record info
828     if (itemSvc.copy)
829         $scope.recordId = itemSvc.copy.call_number().record().id();
830
831     function loadCopy(barcode) {
832         $scope.context.itemNotFound = false;
833
834         // Avoid re-fetching the same copy while jumping tabs.
835         // In addition to being quicker, this helps to avoid flickering
836         // of the top panel which is always visible in the detail view.
837         //
838         // 'barcode' represents the loading of a new item - refetch it
839         // regardless of whether it matches the current item.
840         if (!barcode && itemSvc.copy && itemSvc.copy.id() == copyId) {
841             $scope.copy = itemSvc.copy;
842             $scope.recordId = itemSvc.copy.call_number().record().id();
843             return $q.when();
844         }
845
846         delete $scope.copy;
847         delete itemSvc.copy;
848
849         var deferred = $q.defer();
850         itemSvc.fetch(barcode, copyId, true).then(function(res) {
851             $scope.context.selectBarcode = true;
852
853             if (!res) {
854                 copyId = null;
855                 $scope.context.itemNotFound = true;
856                 deferred.reject(); // avoid propagation of data fetch calls
857                 return;
858             }
859
860             var copy = res.copy;
861             itemSvc.copy = copy;
862
863
864             $scope.copy = copy;
865             $scope.recordId = copy.call_number().record().id();
866             $scope.args.barcode = '';
867
868             // locally flesh org units
869             copy.circ_lib(egCore.org.get(copy.circ_lib()));
870             copy.call_number().owning_lib(
871                 egCore.org.get(copy.call_number().owning_lib()));
872
873             var r = copy.call_number().record();
874             if (r.owner()) r.owner(egCore.org.get(r.owner())); 
875
876             // make boolean for auto-magic true/false display
877             angular.forEach(
878                 ['ref','opac_visible','holdable','circulate'],
879                 function(field) { copy[field](Boolean(copy[field]() == 't')) }
880             );
881
882             // finally, if this is a different copy, redirect.
883             // Note that we flesh first since the copy we just
884             // fetched will be used after the redirect.
885             if (copyId && copyId != copy.id()) {
886                 // if a new barcode is scanned in the detail view,
887                 // update the url to match the ID of the new copy
888                 $location.path('/cat/item/' + copy.id() + '/' + $scope.tab);
889                 deferred.reject(); // avoid propagation of data fetch calls
890                 return;
891             }
892             copyId = copy.id();
893
894             deferred.resolve();
895         });
896
897         return deferred.promise;
898     }
899
900     // if loadPrev load the two most recent circulations
901     function loadCurrentCirc(loadPrev) {
902         delete $scope.circ;
903         delete $scope.circ_summary;
904         delete $scope.prev_circ_summary;
905         if (!copyId) return;
906         
907         egCore.pcrud.search('circ', 
908             {target_copy : copyId},
909             {   flesh : 2,
910                 flesh_fields : {
911                     circ : [
912                         'usr',
913                         'workstation',                                         
914                         'checkin_workstation',                                 
915                         'duration_rule',                                       
916                         'max_fine_rule',                                       
917                         'recurring_fine_rule'   
918                     ],
919                     au : ['card']
920                 },
921                 order_by : {circ : 'xact_start desc'}, 
922                 limit :  1
923             }
924
925         ).then(null, null, function(circ) {
926             $scope.circ = circ;
927
928             // load the chain for this circ
929             egCore.net.request(
930                 'open-ils.circ',
931                 'open-ils.circ.renewal_chain.retrieve_by_circ.summary',
932                 egCore.auth.token(), $scope.circ.id()
933             ).then(function(summary) {
934                 $scope.circ_summary = summary.summary;
935             });
936
937             if (!loadPrev) return;
938
939             // load the chain for the previous circ, plus the user
940             egCore.net.request(
941                 'open-ils.circ',
942                 'open-ils.circ.prev_renewal_chain.retrieve_by_circ.summary',
943                 egCore.auth.token(), $scope.circ.id()
944
945             ).then(null, null, function(summary) {
946                 $scope.prev_circ_summary = summary.summary;
947
948                 egCore.pcrud.retrieve('au', summary.usr,
949                     {flesh : 1, flesh_fields : {au : ['card']}})
950
951                 .then(function(user) {
952                     $scope.prev_circ_usr = user;
953                 });
954             });
955         });
956     }
957
958     var maxHistory;
959     function fetchMaxCircHistory() {
960         if (maxHistory) return $q.when(maxHistory);
961         return egCore.org.settings(
962             'circ.item_checkout_history.max')
963         .then(function(set) {
964             maxHistory = set['circ.item_checkout_history.max'] || 4;
965             return maxHistory;
966         });
967     }
968
969     $scope.addBilling = function(circ) {
970         egBilling.showBillDialog({
971             xact_id : circ.id(),
972             patron : circ.usr()
973         });
974     }
975
976     $scope.retrieveAllPatrons = function() {
977         var users = new Set();
978         angular.forEach($scope.circ_list.map(function(circ) { return circ.usr(); }),function(usr) {
979             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     function loadCircHistory() {
992         $scope.circ_list = [];
993
994         var copy_org = 
995             itemSvc.copy.call_number().id() == -1 ?
996             itemSvc.copy.circ_lib().id() :
997             itemSvc.copy.call_number().owning_lib().id()
998
999         // there is an extra layer of permissibility over circ
1000         // history views
1001         egCore.perm.hasPermAt('VIEW_COPY_CHECKOUT_HISTORY', true)
1002         .then(function(orgIds) {
1003
1004             if (orgIds.indexOf(copy_org) == -1) {
1005                 console.log('User is not allowed to view circ history');
1006                 return $q.when(0);
1007             }
1008
1009             return fetchMaxCircHistory();
1010
1011         }).then(function(count) {
1012
1013             egCore.pcrud.search('circ', 
1014                 {target_copy : copyId},
1015                 {   flesh : 2,
1016                     flesh_fields : {
1017                         circ : [
1018                             'usr',
1019                             'workstation',                                         
1020                             'checkin_workstation',                                 
1021                             'recurring_fine_rule'   
1022                         ],
1023                         au : ['card']
1024                     },
1025                     order_by : {circ : 'xact_start desc'}, 
1026                     limit :  count
1027                 }
1028
1029             ).then(null, null, function(circ) {
1030
1031                 // flesh circ_lib locally
1032                 circ.circ_lib(egCore.org.get(circ.circ_lib()));
1033                 circ.checkin_lib(egCore.org.get(circ.checkin_lib()));
1034                 $scope.circ_list.push(circ);
1035             });
1036         });
1037     }
1038
1039
1040     function loadCircCounts() {
1041
1042         delete $scope.circ_counts;
1043         $scope.total_circs = 0;
1044         $scope.total_circs_this_year = 0;
1045         $scope.total_circs_prev_year = 0;
1046         if (!copyId) return;
1047
1048         egCore.pcrud.search('circbyyr', 
1049             {copy : copyId}, null, {atomic : true})
1050
1051         .then(function(counts) {
1052             $scope.circ_counts = counts;
1053
1054             angular.forEach(counts, function(count) {
1055                 $scope.total_circs += Number(count.count());
1056             });
1057
1058             var this_year = counts.filter(function(c) {
1059                 return c.year() == new Date().getFullYear();
1060             });
1061
1062             $scope.total_circs_this_year = 
1063                 this_year.length ? this_year[0].count() : 0;
1064
1065             var prev_year = counts.filter(function(c) {
1066                 return c.year() == new Date().getFullYear() - 1;
1067             });
1068
1069             $scope.total_circs_prev_year = 
1070                 prev_year.length ? prev_year[0].count() : 0;
1071
1072         });
1073     }
1074
1075     function loadHolds() {
1076         delete $scope.hold;
1077         if (!copyId) return;
1078
1079         egCore.pcrud.search('ahr', 
1080             {   current_copy : copyId, 
1081                 cancel_time : null, 
1082                 fulfillment_time : null,
1083                 capture_time : {'<>' : null}
1084             }, {
1085                 flesh : 2,
1086                 flesh_fields : {
1087                     ahr : ['requestor', 'usr'],
1088                     au  : ['card']
1089                 }
1090             }
1091         ).then(null, null, function(hold) {
1092             $scope.hold = hold;
1093             hold.pickup_lib(egCore.org.get(hold.pickup_lib()));
1094             if (hold.current_shelf_lib()) {
1095                 hold.current_shelf_lib(
1096                     egCore.org.get(hold.current_shelf_lib()));
1097             }
1098             hold.behind_desk(Boolean(hold.behind_desk() == 't'));
1099         });
1100     }
1101
1102     function loadTransits() {
1103         delete $scope.transit;
1104         delete $scope.hold_transit;
1105         if (!copyId) return;
1106
1107         egCore.pcrud.search('atc', 
1108             {target_copy : copyId},
1109             {order_by : {atc : 'source_send_time DESC'}}
1110
1111         ).then(null, null, function(transit) {
1112             $scope.transit = transit;
1113             transit.source(egCore.org.get(transit.source()));
1114             transit.dest(egCore.org.get(transit.dest()));
1115         })
1116     }
1117
1118
1119     // we don't need all data on all tabs, so fetch what's needed when needed.
1120     function loadTabData() {
1121         switch($scope.tab) {
1122             case 'summary':
1123                 loadCurrentCirc();
1124                 loadCircCounts();
1125                 break;
1126
1127             case 'circs':
1128                 loadCurrentCirc(true);
1129                 break;
1130
1131             case 'circ_list':
1132                 loadCircHistory();
1133                 break;
1134
1135             case 'holds':
1136                 loadHolds()
1137                 loadTransits();
1138                 break;
1139
1140             case 'triggered_events':
1141                 var url = $location.absUrl().replace(/\/staff.*/, '/actor/user/event_log');
1142                 url += '?copy_id=' + encodeURIComponent(copyId);
1143                 $scope.triggered_events_url = url;
1144                 $scope.funcs = {};
1145         }
1146
1147         if ($scope.edit) {
1148             egCore.net.request(
1149                 'open-ils.actor',
1150                 'open-ils.actor.anon_cache.set_value',
1151                 null, 'edit-these-copies', {
1152                     record_id: $scope.recordId,
1153                     copies: [copyId],
1154                     hide_vols : true,
1155                     hide_copies : false
1156                 }
1157             ).then(function(key) {
1158                 if (key) {
1159                     var url = egCore.env.basePath + 'cat/volcopy/' + key;
1160                     $window.location.href = url;
1161                 } else {
1162                     alert('Could not create anonymous cache key!');
1163                 }
1164             });
1165         }
1166
1167         return;
1168     }
1169
1170     $scope.context.toggleDisplay = function() {
1171         $location.path('/cat/item/search');
1172     }
1173
1174     // handle the barcode scan box, which will replace our current copy
1175     $scope.context.search = function(args) {
1176         loadCopy(args.barcode).then(loadTabData);
1177     }
1178
1179     $scope.context.show_triggered_events = function() {
1180         $location.path('/cat/item/' + copyId + '/triggered_events');
1181     }
1182
1183     loadCopy().then(loadTabData);
1184 }])