]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/cat/bucket/copy/app.js
LP1915464 follow-up: use spaces, not tabs; remove extra comma
[Evergreen.git] / Open-ILS / web / js / ui / default / staff / cat / bucket / copy / app.js
1 /**
2  * Copy Buckets
3  *
4  * Known Issues
5  *
6  * add-all actions only add visible/fetched items.
7  * remove all from bucket UI leaves busted pagination 
8  *   -- apply a refresh after item removal?
9  * problems with bucket view fetching by record ID instead of bucket item:
10  *   -- dupe bibs always sort to the bottom
11  *   -- dupe bibs result in more records displayed per page than requested
12  *   -- item 'pos' ordering is not honored on initial load.
13  */
14
15 angular.module('egCatCopyBuckets', 
16     ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod'])
17
18 .config(function($routeProvider, $locationProvider, $compileProvider) {
19     $locationProvider.html5Mode(true);
20     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|mailto|blob):/); // grid export
21         
22     var resolver = {delay : function(egStartup) {return egStartup.go()}};
23
24     $routeProvider.when('/cat/bucket/copy/pending/:id', {
25         templateUrl: './cat/bucket/copy/t_pending',
26         controller: 'PendingCtrl',
27         resolve : resolver
28     });
29
30     $routeProvider.when('/cat/bucket/copy/pending', {
31         templateUrl: './cat/bucket/copy/t_pending',
32         controller: 'PendingCtrl',
33         resolve : resolver
34     });
35
36     $routeProvider.when('/cat/bucket/copy/view/:id', {
37         templateUrl: './cat/bucket/copy/t_view',
38         controller: 'ViewCtrl',
39         resolve : resolver
40     });
41
42     $routeProvider.when('/cat/bucket/copy/view', {
43         templateUrl: './cat/bucket/copy/t_view',
44         controller: 'ViewCtrl',
45         resolve : resolver
46     });
47
48     // default page / bucket view
49     $routeProvider.otherwise({redirectTo : '/cat/bucket/copy/view'});
50 })
51
52 /**
53  * bucketSvc allows us to communicate between the pending
54  * and view controllers.  It also allows us to cache
55  * data for each so that data reloads are not needed on every 
56  * tab click (i.e. route persistence).
57  */
58 .factory('bucketSvc', ['$q','egCore', function($q,  egCore) { 
59
60     var service = {
61         allBuckets : [], // un-fleshed user buckets
62         barcodeString : '', // last scanned barcode
63         barcodeRecords : [], // last scanned barcode results
64         currentBucket : null, // currently viewed bucket
65
66         // per-page list collections
67         pendingList : [],
68         viewList  : [],
69
70         // fetches all staff/copy buckets for the authenticated user
71         // this function may only be called after startup.
72         fetchUserBuckets : function(force) {
73             if (this.allBuckets.length && !force) return;
74             var self = this;
75             return egCore.net.request(
76                 'open-ils.actor',
77                 'open-ils.actor.container.retrieve_by_class.authoritative',
78                 egCore.auth.token(), egCore.auth.user().id(), 
79                 'copy', 'staff_client'
80             ).then(function(buckets) { self.allBuckets = buckets });
81         },
82
83         createBucket : function(name, desc) {
84             var deferred = $q.defer();
85             var bucket = new egCore.idl.ccb();
86             bucket.owner(egCore.auth.user().id());
87             bucket.name(name);
88             bucket.description(desc || '');
89             bucket.btype('staff_client');
90
91             egCore.net.request(
92                 'open-ils.actor',
93                 'open-ils.actor.container.create',
94                 egCore.auth.token(), 'copy', bucket
95             ).then(function(resp) {
96                 if (resp) {
97                     if (typeof resp == 'object') {
98                         console.error('bucket create error: ' + js2JSON(resp));
99                         deferred.reject();
100                     } else {
101                         deferred.resolve(resp);
102                     }
103                 }
104             });
105
106             return deferred.promise;
107         },
108
109         // edit the current bucket.  since we edit the 
110         // local object, there's no need to re-fetch.
111         editBucket : function(args) {
112             var bucket = service.currentBucket;
113             bucket.name(args.name);
114             bucket.description(args.desc);
115             bucket.pub(args.pub);
116             return egCore.net.request(
117                 'open-ils.actor',
118                 'open-ils.actor.container.update',
119                 egCore.auth.token(), 'copy', bucket
120             );
121         }
122     }
123
124     // returns 1 if full refresh is needed
125     // returns 2 if list refresh only is needed
126     service.bucketRefreshLevel = function(id) {
127         if (!service.currentBucket) return 1;
128         if (service.bucketNeedsRefresh) {
129             service.bucketNeedsRefresh = false;
130             service.currentBucket = null;
131             return 1;
132         }
133         if (service.currentBucket.id() != id) return 1;
134         return 2;
135     }
136
137     // returns a promise, resolved with bucket, rejected if bucket is
138     // not fetch-able
139     service.fetchBucket = function(id) {
140         var refresh = service.bucketRefreshLevel(id);
141         if (refresh == 2) return $q.when(service.currentBucket);
142
143         var deferred = $q.defer();
144
145         egCore.net.request(
146             'open-ils.actor',
147             'open-ils.actor.container.flesh.authoritative',
148             egCore.auth.token(), 'copy', id
149         ).then(function(bucket) {
150             var evt = egCore.evt.parse(bucket);
151             if (evt) {
152                 console.debug(evt);
153                 deferred.reject(evt);
154                 return;
155             }
156             egCore.pcrud.retrieve(
157                 'au', bucket.owner(),
158                 {flesh : 1, flesh_fields : {au : ["card"]}}
159             ).then(function(patron) {
160                 // On the off chance no barcode is present (it's not 
161                 // required) use the patron username as the identifier.
162                 bucket._owner_ident = patron.card() ? 
163                     patron.card().barcode() : patron.usrname();
164                 bucket._owner_name = patron.family_name();
165                 bucket._owner_ou = egCore.org.get(patron.home_ou()).shortname();
166             });
167
168             service.currentBucket = bucket;
169             deferred.resolve(bucket);
170         });
171
172         return deferred.promise;
173     }
174
175     // deletes a single container item from a bucket by container item ID.
176     // promise is rejected on failure
177     service.detachCopy = function(itemId) {
178         var deferred = $q.defer();
179         egCore.net.request(
180             'open-ils.actor',
181             'open-ils.actor.container.item.delete',
182             egCore.auth.token(), 'copy', itemId
183         ).then(function(resp) { 
184             var evt = egCore.evt.parse(resp);
185             if (evt) {
186                 console.error(evt);
187                 deferred.reject(evt);
188                 return;
189             }
190             console.log('detached bucket item ' + itemId);
191             deferred.resolve(resp);
192         });
193
194         return deferred.promise;
195     }
196
197     // delete bucket by ID.
198     // resolved w/ response on successful delete,
199     // rejected otherwise.
200     service.deleteBucket = function(id) {
201         var deferred = $q.defer();
202         egCore.net.request(
203             'open-ils.actor',
204             'open-ils.actor.container.full_delete',
205             egCore.auth.token(), 'copy', id
206         ).then(function(resp) {
207             var evt = egCore.evt.parse(resp);
208             if (evt) {
209                 console.error(evt);
210                 deferred.reject(evt);
211                 return;
212             }
213             deferred.resolve(resp);
214         });
215         return deferred.promise;
216     }
217
218     // apply last inventory data to fetched bucket items
219     service.fetchRecentInventoryData = function(copy) {
220         return egCore.pcrud.search('alci',
221             {copy: copy.id},
222             {flesh: 2, flesh_fields: {alci: ['inventory_workstation']}}
223         ).then(function(alci) {
224             return alci;
225         });
226     }
227
228     return service;
229 }])
230
231 /**
232  * Top-level controller.  
233  * Hosts functions needed by all controllers.
234  */
235 .controller('CopyBucketCtrl',
236        ['$scope','$location','$q','$timeout','$uibModal',
237         '$window','egCore','bucketSvc','egProgressDialog',
238 function($scope,  $location,  $q,  $timeout,  $uibModal,  
239          $window,  egCore,  bucketSvc , egProgressDialog) {
240
241     $scope.bucketSvc = bucketSvc;
242     $scope.bucket = function() { return bucketSvc.currentBucket }
243
244     // tabs: search, pending, view
245     $scope.setTab = function(tab) { 
246         $scope.tab = tab;
247
248         // for bucket selector; must be called after route resolve
249         bucketSvc.fetchUserBuckets(); 
250     };
251
252     $scope.loadBucketFromMenu = function(item, bucket) {
253         if (bucket) return $scope.loadBucket(bucket.id());
254     }
255
256     $scope.loadBucket = function(id) {
257         $location.path(
258             '/cat/bucket/copy/' + 
259                 $scope.tab + '/' + encodeURIComponent(id));
260     }
261
262     $scope.addToBucket = function(recs) {
263         if (recs.length == 0) return;
264         bucketSvc.bucketNeedsRefresh = true;
265
266         var ids = recs.map(function(rec) { return rec.id; });
267
268         egProgressDialog.open();
269
270         egCore.net.request(
271             'open-ils.actor',
272             'open-ils.actor.container.item.create.batch',
273             egCore.auth.token(), 'copy',
274             bucketSvc.currentBucket.id(), ids
275         ).then(
276             function() {
277                 egProgressDialog.close();
278             }, // complete
279             null, // error
280             function(resp) {
281                 // HACK: add the IDs of the added items so that the size
282                 // of the view list will grow (and update any UI looking at
283                 // the list size).  The data stored is inconsistent, but since
284                 // we are forcing a bucket refresh on the next rendering of
285                 // the view pane, the list will be repaired.
286                 bucketSvc.currentBucket.items().push(resp);
287              }
288         );
289     }
290
291     $scope.openCreateBucketDialog = function() {
292         $uibModal.open({
293             templateUrl: './cat/bucket/share/t_bucket_create',
294             backdrop: 'static',
295             controller: 
296                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
297                 $scope.focusMe = true;
298                 $scope.ok = function(args) { $uibModalInstance.close(args) }
299                 $scope.cancel = function () { $uibModalInstance.dismiss() }
300             }]
301         }).result.then(function (args) {
302             if (!args || !args.name) return;
303             bucketSvc.createBucket(args.name, args.desc).then(
304                 function(id) {
305                     if (!id) return;
306                     bucketSvc.viewList = [];
307                     bucketSvc.allBuckets = []; // reset
308                     bucketSvc.currentBucket = null;
309                     $location.path(
310                         '/cat/bucket/copy/' + $scope.tab + '/' + id);
311                 }
312             );
313         });
314     }
315
316     $scope.openEditBucketDialog = function() {
317         $uibModal.open({
318             templateUrl: './cat/bucket/share/t_bucket_edit',
319             backdrop: 'static',
320             controller: 
321                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
322                 $scope.focusMe = true;
323                 $scope.args = {
324                     name : bucketSvc.currentBucket.name(),
325                     desc : bucketSvc.currentBucket.description(),
326                     pub : bucketSvc.currentBucket.pub() == 't'
327                 };
328                 $scope.ok = function(args) { 
329                     if (!args) return;
330                     $scope.actionPending = true;
331                     args.pub = args.pub ? 't' : 'f';
332                     // close the dialog after edit has completed
333                     bucketSvc.editBucket(args).then(
334                         function() { $uibModalInstance.close() });
335                 }
336                 $scope.cancel = function () { $uibModalInstance.dismiss() }
337             }]
338         })
339     }
340
341     // opens the delete confirmation and deletes the current
342     // bucket if the user confirms.
343     $scope.openDeleteBucketDialog = function() {
344         $uibModal.open({
345             templateUrl: './cat/bucket/share/t_bucket_delete',
346             backdrop: 'static',
347             controller : 
348                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
349                 $scope.bucket = function() { return bucketSvc.currentBucket }
350                 $scope.ok = function() { $uibModalInstance.close() }
351                 $scope.cancel = function() { $uibModalInstance.dismiss() }
352             }]
353         }).result.then(function () {
354             bucketSvc.deleteBucket(bucketSvc.currentBucket.id())
355             .then(function() {
356                 bucketSvc.allBuckets = [];
357                 $location.path('/cat/bucket/copy/view');
358             });
359         });
360     }
361
362     // retrieves the requested bucket by ID
363     $scope.openSharedBucketDialog = function() {
364         $uibModal.open({
365             templateUrl: './cat/bucket/share/t_load_shared',
366             backdrop: 'static',
367             controller :
368                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
369                 $scope.focusMe = true;
370                 $scope.ok = function(args) {
371                     if (args && args.id) {
372                         $uibModalInstance.close(args.id)
373                     }
374                 }
375                 $scope.cancel = function() { $uibModalInstance.dismiss() }
376             }]
377         }).result.then(function(id) {
378             // RecordBucketCtrl $scope is not inherited by the
379             // modal, so we need to call loadBucket from the
380             // promise resolver.
381             $scope.loadBucket(id);
382         });
383     }
384
385 }])
386
387 .controller('PendingCtrl',
388        ['$scope','$routeParams','bucketSvc','egGridDataProvider', 'egCore',
389 function($scope,  $routeParams,  bucketSvc , egGridDataProvider,   egCore) {
390     $scope.setTab('pending');
391
392     $scope.context = {
393         copyNotFound : false,
394         selectPendingBC : true
395     };
396
397     var query;
398     $scope.gridControls = {
399         setQuery : function(q) {
400             if (bucketSvc.pendingList.length)
401                 return {id : bucketSvc.pendingList};
402             else
403             return null;
404         },
405         allItemsRetrieved : function() {
406             $scope.context.selectPendingBC = true;
407         }
408     }
409
410     $scope.handle_barcode_completion = function(barcode) {
411         return egCore.net.request(
412             'open-ils.actor',
413             'open-ils.actor.get_barcodes',
414             egCore.auth.token(), egCore.auth.user().ws_ou(), 
415             'asset', barcode)
416
417         .then(function(resp) {
418             // TODO: handle event during barcode lookup
419             if (evt = egCore.evt.parse(resp)) {
420                 console.error(evt.toString());
421                 return $q.reject();
422             }
423
424             // no matching barcodes: return the barcode as entered
425             // by the user (so that, e.g., checkout can fall back to
426             // precat/noncat handling)
427             if (!resp || !resp[0]) {
428                 return barcode;
429             }
430
431             // exactly one matching barcode: return it
432             if (resp.length == 1) {
433                 return resp[0].barcode;
434             }
435
436             // multiple matching barcodes: let the user pick one 
437             console.debug('multiple matching barcodes');
438             var matches = [];
439             var promises = [];
440             var final_barcode;
441             angular.forEach(resp, function(cp) {
442                 promises.push(
443                     egCore.net.request(
444                         'open-ils.circ',
445                         'open-ils.circ.copy_details.retrieve',
446                         egCore.auth.token(), cp.id
447                     ).then(function(r) {
448                         matches.push({
449                             barcode: r.copy.barcode(),
450                             title: r.mvr.title(),
451                             org_name: egCore.org.get(r.copy.circ_lib()).name(),
452                             org_shortname: egCore.org.get(r.copy.circ_lib()).shortname()
453                         });
454                     })
455                 );
456             });
457             return $q.all(promises)
458             .then(function() {
459                 return $uibModal.open({
460                     templateUrl: './circ/share/t_barcode_choice_dialog',
461                     controller:
462                         ['$scope', '$uibModalInstance',
463                         function($scope, $uibModalInstance) {
464                         $scope.matches = matches;
465                         $scope.ok = function(barcode) {
466                             $uibModalInstance.close();
467                             final_barcode = barcode;
468                         }
469                         $scope.cancel = function() {$uibModalInstance.dismiss()}
470                     }],
471                 }).result.then(function() { return final_barcode });
472             })
473         });
474     }
475
476     $scope.search = function() {
477         bucketSvc.barcodeRecords = [];
478         $scope.context.itemNotFound = false;
479
480         // clear selection so re-selecting can have an effect
481         $scope.context.selectPendingBC = false;
482
483         return $scope.handle_barcode_completion(bucketSvc.barcodeString)
484         .then(function(actual_barcode) {
485             egCore.pcrud.search(
486                 'acp',
487                 {barcode : actual_barcode, deleted : 'f'},
488                 {}
489             ).then(function(copy) {
490                 if (copy) {
491                     bucketSvc.pendingList.push(copy.id());
492                     $scope.gridControls.setQuery({id : bucketSvc.pendingList});
493                     bucketSvc.barcodeString = ''; // clear form on valid copy
494                 } else {
495                     $scope.context.itemNotFound = true;
496                     $scope.context.selectPendingBC = true;
497                 }
498             });
499         });
500     }
501
502     $scope.resetPendingList = function() {
503         bucketSvc.pendingList = [];
504         $scope.gridControls.setQuery({});
505     }
506     
507     if ($routeParams.id && 
508         (!bucketSvc.currentBucket || 
509             bucketSvc.currentBucket.id() != $routeParams.id)) {
510         // user has accessed this page cold with a bucket ID.
511         // fetch the bucket for display, then set the totalCount
512         // (also for display), but avoid fully fetching the bucket,
513         // since it's premature, in this UI.
514         bucketSvc.fetchBucket($routeParams.id);
515     }
516     $scope.gridControls.setQuery();
517 }])
518
519 .controller('ViewCtrl',
520        ['$scope','$q','$routeParams','$timeout','$window','$uibModal','bucketSvc','egCore','egOrg','egUser',
521         'ngToast','egConfirmDialog','egProgressDialog', 'egItem',
522 function($scope,  $q , $routeParams , $timeout , $window , $uibModal , bucketSvc , egCore , egOrg , egUser ,
523          ngToast , egConfirmDialog , egProgressDialog, itemSvc) {
524
525     $scope.setTab('view');
526     $scope.bucketId = $routeParams.id;
527
528     var query;
529     $scope.gridControls = {
530         setQuery : function(q) {
531             if (q) query = q;
532             return query;
533         }
534     };
535
536     function drawBucket() {
537         return bucketSvc.fetchBucket($scope.bucketId).then(
538             function(bucket) {
539                 var ids = bucket.items().map(
540                     function(i){return i.target_copy()}
541                 );
542                 if (ids.length) {
543                     $scope.gridControls.setQuery({id : ids});
544                 } else {
545                     $scope.gridControls.setQuery({});
546                 }
547             }
548         );
549     }
550
551     $scope.detachCopies = function(copies) {
552         bucketSvc.bucketNeedsRefresh = true;
553         var ids = copies.map(function(rec) { return rec.id; });
554
555
556         egProgressDialog.open();
557         return egCore.net.request(
558             'open-ils.actor',
559             'open-ils.actor.container.item.delete.batch',
560             egCore.auth.token(), 'copy',
561             bucketSvc.currentBucket.id(), ids
562         ).then(
563             function() {
564                 egProgressDialog.close();
565             }, // complete
566             null, // error
567             function(resp) {
568                 // Remove the items as the API responds so the UI can show
569                 // the count of items decreasing.
570                 bucketSvc.currentBucket.items(
571                     bucketSvc.currentBucket.items().filter(function(item) {
572                         return item.target_copy() != resp;
573                     })
574                 );
575             }
576         ).then(drawBucket);
577     }
578     
579     $scope.moveToPending = function(copies) {
580         angular.forEach(copies, function(copy) {
581             bucketSvc.pendingList.push(copy.id);
582         });
583         $scope.detachCopies(copies);
584     }
585
586     $scope.spawnHoldingsEdit = function() {
587         $scope.spawnEdit(true, false);
588     }
589
590     $scope.spawnCallNumberEdit = function() {
591         $scope.spawnEdit(false, true);
592     }
593
594     $scope.spawnEdit = function(hide_vols,hide_copies) {
595         var cp_list = []
596         angular.forEach($scope.gridControls.selectedItems(), function (i) {
597             cp_list.push(i.id);
598         });
599         egCore.net.request(
600             'open-ils.actor',
601             'open-ils.actor.anon_cache.set_value',
602             null, 'edit-these-copies', {
603                 record_id: 0, // false-y value for record_id disables record summary
604                 copies: cp_list,
605                 hide_vols : hide_vols,
606                 hide_copies : hide_copies
607             }
608         ).then(function(key) {
609             if (key) {
610                 var tab = (hide_vols === true) ? 'attrs' : 'holdings';
611                 var url = '/eg2/staff/cat/volcopy/' + tab + '/session/ ' + key;
612                 $timeout(function() { $window.open(url, '_blank') });
613             } else {
614                 alert('Could not create anonymous cache key!');
615             }
616         });
617     }
618
619     $scope.print_labels = function() {
620         var cp_list = []
621         angular.forEach($scope.gridControls.selectedItems(), function (i) {
622             cp_list.push(i.id);
623         })
624
625         egCore.net.request(
626             'open-ils.actor',
627             'open-ils.actor.anon_cache.set_value',
628             null, 'print-labels-these-copies', {
629                 copies : cp_list
630             }
631         ).then(function(key) {
632             if (key) {
633                 var url = egCore.env.basePath + 'cat/printlabels/' + key;
634                 $timeout(function() { $window.open(url, '_blank') });
635             } else {
636                 alert('Could not create anonymous cache key!');
637             }
638         });
639     }
640
641     $scope.showItems = function() {
642         var cp_list = []
643         angular.forEach($scope.gridControls.selectedItems(), function (i) {
644             cp_list.push(i.id);
645         })
646         var url = egCore.env.basePath + '/cat/item/search/' + cp_list.join();
647         $timeout(function() { $window.open(url, '_blank') });
648     }
649
650     $scope.requestItems = function() {
651         var copy_list = $scope.gridControls.selectedItems().map(
652             function (i) {
653                 return i.id;
654             }
655         );
656         var record_list = $scope.gridControls.selectedItems().map(
657             function (i) {
658                 return i['call_number.record.id'];
659             }
660         ).filter(function(v,i,s){ // dedup
661             return s.indexOf(v) == i;
662         });
663
664         if (copy_list.length == 0) return;
665
666         return $uibModal.open({
667             templateUrl: './cat/catalog/t_request_items',
668             backdrop: 'static',
669             animation: true,
670             controller:
671                    ['$scope','$uibModalInstance',
672             function($scope , $uibModalInstance) {
673                 $scope.user = null;
674                 $scope.first_user_fetch = true;
675
676                 $scope.hold_data = {
677                     hold_type : 'C',
678                     copy_list : copy_list,
679                     record_list : record_list,
680                     pickup_lib: egCore.org.get(egCore.auth.user().ws_ou()),
681                     user      : egCore.auth.user().id(),
682                     honor_user_settings : 
683                         egCore.hatch.getLocalItem('eg.cat.request_items.honor_user_settings')
684                 };
685
686                 egUser.get( $scope.hold_data.user ).then(function(u) {
687                     $scope.user = u;
688                     $scope.barcode = u.card().barcode();
689                     $scope.user_name = egUser.format_name(u);
690                     $scope.hold_data.user = u.id();
691                 });
692
693                 $scope.user_name = '';
694                 $scope.barcode = '';
695                 function user_preferred_pickup_lib(u) {
696                     var pickup_lib = u.home_ou();
697                     angular.forEach(u.settings(), function (s) {
698                         if (s.name() == "opac.default_pickup_location") {
699                             pickup_lib = s.value();
700                         }
701                     });
702                     return egOrg.get(pickup_lib);
703                 }
704                 $scope.$watch('barcode', function (n) {
705                     if (!$scope.first_user_fetch) {
706                         egUser.getByBarcode(n).then(function(u) {
707                             $scope.user = u;
708                             $scope.user_name = egUser.format_name(u);
709                             $scope.hold_data.user = u.id();
710                             if ($scope.hold_data.honor_user_settings) {
711                                 $scope.hold_data.pickup_lib = user_preferred_pickup_lib(u);
712                             }
713                         }, function() {
714                             $scope.user = null;
715                             $scope.user_name = '';
716                             delete $scope.hold_data.user;
717                         });
718                     }
719                     $scope.first_user_fetch = false;
720                 });
721                 $scope.$watch('hold_data.honor_user_settings', function (n) {
722                     if (n && $scope.user) {
723                         $scope.hold_data.pickup_lib = user_preferred_pickup_lib($scope.user);
724                     } else {
725                         $scope.hold_data.pickup_lib = egCore.org.get(egCore.auth.user().ws_ou());
726                     }
727                     egCore.hatch.setLocalItem('eg.cat.request_items.honor_user_settings',n);
728                 });
729
730                 $scope.ok = function(h) {
731                     var args = {
732                         patronid  : h.user,
733                         hold_type : h.hold_type,
734                         pickup_lib: h.pickup_lib.id(),
735                         depth     : 0
736                     };
737
738                     egCore.net.request(
739                         'open-ils.circ',
740                         'open-ils.circ.holds.test_and_create.batch.override',
741                         egCore.auth.token(), args,
742                         h.hold_type == 'T' ? h.record_list : h.copy_list,
743                         { 'all' : 1, 'honor_user_settings' : h.honor_user_settings }
744                     ).then(function(r) {
745                         console.log('request result',r);
746                         if (isNaN(r.result)) {
747                             if (typeof r.result.desc != 'undefined') {
748                                 ngToast.danger(r.result.desc);
749                             } else {
750                                 if (typeof r.result.last_event != 'undefined') {
751                                     ngToast.danger(r.result.last_event.desc);
752                                 } else {
753                                     ngToast.danger(egCore.strings.FAILURE_HOLD_REQUEST);
754                                 }
755                             }
756                         } else {
757                             ngToast.success(egCore.strings.SUCCESS_HOLD_REQUEST);
758                         }
759                     });
760
761                     $uibModalInstance.close();
762                 }
763
764                 $scope.cancel = function($event) {
765                     $uibModalInstance.dismiss();
766                     $event.preventDefault();
767                 }
768             }]
769         });
770     }
771
772     $scope.deleteCopiesFromCatalog = function(copies) {
773         egConfirmDialog.open(
774             egCore.strings.CONFIRM_DELETE_COPY_BUCKET_ITEMS_FROM_CATALOG,
775             '', {}
776         ).result.then(function() {
777             egProgressDialog.open();
778             var fleshed_copies = [];
779
780             var chain = $q.when();
781             angular.forEach(copies, function(i) {
782                 chain = chain.then(function() {
783                      return egCore.net.request(
784                         'open-ils.search',
785                         'open-ils.search.asset.copy.fleshed2.retrieve',
786                         i.id
787                     ).then(function(copy) {
788                         copy.ischanged(1);
789                         copy.isdeleted(1);
790                         fleshed_copies.push(copy);
791                     });
792                 });
793             });
794
795             chain.finally(function() {
796                 egCore.net.request(
797                     'open-ils.cat',
798                     'open-ils.cat.asset.copy.fleshed.batch.update',
799                     egCore.auth.token(), fleshed_copies, true
800                 ).then(function(resp) {
801                     var evt = egCore.evt.parse(resp);
802                     if (evt) {
803                         egConfirmDialog.open(
804                             egCore.strings.OVERRIDE_DELETE_COPY_BUCKET_ITEMS_FROM_CATALOG_TITLE,
805                             egCore.strings.OVERRIDE_DELETE_COPY_BUCKET_ITEMS_FROM_CATALOG_BODY,
806                             {'evt_desc': evt.desc}
807                         ).result.then(function() {
808                             egCore.net.request(
809                                 'open-ils.cat',
810                                 'open-ils.cat.asset.copy.fleshed.batch.update.override',
811                                 egCore.auth.token(), fleshed_copies, true,
812                                 { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
813                             ).then(function(resp) {
814                                 bucketSvc.bucketNeedsRefresh = true;
815                                 drawBucket();
816                             });
817                         });
818                     }
819                     bucketSvc.bucketNeedsRefresh = true;
820                     drawBucket();
821                     egProgressDialog.close();
822                 });
823             });
824         });
825     }
826
827     $scope.createCarouselFromBucket = function() {
828         if (!bucketSvc?.currentBucket?.items()?.length) {
829             return;
830         }
831         itemSvc.create_carousel_from_items(
832             bucketSvc.currentBucket.items().map(function (item) {return item.target_copy()})
833         );
834     }
835
836     $scope.transferCopies = function(copies) {
837         var xfer_target = egCore.hatch.getLocalItem('eg.cat.transfer_target_vol');
838         var copy_ids = copies.map(
839             function(curr,idx,arr) {
840                 return curr.id;
841             }
842         );
843         if (xfer_target) {
844             egCore.net.request(
845                 'open-ils.cat',
846                 'open-ils.cat.transfer_copies_to_volume',
847                 egCore.auth.token(),
848                 xfer_target,
849                 copy_ids
850             ).then(
851                 function(resp) { // oncomplete
852                     var evt = egCore.evt.parse(resp);
853                     if (evt) {
854                         egConfirmDialog.open(
855                             egCore.strings.OVERRIDE_TRANSFER_COPY_BUCKET_ITEMS_TO_MARKED_VOLUME_TITLE,
856                             egCore.strings.OVERRIDE_TRANSFER_COPY_BUCKET_ITEMS_TO_MARKED_VOLUME_BODY,
857                             {'evt_desc': evt.desc}
858                         ).result.then(function() {
859                             egCore.net.request(
860                                 'open-ils.cat',
861                                 'open-ils.cat.transfer_copies_to_volume.override',
862                                 egCore.auth.token(),
863                                 xfer_target,
864                                 copy_ids,
865                                 { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
866                             ).then(function(resp) {
867                                 bucketSvc.bucketNeedsRefresh = true;
868                                 drawBucket();
869                             });
870                         });
871                     } else {
872                         bucketSvc.bucketNeedsRefresh = true;
873                         drawBucket();
874                     }
875                 },
876                 null, // onerror
877                 null // onprogress
878             )
879         }
880     }
881
882     $scope.applyTags = function(copies) {
883         return $uibModal.open({
884             templateUrl: './cat/bucket/copy/t_apply_tags',
885             backdrop: 'static',
886             animation: true,
887             controller:
888                    ['$scope','$uibModalInstance',
889             function($scope , $uibModalInstance) {
890
891                 $scope.tag_map = [];
892
893                 egCore.pcrud.retrieveAll('cctt', {order_by : { cctt : 'label' }}, {atomic : true}).then(function(list) {
894                     $scope.tag_types = list;
895                     $scope.tag_type = $scope.tag_types[0].code(); // just pick a default
896                 });
897
898                 $scope.getTags = function(val) {
899                     return egCore.pcrud.search('acpt',
900                         {
901                             owner :  egCore.org.fullPath(egCore.auth.user().ws_ou(), true),
902                             label : { 'startwith' : {
903                                         transform: 'evergreen.lowercase',
904                                         value : [ 'evergreen.lowercase', val ]
905                                     }},
906                             tag_type : $scope.tag_type
907                         },
908                         { order_by : { 'acpt' : ['label'] } }, { atomic: true }
909                     ).then(function(list) {
910                         return list.map(function(item) {
911                             return { value: item.label(), display: item.label() + " (" + egCore.org.get(item.owner()).shortname() + ")" };
912                         });
913                     });
914                 }
915
916                 $scope.addTag = function() {
917                     var tagLabel = $scope.selectedLabel;
918                     // clear the typeahead
919                     $scope.selectedLabel = "";
920
921                     egCore.pcrud.search('acpt',
922                         {
923                             owner : egCore.org.fullPath(egCore.auth.user().ws_ou(), true),
924                             label : tagLabel,
925                             tag_type : $scope.tag_type
926                         },
927                         { order_by : { 'acpt' : ['label'] } }, { atomic: true }
928                     ).then(function(list) {
929                         if (list.length > 0) {
930                             var newMap = new egCore.idl.acptcm();
931                             newMap.isnew(1);
932                             newMap.tag(egCore.idl.Clone(list[0]));
933                             $scope.tag_map.push(newMap);
934                         }
935                     });
936                 }
937
938                 $scope.ok = function() {
939                     var promises = [];
940                     angular.forEach($scope.tag_map, function(map) {
941                         if (map.isdeleted()) return;
942                         angular.forEach(copies, function (cp) {
943                             var m = new egCore.idl.acptcm();
944                             m.isnew(1);
945                             m.copy(cp.id);
946                             m.tag(map.tag().id());
947                             promises.push(egCore.pcrud.create(m));
948                         });
949                     });
950                     return $q.all(promises).then(function(){$uibModalInstance.close()});
951                 }
952
953                 $scope.cancel = function($event) {
954                     $uibModalInstance.dismiss();
955                     $event.preventDefault();
956                 }
957             }]
958         });
959     }
960
961     // fetch the bucket;  on error show the not-allowed message
962     if ($scope.bucketId) 
963         drawBucket()['catch'](function() { $scope.forbidden = true });
964 }])