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