]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/cat/bucket/copy/app.js
LP#1949910: serialize deleting items from item bucket
[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',
238 function($scope,  $location,  $q,  $timeout,  $uibModal,  
239          $window,  egCore,  bucketSvc) {
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         var promise = $q.when();
266         angular.forEach(recs,
267             function(rec) {
268                 var item = new egCore.idl.ccbi();
269                 item.bucket(bucketSvc.currentBucket.id());
270                 item.target_copy(rec.id);
271                 promise = promise.then(function() {
272                     return egCore.net.request(
273                         'open-ils.actor',
274                         'open-ils.actor.container.item.create', 
275                         egCore.auth.token(), 'copy', item
276                     );
277                 }).then(function(resp) {
278                     // HACK: add the IDs of the added items so that the size
279                     // of the view list will grow (and update any UI looking at
280                     // the list size).  The data stored is inconsistent, but since
281                     // we are forcing a bucket refresh on the next rendering of 
282                     // the view pane, the list will be repaired.
283                     bucketSvc.currentBucket.items().push(resp);
284                 });
285             }
286         );
287     }
288
289     $scope.openCreateBucketDialog = function() {
290         $uibModal.open({
291             templateUrl: './cat/bucket/share/t_bucket_create',
292             backdrop: 'static',
293             controller: 
294                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
295                 $scope.focusMe = true;
296                 $scope.ok = function(args) { $uibModalInstance.close(args) }
297                 $scope.cancel = function () { $uibModalInstance.dismiss() }
298             }]
299         }).result.then(function (args) {
300             if (!args || !args.name) return;
301             bucketSvc.createBucket(args.name, args.desc).then(
302                 function(id) {
303                     if (!id) return;
304                     bucketSvc.viewList = [];
305                     bucketSvc.allBuckets = []; // reset
306                     bucketSvc.currentBucket = null;
307                     $location.path(
308                         '/cat/bucket/copy/' + $scope.tab + '/' + id);
309                 }
310             );
311         });
312     }
313
314     $scope.openEditBucketDialog = function() {
315         $uibModal.open({
316             templateUrl: './cat/bucket/share/t_bucket_edit',
317             backdrop: 'static',
318             controller: 
319                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
320                 $scope.focusMe = true;
321                 $scope.args = {
322                     name : bucketSvc.currentBucket.name(),
323                     desc : bucketSvc.currentBucket.description(),
324                     pub : bucketSvc.currentBucket.pub() == 't'
325                 };
326                 $scope.ok = function(args) { 
327                     if (!args) return;
328                     $scope.actionPending = true;
329                     args.pub = args.pub ? 't' : 'f';
330                     // close the dialog after edit has completed
331                     bucketSvc.editBucket(args).then(
332                         function() { $uibModalInstance.close() });
333                 }
334                 $scope.cancel = function () { $uibModalInstance.dismiss() }
335             }]
336         })
337     }
338
339     // opens the delete confirmation and deletes the current
340     // bucket if the user confirms.
341     $scope.openDeleteBucketDialog = function() {
342         $uibModal.open({
343             templateUrl: './cat/bucket/share/t_bucket_delete',
344             backdrop: 'static',
345             controller : 
346                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
347                 $scope.bucket = function() { return bucketSvc.currentBucket }
348                 $scope.ok = function() { $uibModalInstance.close() }
349                 $scope.cancel = function() { $uibModalInstance.dismiss() }
350             }]
351         }).result.then(function () {
352             bucketSvc.deleteBucket(bucketSvc.currentBucket.id())
353             .then(function() {
354                 bucketSvc.allBuckets = [];
355                 $location.path('/cat/bucket/copy/view');
356             });
357         });
358     }
359
360     // retrieves the requested bucket by ID
361     $scope.openSharedBucketDialog = function() {
362         $uibModal.open({
363             templateUrl: './cat/bucket/share/t_load_shared',
364             backdrop: 'static',
365             controller :
366                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
367                 $scope.focusMe = true;
368                 $scope.ok = function(args) {
369                     if (args && args.id) {
370                         $uibModalInstance.close(args.id)
371                     }
372                 }
373                 $scope.cancel = function() { $uibModalInstance.dismiss() }
374             }]
375         }).result.then(function(id) {
376             // RecordBucketCtrl $scope is not inherited by the
377             // modal, so we need to call loadBucket from the
378             // promise resolver.
379             $scope.loadBucket(id);
380         });
381     }
382
383 }])
384
385 .controller('PendingCtrl',
386        ['$scope','$routeParams','bucketSvc','egGridDataProvider', 'egCore',
387 function($scope,  $routeParams,  bucketSvc , egGridDataProvider,   egCore) {
388     $scope.setTab('pending');
389
390     $scope.context = {
391         copyNotFound : false,
392         selectPendingBC : true
393     };
394
395     var query;
396     $scope.gridControls = {
397         setQuery : function(q) {
398             if (bucketSvc.pendingList.length)
399                 return {id : bucketSvc.pendingList};
400             else
401             return null;
402         },
403         allItemsRetrieved : function() {
404             $scope.context.selectPendingBC = true;
405         }
406     }
407
408     $scope.handle_barcode_completion = function(barcode) {
409         return egCore.net.request(
410             'open-ils.actor',
411             'open-ils.actor.get_barcodes',
412             egCore.auth.token(), egCore.auth.user().ws_ou(), 
413             'asset', barcode)
414
415         .then(function(resp) {
416             // TODO: handle event during barcode lookup
417             if (evt = egCore.evt.parse(resp)) {
418                 console.error(evt.toString());
419                 return $q.reject();
420             }
421
422             // no matching barcodes: return the barcode as entered
423             // by the user (so that, e.g., checkout can fall back to
424             // precat/noncat handling)
425             if (!resp || !resp[0]) {
426                 return barcode;
427             }
428
429             // exactly one matching barcode: return it
430             if (resp.length == 1) {
431                 return resp[0].barcode;
432             }
433
434             // multiple matching barcodes: let the user pick one 
435             console.debug('multiple matching barcodes');
436             var matches = [];
437             var promises = [];
438             var final_barcode;
439             angular.forEach(resp, function(cp) {
440                 promises.push(
441                     egCore.net.request(
442                         'open-ils.circ',
443                         'open-ils.circ.copy_details.retrieve',
444                         egCore.auth.token(), cp.id
445                     ).then(function(r) {
446                         matches.push({
447                             barcode: r.copy.barcode(),
448                             title: r.mvr.title(),
449                             org_name: egCore.org.get(r.copy.circ_lib()).name(),
450                             org_shortname: egCore.org.get(r.copy.circ_lib()).shortname()
451                         });
452                     })
453                 );
454             });
455             return $q.all(promises)
456             .then(function() {
457                 return $uibModal.open({
458                     templateUrl: './circ/share/t_barcode_choice_dialog',
459                     controller:
460                         ['$scope', '$uibModalInstance',
461                         function($scope, $uibModalInstance) {
462                         $scope.matches = matches;
463                         $scope.ok = function(barcode) {
464                             $uibModalInstance.close();
465                             final_barcode = barcode;
466                         }
467                         $scope.cancel = function() {$uibModalInstance.dismiss()}
468                     }],
469                 }).result.then(function() { return final_barcode });
470             })
471         });
472     }
473
474     $scope.search = function() {
475         bucketSvc.barcodeRecords = [];
476         $scope.context.itemNotFound = false;
477
478         // clear selection so re-selecting can have an effect
479         $scope.context.selectPendingBC = false;
480
481         return $scope.handle_barcode_completion(bucketSvc.barcodeString)
482         .then(function(actual_barcode) {
483             egCore.pcrud.search(
484                 'acp',
485                 {barcode : actual_barcode, deleted : 'f'},
486                 {}
487             ).then(function(copy) {
488                 if (copy) {
489                     bucketSvc.pendingList.push(copy.id());
490                     $scope.gridControls.setQuery({id : bucketSvc.pendingList});
491                     bucketSvc.barcodeString = ''; // clear form on valid copy
492                 } else {
493                     $scope.context.itemNotFound = true;
494                     $scope.context.selectPendingBC = true;
495                 }
496             });
497         });
498     }
499
500     $scope.resetPendingList = function() {
501         bucketSvc.pendingList = [];
502         $scope.gridControls.setQuery({});
503     }
504     
505     if ($routeParams.id && 
506         (!bucketSvc.currentBucket || 
507             bucketSvc.currentBucket.id() != $routeParams.id)) {
508         // user has accessed this page cold with a bucket ID.
509         // fetch the bucket for display, then set the totalCount
510         // (also for display), but avoid fully fetching the bucket,
511         // since it's premature, in this UI.
512         bucketSvc.fetchBucket($routeParams.id);
513     }
514     $scope.gridControls.setQuery();
515 }])
516
517 .controller('ViewCtrl',
518        ['$scope','$q','$routeParams','$timeout','$window','$uibModal','bucketSvc','egCore','egOrg','egUser',
519         'ngToast','egConfirmDialog','egProgressDialog',
520 function($scope,  $q , $routeParams , $timeout , $window , $uibModal , bucketSvc , egCore , egOrg , egUser ,
521          ngToast , egConfirmDialog , egProgressDialog) {
522
523     $scope.setTab('view');
524     $scope.bucketId = $routeParams.id;
525
526     var query;
527     $scope.gridControls = {
528         setQuery : function(q) {
529             if (q) query = q;
530             return query;
531         }
532     };
533
534     function drawBucket() {
535         return bucketSvc.fetchBucket($scope.bucketId).then(
536             function(bucket) {
537                 var ids = bucket.items().map(
538                     function(i){return i.target_copy()}
539                 );
540                 if (ids.length) {
541                     $scope.gridControls.setQuery({id : ids});
542                 } else {
543                     $scope.gridControls.setQuery({});
544                 }
545             }
546         );
547     }
548
549     $scope.detachCopies = function(copies) {
550         var promises = [];
551         angular.forEach(copies, function(rec) {
552             var item = bucketSvc.currentBucket.items().filter(
553                 function(i) {
554                     return (i.target_copy() == rec.id)
555                 }
556             );
557             if (item.length)
558                 promises.push(bucketSvc.detachCopy(item[0].id()));
559         });
560
561         bucketSvc.bucketNeedsRefresh = true;
562         return $q.all(promises).then(drawBucket);
563     }
564     
565     $scope.moveToPending = function(copies) {
566         angular.forEach(copies, function(copy) {
567             bucketSvc.pendingList.push(copy.id);
568         });
569         $scope.detachCopies(copies);
570     }
571
572     $scope.spawnHoldingsEdit = function() {
573         $scope.spawnEdit(true, false);
574     }
575
576     $scope.spawnCallNumberEdit = function() {
577         $scope.spawnEdit(false, true);
578     }
579
580     $scope.spawnEdit = function(hide_vols,hide_copies) {
581         var cp_list = []
582         angular.forEach($scope.gridControls.selectedItems(), function (i) {
583             cp_list.push(i.id);
584         });
585         egCore.net.request(
586             'open-ils.actor',
587             'open-ils.actor.anon_cache.set_value',
588             null, 'edit-these-copies', {
589                 record_id: 0, // false-y value for record_id disables record summary
590                 copies: cp_list,
591                 hide_vols : hide_vols,
592                 hide_copies : hide_copies
593             }
594         ).then(function(key) {
595             if (key) {
596                 var url = egCore.env.basePath + 'cat/volcopy/' + key;
597                 $timeout(function() { $window.open(url, '_blank') });
598             } else {
599                 alert('Could not create anonymous cache key!');
600             }
601         });
602     }
603
604     $scope.print_labels = function() {
605         var cp_list = []
606         angular.forEach($scope.gridControls.selectedItems(), function (i) {
607             cp_list.push(i.id);
608         })
609
610         egCore.net.request(
611             'open-ils.actor',
612             'open-ils.actor.anon_cache.set_value',
613             null, 'print-labels-these-copies', {
614                 copies : cp_list
615             }
616         ).then(function(key) {
617             if (key) {
618                 var url = egCore.env.basePath + 'cat/printlabels/' + key;
619                 $timeout(function() { $window.open(url, '_blank') });
620             } else {
621                 alert('Could not create anonymous cache key!');
622             }
623         });
624     }
625
626     $scope.showItems = function() {
627         var cp_list = []
628         angular.forEach($scope.gridControls.selectedItems(), function (i) {
629             cp_list.push(i.id);
630         })
631         var url = egCore.env.basePath + '/cat/item/search/' + cp_list.join();
632         $timeout(function() { $window.open(url, '_blank') });
633     }
634
635     $scope.requestItems = function() {
636         var copy_list = $scope.gridControls.selectedItems().map(
637             function (i) {
638                 return i.id;
639             }
640         );
641         var record_list = $scope.gridControls.selectedItems().map(
642             function (i) {
643                 return i['call_number.record.id'];
644             }
645         ).filter(function(v,i,s){ // dedup
646             return s.indexOf(v) == i;
647         });
648
649         if (copy_list.length == 0) return;
650
651         return $uibModal.open({
652             templateUrl: './cat/catalog/t_request_items',
653             backdrop: 'static',
654             animation: true,
655             controller:
656                    ['$scope','$uibModalInstance',
657             function($scope , $uibModalInstance) {
658                 $scope.user = null;
659                 $scope.first_user_fetch = true;
660
661                 $scope.hold_data = {
662                     hold_type : 'C',
663                     copy_list : copy_list,
664                     record_list : record_list,
665                     pickup_lib: egCore.org.get(egCore.auth.user().ws_ou()),
666                     user      : egCore.auth.user().id(),
667                     honor_user_settings : 
668                         egCore.hatch.getLocalItem('eg.cat.request_items.honor_user_settings')
669                 };
670
671                 egUser.get( $scope.hold_data.user ).then(function(u) {
672                     $scope.user = u;
673                     $scope.barcode = u.card().barcode();
674                     $scope.user_name = egUser.format_name(u);
675                     $scope.hold_data.user = u.id();
676                 });
677
678                 $scope.user_name = '';
679                 $scope.barcode = '';
680                 function user_preferred_pickup_lib(u) {
681                     var pickup_lib = u.home_ou();
682                     angular.forEach(u.settings(), function (s) {
683                         if (s.name() == "opac.default_pickup_location") {
684                             pickup_lib = s.value();
685                         }
686                     });
687                     return egOrg.get(pickup_lib);
688                 }
689                 $scope.$watch('barcode', function (n) {
690                     if (!$scope.first_user_fetch) {
691                         egUser.getByBarcode(n).then(function(u) {
692                             $scope.user = u;
693                             $scope.user_name = egUser.format_name(u);
694                             $scope.hold_data.user = u.id();
695                             if ($scope.hold_data.honor_user_settings) {
696                                 $scope.hold_data.pickup_lib = user_preferred_pickup_lib(u);
697                             }
698                         }, function() {
699                             $scope.user = null;
700                             $scope.user_name = '';
701                             delete $scope.hold_data.user;
702                         });
703                     }
704                     $scope.first_user_fetch = false;
705                 });
706                 $scope.$watch('hold_data.honor_user_settings', function (n) {
707                     if (n && $scope.user) {
708                         $scope.hold_data.pickup_lib = user_preferred_pickup_lib($scope.user);
709                     } else {
710                         $scope.hold_data.pickup_lib = egCore.org.get(egCore.auth.user().ws_ou());
711                     }
712                     egCore.hatch.setLocalItem('eg.cat.request_items.honor_user_settings',n);
713                 });
714
715                 $scope.ok = function(h) {
716                     var args = {
717                         patronid  : h.user,
718                         hold_type : h.hold_type,
719                         pickup_lib: h.pickup_lib.id(),
720                         depth     : 0
721                     };
722
723                     egCore.net.request(
724                         'open-ils.circ',
725                         'open-ils.circ.holds.test_and_create.batch.override',
726                         egCore.auth.token(), args,
727                         h.hold_type == 'T' ? h.record_list : h.copy_list,
728                         { 'all' : 1, 'honor_user_settings' : h.honor_user_settings }
729                     ).then(function(r) {
730                         console.log('request result',r);
731                         if (isNaN(r.result)) {
732                             if (typeof r.result.desc != 'undefined') {
733                                 ngToast.danger(r.result.desc);
734                             } else {
735                                 if (typeof r.result.last_event != 'undefined') {
736                                     ngToast.danger(r.result.last_event.desc);
737                                 } else {
738                                     ngToast.danger(egCore.strings.FAILURE_HOLD_REQUEST);
739                                 }
740                             }
741                         } else {
742                             ngToast.success(egCore.strings.SUCCESS_HOLD_REQUEST);
743                         }
744                     });
745
746                     $uibModalInstance.close();
747                 }
748
749                 $scope.cancel = function($event) {
750                     $uibModalInstance.dismiss();
751                     $event.preventDefault();
752                 }
753             }]
754         });
755     }
756
757     $scope.deleteCopiesFromCatalog = function(copies) {
758         egConfirmDialog.open(
759             egCore.strings.CONFIRM_DELETE_COPY_BUCKET_ITEMS_FROM_CATALOG,
760             '', {}
761         ).result.then(function() {
762             egProgressDialog.open();
763             var fleshed_copies = [];
764
765             var chain = $q.when();
766             angular.forEach(copies, function(i) {
767                 chain = chain.then(function() {
768                      return egCore.net.request(
769                         'open-ils.search',
770                         'open-ils.search.asset.copy.fleshed2.retrieve',
771                         i.id
772                     ).then(function(copy) {
773                         copy.ischanged(1);
774                         copy.isdeleted(1);
775                         fleshed_copies.push(copy);
776                     });
777                 });
778             });
779
780             chain.finally(function() {
781                 egCore.net.request(
782                     'open-ils.cat',
783                     'open-ils.cat.asset.copy.fleshed.batch.update',
784                     egCore.auth.token(), fleshed_copies, true
785                 ).then(function(resp) {
786                     var evt = egCore.evt.parse(resp);
787                     if (evt) {
788                         egConfirmDialog.open(
789                             egCore.strings.OVERRIDE_DELETE_COPY_BUCKET_ITEMS_FROM_CATALOG_TITLE,
790                             egCore.strings.OVERRIDE_DELETE_COPY_BUCKET_ITEMS_FROM_CATALOG_BODY,
791                             {'evt_desc': evt.desc}
792                         ).result.then(function() {
793                             egCore.net.request(
794                                 'open-ils.cat',
795                                 'open-ils.cat.asset.copy.fleshed.batch.update.override',
796                                 egCore.auth.token(), fleshed_copies, true,
797                                 { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
798                             ).then(function(resp) {
799                                 bucketSvc.bucketNeedsRefresh = true;
800                                 drawBucket();
801                             });
802                         });
803                     }
804                     bucketSvc.bucketNeedsRefresh = true;
805                     drawBucket();
806                     egProgressDialog.close();
807                 });
808             });
809         });
810     }
811
812     $scope.transferCopies = function(copies) {
813         var xfer_target = egCore.hatch.getLocalItem('eg.cat.transfer_target_vol');
814         var copy_ids = copies.map(
815             function(curr,idx,arr) {
816                 return curr.id;
817             }
818         );
819         if (xfer_target) {
820             egCore.net.request(
821                 'open-ils.cat',
822                 'open-ils.cat.transfer_copies_to_volume',
823                 egCore.auth.token(),
824                 xfer_target,
825                 copy_ids
826             ).then(
827                 function(resp) { // oncomplete
828                     var evt = egCore.evt.parse(resp);
829                     if (evt) {
830                         egConfirmDialog.open(
831                             egCore.strings.OVERRIDE_TRANSFER_COPY_BUCKET_ITEMS_TO_MARKED_VOLUME_TITLE,
832                             egCore.strings.OVERRIDE_TRANSFER_COPY_BUCKET_ITEMS_TO_MARKED_VOLUME_BODY,
833                             {'evt_desc': evt.desc}
834                         ).result.then(function() {
835                             egCore.net.request(
836                                 'open-ils.cat',
837                                 'open-ils.cat.transfer_copies_to_volume.override',
838                                 egCore.auth.token(),
839                                 xfer_target,
840                                 copy_ids,
841                                 { events: ['TITLE_LAST_COPY', 'COPY_DELETE_WARNING'] }
842                             ).then(function(resp) {
843                                 bucketSvc.bucketNeedsRefresh = true;
844                                 drawBucket();
845                             });
846                         });
847                     } else {
848                         bucketSvc.bucketNeedsRefresh = true;
849                         drawBucket();
850                     }
851                 },
852                 null, // onerror
853                 null // onprogress
854             )
855         }
856     }
857
858     $scope.applyTags = function(copies) {
859         return $uibModal.open({
860             templateUrl: './cat/bucket/copy/t_apply_tags',
861             backdrop: 'static',
862             animation: true,
863             controller:
864                    ['$scope','$uibModalInstance',
865             function($scope , $uibModalInstance) {
866
867                 $scope.tag_map = [];
868
869                 egCore.pcrud.retrieveAll('cctt', {order_by : { cctt : 'label' }}, {atomic : true}).then(function(list) {
870                     $scope.tag_types = list;
871                     $scope.tag_type = $scope.tag_types[0].code(); // just pick a default
872                 });
873
874                 $scope.getTags = function(val) {
875                     return egCore.pcrud.search('acpt',
876                         {
877                             owner :  egCore.org.fullPath(egCore.auth.user().ws_ou(), true),
878                             label : { 'startwith' : {
879                                         transform: 'evergreen.lowercase',
880                                         value : [ 'evergreen.lowercase', val ]
881                                     }},
882                             tag_type : $scope.tag_type
883                         },
884                         { order_by : { 'acpt' : ['label'] } }, { atomic: true }
885                     ).then(function(list) {
886                         return list.map(function(item) {
887                             return { value: item.label(), display: item.label() + " (" + egCore.org.get(item.owner()).shortname() + ")" };
888                         });
889                     });
890                 }
891
892                 $scope.addTag = function() {
893                     var tagLabel = $scope.selectedLabel;
894                     // clear the typeahead
895                     $scope.selectedLabel = "";
896
897                     egCore.pcrud.search('acpt',
898                         {
899                             owner : egCore.org.fullPath(egCore.auth.user().ws_ou(), true),
900                             label : tagLabel,
901                             tag_type : $scope.tag_type
902                         },
903                         { order_by : { 'acpt' : ['label'] } }, { atomic: true }
904                     ).then(function(list) {
905                         if (list.length > 0) {
906                             var newMap = new egCore.idl.acptcm();
907                             newMap.isnew(1);
908                             newMap.tag(egCore.idl.Clone(list[0]));
909                             $scope.tag_map.push(newMap);
910                         }
911                     });
912                 }
913
914                 $scope.ok = function() {
915                     var promises = [];
916                     angular.forEach($scope.tag_map, function(map) {
917                         if (map.isdeleted()) return;
918                         angular.forEach(copies, function (cp) {
919                             var m = new egCore.idl.acptcm();
920                             m.isnew(1);
921                             m.copy(cp.id);
922                             m.tag(map.tag().id());
923                             promises.push(egCore.pcrud.create(m));
924                         });
925                     });
926                     return $q.all(promises).then(function(){$uibModalInstance.close()});
927                 }
928
929                 $scope.cancel = function($event) {
930                     $uibModalInstance.dismiss();
931                     $event.preventDefault();
932                 }
933             }]
934         });
935     }
936
937     // fetch the bucket;  on error show the not-allowed message
938     if ($scope.bucketId) 
939         drawBucket()['catch'](function() { $scope.forbidden = true });
940 }])