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