]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/circ/patron/bucket/app.js
LP#1689608: Batch user editing
[working/Evergreen.git] / Open-ILS / web / js / ui / default / staff / circ / patron / bucket / app.js
1 /**
2  * User 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('egCatUserBuckets', 
16     ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod', 'egUserBucketMod', 'ngToast'])
17
18 .config(function($routeProvider, $locationProvider, $compileProvider) {
19     $locationProvider.html5Mode(true);
20     $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|blob):/); // grid export
21
22     var resolver = {delay : function(egStartup) {return egStartup.go()}};
23
24     $routeProvider.when('/circ/patron/bucket/add/:id', {
25         templateUrl: './circ/patron/bucket/t_pending',
26         controller: 'PendingCtrl',
27         resolve : resolver
28     });
29
30     $routeProvider.when('/circ/patron/bucket/add', {
31         templateUrl: './circ/patron/bucket/t_pending',
32         controller: 'PendingCtrl',
33         resolve : resolver
34     });
35
36     $routeProvider.when('/circ/patron/bucket/view/:id', {
37         templateUrl: './circ/patron/bucket/t_view',
38         controller: 'ViewCtrl',
39         resolve : resolver
40     });
41
42     $routeProvider.when('/circ/patron/bucket/view', {
43         templateUrl: './circ/patron/bucket/t_view',
44         controller: 'ViewCtrl',
45         resolve : resolver
46     });
47
48     // default page / bucket view
49     $routeProvider.otherwise({redirectTo : '/circ/patron/bucket/view'});
50 })
51
52 /**
53  * Top-level controller.  
54  * Hosts functions needed by all controllers.
55  */
56 .controller('UserBucketCtrl',
57        ['$scope','$location','$q','$timeout','$uibModal',
58         '$window','egCore','bucketSvc','ngToast',
59 function($scope,  $location,  $q,  $timeout,  $uibModal,  
60          $window,  egCore,  bucketSvc , ngToast) {
61
62     $scope.bucketSvc = bucketSvc;
63     $scope.bucket = function() { return bucketSvc.currentBucket }
64
65     // tabs: add, view
66     $scope.setTab = function(tab) { 
67         $scope.tab = tab;
68
69         // for bucket selector; must be called after route resolve
70         bucketSvc.fetchUserBuckets(); 
71     };
72
73     $scope.loadBucketFromMenu = function(item, bucket) {
74         if (bucket) return $scope.loadBucket(bucket.id());
75     }
76
77     $scope.loadBucket = function(id) {
78         $location.path(
79             '/circ/patron/bucket/' + 
80                 $scope.tab + '/' + encodeURIComponent(id));
81     }
82
83     $scope.addToBucket = function(recs) {
84         if (recs.length == 0) return;
85         bucketSvc.bucketNeedsRefresh = true;
86
87         angular.forEach(recs,
88             function(rec) {
89                 var item = new egCore.idl.cubi();
90                 item.bucket(bucketSvc.currentBucket.id());
91                 item.target_user(rec.id);
92                 egCore.net.request(
93                     'open-ils.actor',
94                     'open-ils.actor.container.item.create', 
95                     egCore.auth.token(), 'user', item
96                 ).then(function(resp) {
97
98                     // HACK: add the IDs of the added items so that the size
99                     // of the view list will grow (and update any UI looking at
100                     // the list size).  The data stored is inconsistent, but since
101                     // we are forcing a bucket refresh on the next rendering of 
102                     // the view pane, the list will be repaired.
103                     bucketSvc.currentBucket.items().push(resp);
104                 });
105             }
106         );
107         $scope.resetPendingList();
108     }
109
110     $scope.openCreateBucketDialog = function() {
111         $uibModal.open({
112             templateUrl: './circ/patron/bucket/t_bucket_create',
113             controller: 
114                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
115                 $scope.focusMe = true;
116                 $scope.ok = function(args) { $uibModalInstance.close(args) }
117                 $scope.cancel = function () { $uibModalInstance.dismiss() }
118             }]
119         }).result.then(function (args) {
120             if (!args || !args.name) return;
121             bucketSvc.createBucket(args.name, args.desc).then(
122                 function(id) {
123                     if (!id) return;
124                     bucketSvc.viewList = [];
125                     bucketSvc.allBuckets = []; // reset
126                     bucketSvc.currentBucket = null;
127                     $location.path(
128                         '/circ/patron/bucket/' + $scope.tab + '/' + id);
129                 }
130             );
131         });
132     }
133
134     $scope.openEditBucketDialog = function() {
135         $uibModal.open({
136             templateUrl: './circ/patron/bucket/t_bucket_edit',
137             controller: 
138                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
139                 $scope.focusMe = true;
140                 $scope.args = {
141                     name : bucketSvc.currentBucket.name(),
142                     desc : bucketSvc.currentBucket.description(),
143                     pub : bucketSvc.currentBucket.pub() == 't'
144                 };
145                 $scope.ok = function(args) { 
146                     if (!args) return;
147                     $scope.actionPending = true;
148                     args.pub = args.pub ? 't' : 'f';
149                     // close the dialog after edit has completed
150                     bucketSvc.editBucket(args).then(
151                         function() { $uibModalInstance.close() });
152                 }
153                 $scope.cancel = function () { $uibModalInstance.dismiss() }
154             }]
155         })
156     }
157
158     // opens the delete confirmation and deletes the current
159     // bucket if the user confirms.
160     $scope.openDeleteBucketDialog = function() {
161         $uibModal.open({
162             templateUrl: './circ/patron/bucket/t_bucket_delete',
163             controller : 
164                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
165                 $scope.bucket = function() { return bucketSvc.currentBucket }
166                 $scope.ok = function() { $uibModalInstance.close() }
167                 $scope.cancel = function() { $uibModalInstance.dismiss() }
168             }]
169         }).result.then(function () {
170             bucketSvc.deleteBucket(bucketSvc.currentBucket.id())
171             .then(function() {
172                 bucketSvc.allBuckets = [];
173                 $location.path('/circ/patron/bucket/view');
174             });
175         });
176     }
177
178     // retrieves the requested bucket by ID
179     $scope.openSharedBucketDialog = function() {
180         $uibModal.open({
181             templateUrl: './circ/patron/bucket/t_load_shared',
182             controller :
183                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
184                 $scope.focusMe = true;
185                 $scope.ok = function(args) {
186                     if (args && args.id) {
187                         $uibModalInstance.close(args.id)
188                     }
189                 }
190                 $scope.cancel = function() { $uibModalInstance.dismiss() }
191             }]
192         }).result.then(function(id) {
193             // RecordBucketCtrl $scope is not inherited by the
194             // modal, so we need to call loadBucket from the
195             // promise resolver.
196             $scope.loadBucket(id);
197         });
198     }
199
200 }])
201
202 .controller('PendingCtrl',
203        ['$scope','$routeParams','bucketSvc','egGridDataProvider', 'egCore','ngToast','$q',
204 function($scope,  $routeParams,  bucketSvc , egGridDataProvider,   egCore , ngToast , $q) {
205     $scope.setTab('add');
206
207     var query;
208     $scope.gridControls = {
209         setQuery : function(q) {
210             if (bucketSvc.pendingList.length)
211                 return {id : bucketSvc.pendingList};
212             else
213             return null;
214         }
215     }
216
217     $scope.$watch('barcodesFromFile', function(newVal, oldVal) {
218         if (newVal && newVal != oldVal) {
219             var promises = [];
220             // $scope.resetPendingList(); // ??? Add instead of replace
221             angular.forEach(newVal.split(/\n/), function(line) {
222                 if (!line) return;
223                 // scrub any trailing spaces or commas from the barcode
224                 line = line.replace(/(.*?)($|\s.*|,.*)/,'$1');
225                 promises.push(egCore.pcrud.search(
226                     'ac',
227                     {barcode : line},
228                     {}
229                 ).then(null, null, function(card) {
230                     bucketSvc.pendingList.push(card.usr());
231                 }));
232             });
233
234             $q.all(promises).then(function () {
235                 $scope.gridControls.setQuery({id : bucketSvc.pendingList});
236             });
237         }
238     });
239
240     $scope.search = function() {
241         bucketSvc.barcodeRecords = [];
242
243         egCore.pcrud.search(
244             'ac',
245             {barcode : bucketSvc.barcodeString},
246             {}
247         ).then(null, null, function(card) {
248             bucketSvc.pendingList.push(card.usr());
249             $scope.gridControls.setQuery({id : bucketSvc.pendingList});
250         });
251         bucketSvc.barcodeString = '';
252     }
253
254     $scope.resetPendingList = function() {
255         bucketSvc.pendingList = [];
256         $scope.gridControls.setQuery({});
257     }
258
259     $scope.$parent.resetPendingList = $scope.resetPendingList;
260     
261     if ($routeParams.id && 
262         (!bucketSvc.currentBucket || 
263             bucketSvc.currentBucket.id() != $routeParams.id)) {
264         // user has accessed this page cold with a bucket ID.
265         // fetch the bucket for display, then set the totalCount
266         // (also for display), but avoid fully fetching the bucket,
267         // since it's premature, in this UI.
268         bucketSvc.fetchBucket($routeParams.id);
269     }
270     $scope.gridControls.setQuery();
271 }])
272
273 .controller('ViewCtrl',
274        ['$scope','$q','$routeParams','$timeout','$window','$uibModal','bucketSvc','egCore','egUser',
275         'egConfirmDialog','egPerm','ngToast','$filter',
276 function($scope,  $q , $routeParams , $timeout , $window , $uibModal , bucketSvc , egCore , egUser ,
277          egConfirmDialog , egPerm , ngToast , $filter) {
278
279     $scope.setTab('view');
280     $scope.bucketId = $routeParams.id;
281
282     var query;
283     $scope.gridControls = {
284         setQuery : function(q) {
285             if (q) query = q;
286             return query;
287         }
288     };
289
290     $scope.modifyStatcats = function() {
291         bucketSvc.bucketNeedsRefresh = true;
292
293         $uibModal.open({
294             templateUrl: './circ/patron/bucket/t_update_statcats',
295             controller: 
296                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
297                 $scope.running = false;
298                 $scope.complete = false;
299                 $scope.states = [];
300
301                 $scope.modal = $uibModalInstance;
302                 $scope.ok = function(args) { $uibModalInstance.close() }
303                 $scope.cancel = function () { $uibModalInstance.dismiss() }
304
305                 $scope.current_bucket = bucketSvc.currentBucket;
306
307                 egCore.net.request(
308                     'open-ils.circ',
309                     'open-ils.circ.stat_cat.actor.retrieve.all',
310                     egCore.auth.token(), egCore.auth.user().ws_ou()
311                 ).then(function(cats) {
312                     cats = cats.sort(function(a, b) {
313                         return a.name() < b.name() ? -1 : 1});
314                     angular.forEach(cats, function(cat) {
315                         cat.new_value = '';
316                         cat.allow_freetext(parseInt(cat.allow_freetext())); // just to be sure
317                         cat.entries(
318                             cat.entries().sort(function(a,b) {
319                                 return a.value() < b.value() ? -1 : 1
320                             })
321                         );
322                     });
323                     $scope.stat_cats = cats;
324                 });
325
326                 // This handels the progress magic instead of a normal close handler
327                 $scope.$on('modal.closing', function(event, reason, closed) {
328                     if (!closed) return; // dismissed
329                     if ($scope.complete) return; // already done
330
331                     $scope.running = true;
332
333                     var changes = {remove:[], apply:{}};
334                     angular.forEach($scope.stat_cats, function (sc) {
335                         if (sc.delete_me) {
336                             changes.remove.push(sc.id());
337                         } else if (sc.new_value) {
338                             changes.apply[sc.id()] = sc.new_value;
339                         }
340                     });
341
342                     egCore.net.request(
343                         'open-ils.actor',
344                         'open-ils.actor.container.user.batch_statcat_apply',
345                         egCore.auth.token(), bucketSvc.currentBucket.id(), changes
346                     ).then(
347                         function () {
348                             $scope.complete = true;
349                             $scope.modal.close();
350                             drawBucket();
351                         },
352                         function (err) { console.log('User edit error: ' + err); },
353                         function (p) {
354                             if (p.error) {
355                                 ngToast.warning(p.error);
356                             }
357                             if (p.stage == 'COMPLETE') return;
358
359                             p.label = egCore.strings[p.stage];
360                             if (!p.max) {
361                                 p.max = 1;
362                                 p.count = 1;
363                             }
364                             $scope.states[p.ord] = p;
365                         }
366                     );
367
368                     return event.preventDefault();
369                 });
370             }]
371         });
372     }
373
374
375     function drawBucket() {
376         return bucketSvc.fetchBucket($scope.bucketId).then(
377             function(bucket) {
378                 var ids = bucket.items().map(
379                     function(i){return i.target_user()}
380                 );
381                 if (ids.length) {
382                     $scope.gridControls.setQuery({id : ids});
383                 } else {
384                     $scope.gridControls.setQuery({});
385                 }
386             }
387         );
388     }
389
390     $scope.no_update_perms = true;
391     $scope.noUpdatePerms = function () { return $scope.no_update_perms; }
392
393     egPerm.hasPermHere(['UPDATE_USER']).then(
394         function (hash) {
395             if (Object.keys(hash).length == 0) return;
396
397             var one_false = false;
398             angular.forEach(hash, function(has) {
399                 if (!has) one_false = true;
400             });
401
402             if (!one_false) $scope.no_update_perms = false;
403         }
404     );
405
406     function annotate_groups(grps) {
407         angular.forEach(grps, function (g) {
408             if (!g.hasOwnProperty('cannot_use')) {
409                 if (g.usergroup() == 'f') {
410                     g.cannot_use = true;
411                 } else if (g.application_perm) {
412                     egPerm.hasPermHere(['EVERYTHING',g.application_perm]).then(
413                         function (hash) {
414                             if (Object.keys(hash).length == 0) {
415                                 g.cannot_use = true;
416                                 return;
417                             }
418
419                             var one_false = false;
420                             angular.forEach(hash, function(has) {
421                                 if (has) g.cannot_use = false;
422                             });
423                         }
424                     );
425                 } else {
426                     g.cannot_use = false;
427                 }
428             }
429         });
430     }
431
432     $scope.viewChangesets = function() {
433         bucketSvc.bucketNeedsRefresh = true;
434
435         $uibModal.open({
436             templateUrl: './circ/patron/bucket/t_changesets',
437             controller: 
438                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
439                 $scope.running = false;
440                 $scope.complete = false;
441                 $scope.states = [];
442
443                 $scope.focusMe = true;
444                 $scope.modal = $uibModalInstance;
445                 $scope.ok = function() { $uibModalInstance.close() }
446                 $scope.cancel = function () { $uibModalInstance.dismiss() }
447
448                 $scope.current_bucket = bucketSvc.currentBucket;
449                 $scope.fieldset_groups = [];
450
451                 $scope.deleteChangeset = function (grp) {
452                     egCore.pcrud.remove(grp).then(
453                         function () {
454                             if (grp.rollback_group()) {
455                                 egCore.pcrud
456                                     .retrieve('afsg',grp.rollback_group())
457                                     .then(function(g) {
458                                         egCore.pcrud.remove(g)
459                                             .then( function () { refresh_groups() } );
460                                     });
461                             }
462                         }
463                     );
464                     return event.preventDefault();
465                 }
466
467                 function refresh_groups () {
468                     $scope.fieldset_groups = [];
469                     egCore.pcrud.search('afsg',{
470                         rollback_group : { '>' : 0 },
471                         container      : bucketSvc.currentBucket.id(),
472                         container_type : 'user'
473                     } ).then( null,null,function(g) {
474                         $scope.fieldset_groups.push(g);
475                     });
476                 }
477                 refresh_groups();
478
479             }]
480         });
481     }
482
483     $scope.applyRollback = function() {
484         bucketSvc.bucketNeedsRefresh = true;
485
486         $uibModal.open({
487             templateUrl: './circ/patron/bucket/t_rollback',
488             controller: 
489                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
490                 $scope.running = false;
491                 $scope.complete = false;
492                 $scope.states = [];
493                 $scope.revert_me = null;
494
495                 $scope.focusMe = true;
496                 $scope.modal = $uibModalInstance;
497                 $scope.ok = function(args) { $uibModalInstance.close() }
498                 $scope.cancel = function () { $uibModalInstance.dismiss() }
499
500                 $scope.current_bucket = bucketSvc.currentBucket;
501                 $scope.revertable_fieldset_groups = [];
502
503                 egCore.pcrud.search('afsg',{
504                     rollback_group : { '>' : 0},
505                     rollback_time  : null,
506                     container      : bucketSvc.currentBucket.id(),
507                     container_type : 'user'
508                 } ).then( null,null,function(g) {
509                     $scope.revertable_fieldset_groups.push(g);
510                 });
511
512                 // This handels the progress magic instead of a normal close handler
513                 $scope.$on('modal.closing', function(event, reason, closed) {
514                     if (!$scope.revert_me) return;
515                     if (!closed) return; // dismissed
516                     if ($scope.complete) return; // already done
517
518                     $scope.running = true;
519
520                     var last_stage = '';
521                     egCore.net.request(
522                         'open-ils.actor',
523                         'open-ils.actor.container.user.apply_rollback',
524                         egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.revert_me.id()
525                     ).then(
526                         function () {
527                             $scope.complete = true;
528                             $scope.modal.close();
529                             drawBucket();
530                         },
531                         function (err) { console.log('User edit error: ' + err); },
532                         function (p) {
533                             last_stage = p.stage;
534                             if (p.error) {
535                                 ngToast.warning(p.error);
536                             }
537                             if (p.stage == 'COMPLETE') return;
538
539                             p.label = egCore.strings[p.stage];
540                             if (!p.max) {
541                                 p.max = 1;
542                                 p.count = 1;
543                             }
544                             $scope.states[p.ord] = p;
545                         }
546                     ).then(function() {
547                         if (last_stage != 'COMPLETE')
548                             ngToast.warning(egCore.strings.BATCH_FAILED);
549                     });
550
551                     return event.preventDefault();
552                 });
553             }]
554         });
555     }
556
557     $scope.updateAllUsers = function() {
558         bucketSvc.bucketNeedsRefresh = true;
559
560         $uibModal.open({
561             templateUrl: './circ/patron/bucket/t_update_all',
562             controller: 
563                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
564                 $scope.running = false;
565                 $scope.complete = false;
566                 $scope.states = [];
567                 $scope.home_ou_name = '';
568                 $scope.args = {home_ou:null};
569                 $scope.focusMe = true;
570                 $scope.modal = $uibModalInstance;
571                 $scope.ok = function(args) { $uibModalInstance.close() }
572                 $scope.cancel = function () { $uibModalInstance.dismiss() }
573
574                 $scope.disable_home_org = function(org_id) {
575                     if (!org_id) return;
576                     var org = egCore.org.get(org_id);
577                     return (
578                         org &&
579                         org.ou_type() &&
580                         org.ou_type().can_have_users() == 'f'
581                     );
582                 }
583
584                 $scope.pgt_depth = function(grp) {
585                     var d = 0;
586                     while (grp = egCore.env.pgt.map[grp.parent()]) d++;
587                     return d;
588                 }
589
590                 if (egCore.env.cnal) {
591                     $scope.net_access_levels = egCore.env.cnal.list;
592                 } else {
593                     egCore.pcrud.retrieveAll('cnal', {}, {atomic : true})
594                     .then(function(types) {
595                         egCore.env.absorbList(types, 'cnal')
596                         $scope.net_access_levels = egCore.env.cnal.list;
597                     });
598                 }
599
600                 if (egCore.env.pgt) {
601                     $scope.profiles = egCore.env.pgt.list;
602                     annotate_groups($scope.profiles);
603                 } else {
604                     egCore.pcrud.search('pgt', {parent : null}, 
605                         {flesh : -1, flesh_fields : {pgt : ['children']}}
606                     ).then(
607                         function(tree) {
608                             egCore.env.absorbTree(tree, 'pgt')
609                             $scope.profiles = egCore.env.pgt.list;
610                             annotate_groups($scope.profiles);
611                         }
612                     );
613                 }
614
615                 $scope.unset_field = function (event,field) {
616                     $scope.args[field] = null;
617                     return event.preventDefault();
618                 }
619
620                 // This handels the progress magic instead of a normal close handler
621                 $scope.$on('modal.closing', function(event, reason, closed) {
622                     if (!$scope.args || !$scope.args.name) return;
623                     if (!closed) return; // dismissed
624                     if ($scope.complete) return; // already done
625
626                     $scope.running = true;
627
628                     // XXX fix up $scope.args values here
629                     if ($scope.args.home_ou) {
630                         $scope.args.home_ou = $scope.args.home_ou.id();
631                     }
632                     if ($scope.args.net_access_level) {
633                         $scope.args.net_access_level = $scope.args.net_access_level.id();
634                     }
635                     if ($scope.args.profile) {
636                         $scope.args.profile = $scope.args.profile.id();
637                     }
638                     if ($scope.args.expire_date) {
639                         $scope.args.expire_date = $scope.args.expire_date.toJSON().substr(0,10);
640                     }
641
642                     for (var key in $scope.args) {
643                         if (!$scope.args[key] && $scope.args[key] !== 0) {
644                             delete $scope.args[key];
645                         }
646                     }
647
648                     var last_stage = '';
649                     egCore.net.request(
650                         'open-ils.actor',
651                         'open-ils.actor.container.user.batch_edit',
652                         egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.args.name, $scope.args
653                     ).then(
654                         function () {
655                             $scope.complete = true;
656                             $scope.modal.close();
657                             drawBucket();
658                         },
659                         function (err) { console.log('User edit error: ' + err); },
660                         function (p) {
661                             last_stage = p.stage;
662                             if (p.error) {
663                                 ngToast.warning(p.error);
664                             }
665                             if (p.stage == 'COMPLETE') return;
666
667                             p.label = egCore.strings[p.stage];
668                             if (!p.max) {
669                                 p.max = 1;
670                                 p.count = 1;
671                             }
672                             $scope.states[p.ord] = p;
673                         }
674                     ).then(function() {
675                         if (last_stage != 'COMPLETE')
676                             ngToast.warning(egCore.strings.BATCH_FAILED);
677                     });
678
679                     return event.preventDefault();
680                 });
681             }]
682         });
683     }
684
685     $scope.no_delete_perms = true;
686     $scope.noDeletePerms = function () { return $scope.no_delete_perms; }
687
688     egPerm.hasPermHere(['UPDATE_USER','DELETE_USER']).then(
689         function (hash) {
690             if (Object.keys(hash).length == 0) return;
691
692             var one_false = false;
693             angular.forEach(hash, function(has) {
694                 if (!has) one_false = true;
695             });
696
697             if (!one_false) $scope.no_delete_perms = false;
698         }
699     );
700
701     $scope.deleteAllUsers = function() {
702         bucketSvc.bucketNeedsRefresh = true;
703
704         $uibModal.open({
705             templateUrl: './circ/patron/bucket/t_delete_all',
706             controller: 
707                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
708                 $scope.running = false;
709                 $scope.complete = false;
710                 $scope.states = [];
711                 $scope.args = {};
712                 $scope.focusMe = true;
713                 $scope.modal = $uibModalInstance;
714                 $scope.ok = function(args) { $uibModalInstance.close() }
715                 $scope.cancel = function () { $uibModalInstance.dismiss() }
716
717                 // This handels the progress magic instead of a normal close handler
718                 $scope.$on('modal.closing', function(event, reason, closed) {
719                     if (!$scope.args || !$scope.args.name) return;
720                     if (!closed) return; // dismissed
721                     if ($scope.complete) return; // already done
722
723                     $scope.running = true;
724
725                     var last_stage = '';
726                     egCore.net.request(
727                         'open-ils.actor',
728                         'open-ils.actor.container.user.batch_delete',
729                         egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.args.name, { deleted : 't' }
730                     ).then(
731                         function () {
732                             $scope.complete = true;
733                             $scope.modal.close();
734                             drawBucket();
735                         },
736                         function (err) { console.log('User deletion error: ' + err); },
737                         function (p) {
738                             last_stage = p.stage;
739                             if (p.error) {
740                                 ngToast.warning(p.error);
741                             }
742                             if (p.stage == 'COMPLETE') return;
743
744                             p.label = egCore.strings[p.stage];
745                             if (!p.max) {
746                                 p.max = 1;
747                                 p.count = 1;
748                             }
749                             $scope.states[p.ord] = p;
750                         }
751                     ).then(function() {
752                         if (last_stage != 'COMPLETE')
753                             ngToast.warning(egCore.strings.BATCH_FAILED);
754                     });
755
756                     return event.preventDefault();
757                 });
758             }]
759         });
760
761     }
762
763     $scope.detachUsers = function(users) {
764         var promises = [];
765         angular.forEach(users, function(rec) {
766             var item = bucketSvc.currentBucket.items().filter(
767                 function(i) {
768                     return (i.target_user() == rec.id)
769                 }
770             );
771             if (item.length)
772                 promises.push(bucketSvc.detachUser(item[0].id()));
773         });
774
775         bucketSvc.bucketNeedsRefresh = true;
776         return $q.all(promises).then(drawBucket);
777     }
778
779     $scope.spawnUserEdit = function (users) {
780         angular.forEach($scope.gridControls.selectedItems(), function (i) {
781             var url = egCore.env.basePath + 'circ/patron/' + i.id + '/edit';
782             $timeout(function() { $window.open(url, '_blank') });
783         })
784     }
785
786     // fetch the bucket;  on error show the not-allowed message
787     if ($scope.bucketId) 
788         drawBucket()['catch'](function() { $scope.forbidden = true });
789 }])