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.
15 angular.module('egCatUserBuckets',
16 ['ngRoute', 'ui.bootstrap', 'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod', 'egUserBucketMod', 'ngToast'])
18 .config(function($routeProvider, $locationProvider, $compileProvider) {
19 $locationProvider.html5Mode(true);
20 $compileProvider.aHrefSanitizationWhitelist(/^\s*(https?|mailto|blob):/); // grid export
22 var resolver = {delay : function(egStartup) {return egStartup.go()}};
24 $routeProvider.when('/circ/patron/bucket/add/:id', {
25 templateUrl: './circ/patron/bucket/t_pending',
26 controller: 'PendingCtrl',
30 $routeProvider.when('/circ/patron/bucket/add', {
31 templateUrl: './circ/patron/bucket/t_pending',
32 controller: 'PendingCtrl',
36 $routeProvider.when('/circ/patron/bucket/view/:id', {
37 templateUrl: './circ/patron/bucket/t_view',
38 controller: 'ViewCtrl',
42 $routeProvider.when('/circ/patron/bucket/view', {
43 templateUrl: './circ/patron/bucket/t_view',
44 controller: 'ViewCtrl',
48 // default page / bucket view
49 $routeProvider.otherwise({redirectTo : '/circ/patron/bucket/view'});
53 * Top-level controller.
54 * Hosts functions needed by all controllers.
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) {
62 $scope.bucketSvc = bucketSvc;
63 $scope.bucket = function() { return bucketSvc.currentBucket }
66 $scope.setTab = function(tab) {
69 // for bucket selector; must be called after route resolve
70 bucketSvc.fetchUserBuckets();
73 $scope.loadBucketFromMenu = function(item, bucket) {
74 if (bucket) return $scope.loadBucket(bucket.id());
77 $scope.loadBucket = function(id) {
79 '/circ/patron/bucket/' +
80 $scope.tab + '/' + encodeURIComponent(id));
83 $scope.addToBucket = function(recs) {
84 if (recs.length == 0) return;
85 bucketSvc.bucketNeedsRefresh = true;
89 var item = new egCore.idl.cubi();
90 item.bucket(bucketSvc.currentBucket.id());
91 item.target_user(rec.id);
94 'open-ils.actor.container.item.create',
95 egCore.auth.token(), 'user', item
96 ).then(function(resp) {
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);
107 $scope.resetPendingList();
110 $scope.openCreateBucketDialog = function() {
112 templateUrl: './circ/patron/bucket/t_bucket_create',
115 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
116 $scope.focusMe = true;
117 $scope.ok = function(args) { $uibModalInstance.close(args) }
118 $scope.cancel = function () { $uibModalInstance.dismiss() }
120 }).result.then(function (args) {
121 if (!args || !args.name) return;
122 bucketSvc.createBucket(args.name, args.desc).then(
125 bucketSvc.viewList = [];
126 bucketSvc.allBuckets = []; // reset
127 bucketSvc.currentBucket = null;
129 '/circ/patron/bucket/' + $scope.tab + '/' + id);
135 $scope.openEditBucketDialog = function() {
137 templateUrl: './circ/patron/bucket/t_bucket_edit',
140 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
141 $scope.focusMe = true;
143 name : bucketSvc.currentBucket.name(),
144 desc : bucketSvc.currentBucket.description(),
145 pub : bucketSvc.currentBucket.pub() == 't'
147 $scope.ok = function(args) {
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() });
155 $scope.cancel = function () { $uibModalInstance.dismiss() }
160 // opens the delete confirmation and deletes the current
161 // bucket if the user confirms.
162 $scope.openDeleteBucketDialog = function() {
164 templateUrl: './circ/patron/bucket/t_bucket_delete',
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() }
172 }).result.then(function () {
173 bucketSvc.deleteBucket(bucketSvc.currentBucket.id())
175 bucketSvc.allBuckets = [];
176 $location.path('/circ/patron/bucket/view');
181 // retrieves the requested bucket by ID
182 $scope.openSharedBucketDialog = function() {
184 templateUrl: './circ/patron/bucket/t_load_shared',
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)
194 $scope.cancel = function() { $uibModalInstance.dismiss() }
196 }).result.then(function(id) {
197 // RecordBucketCtrl $scope is not inherited by the
198 // modal, so we need to call loadBucket from the
200 $scope.loadBucket(id);
206 .controller('PendingCtrl',
207 ['$scope','$routeParams','bucketSvc','egGridDataProvider', 'egCore','ngToast','$q',
208 function($scope, $routeParams, bucketSvc , egGridDataProvider, egCore , ngToast , $q) {
209 $scope.setTab('add');
212 $scope.gridControls = {
213 setQuery : function(q) {
214 if (bucketSvc.pendingList.length)
215 return {id : bucketSvc.pendingList};
221 $scope.$watch('barcodesFromFile', function(newVal, oldVal) {
222 if (newVal && newVal != oldVal) {
224 // $scope.resetPendingList(); // ??? Add instead of replace
225 angular.forEach(newVal.split(/\n/), function(line) {
227 // scrub any trailing spaces or commas from the barcode
228 line = line.replace(/(.*?)($|\s.*|,.*)/,'$1');
234 {barcode : barcodes},
238 $scope.gridControls.setQuery({id : bucketSvc.pendingList});
242 bucketSvc.pendingList.push(card.usr());
248 $scope.search = function() {
249 bucketSvc.barcodeRecords = [];
253 {barcode : bucketSvc.barcodeString},
255 ).then(null, null, function(card) {
256 bucketSvc.pendingList.push(card.usr());
257 $scope.gridControls.setQuery({id : bucketSvc.pendingList});
259 bucketSvc.barcodeString = '';
262 $scope.resetPendingList = function() {
263 bucketSvc.pendingList = [];
264 $scope.gridControls.setQuery({});
267 $scope.$parent.resetPendingList = $scope.resetPendingList;
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);
278 $scope.gridControls.setQuery();
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) {
287 $scope.setTab('view');
288 $scope.bucketId = $routeParams.id;
291 $scope.gridControls = {
292 setQuery : function(q) {
298 $scope.modifyStatcats = function() {
299 bucketSvc.bucketNeedsRefresh = true;
302 templateUrl: './circ/patron/bucket/t_update_statcats',
305 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
306 $scope.running = false;
307 $scope.complete = false;
310 $scope.modal = $uibModalInstance;
311 $scope.ok = function(args) { $uibModalInstance.close() }
312 $scope.cancel = function () { $uibModalInstance.dismiss() }
314 $scope.current_bucket = bucketSvc.currentBucket;
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) {
325 cat.allow_freetext(parseInt(cat.allow_freetext())); // just to be sure
327 cat.entries().sort(function(a,b) {
328 return a.value() < b.value() ? -1 : 1
332 $scope.stat_cats = cats;
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
340 $scope.running = true;
342 var changes = {remove:[], apply:{}};
343 angular.forEach($scope.stat_cats, function (sc) {
345 changes.remove.push(sc.id());
346 } else if (sc.new_value) {
347 changes.apply[sc.id()] = sc.new_value;
353 'open-ils.actor.container.user.batch_statcat_apply',
354 egCore.auth.token(), bucketSvc.currentBucket.id(), changes
357 $scope.complete = true;
358 $scope.modal.close();
361 function (err) { console.log('User edit error: ' + err); },
364 ngToast.warning(p.error);
366 if (p.stage == 'COMPLETE') return;
368 p.label = egCore.strings[p.stage];
373 $scope.states[p.ord] = p;
377 return event.preventDefault();
384 function drawBucket() {
385 return bucketSvc.fetchBucket($scope.bucketId).then(
387 var ids = bucket.items().map(
388 function(i){return i.target_user()}
391 $scope.gridControls.setQuery({id : ids});
393 $scope.gridControls.setQuery({});
399 $scope.no_update_perms = true;
400 $scope.noUpdatePerms = function () { return $scope.no_update_perms; }
402 egPerm.hasPermHere(['UPDATE_USER']).then(
404 if (Object.keys(hash).length == 0) return;
406 var one_false = false;
407 angular.forEach(hash, function(has) {
408 if (!has) one_false = true;
411 if (!one_false) $scope.no_update_perms = false;
415 function annotate_groups(grps) {
416 angular.forEach(grps, function (g) {
417 if (!g.hasOwnProperty('cannot_use')) {
418 if (g.usergroup() == 'f') {
420 } else if (g.application_perm) {
421 egPerm.hasPermHere(['EVERYTHING',g.application_perm]).then(
423 if (Object.keys(hash).length == 0) {
428 var one_false = false;
429 angular.forEach(hash, function(has) {
430 if (has) g.cannot_use = false;
435 g.cannot_use = false;
441 $scope.viewChangesets = function() {
442 bucketSvc.bucketNeedsRefresh = true;
445 templateUrl: './circ/patron/bucket/t_changesets',
448 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
449 $scope.running = false;
450 $scope.complete = false;
453 $scope.focusMe = true;
454 $scope.modal = $uibModalInstance;
455 $scope.ok = function() { $uibModalInstance.close() }
456 $scope.cancel = function () { $uibModalInstance.dismiss() }
458 $scope.current_bucket = bucketSvc.currentBucket;
459 $scope.fieldset_groups = [];
461 $scope.deleteChangeset = function (grp) {
462 egCore.pcrud.remove(grp).then(
464 if (grp.rollback_group()) {
466 .retrieve('afsg',grp.rollback_group())
468 egCore.pcrud.remove(g)
469 .then( function () { refresh_groups() } );
474 return event.preventDefault();
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);
493 $scope.applyRollback = function() {
494 bucketSvc.bucketNeedsRefresh = true;
497 templateUrl: './circ/patron/bucket/t_rollback',
499 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
500 $scope.running = false;
501 $scope.complete = false;
503 $scope.revert_me = null;
505 $scope.focusMe = true;
506 $scope.modal = $uibModalInstance;
507 $scope.ok = function(args) { $uibModalInstance.close() }
508 $scope.cancel = function () { $uibModalInstance.dismiss() }
510 $scope.current_bucket = bucketSvc.currentBucket;
511 $scope.revertable_fieldset_groups = [];
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);
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
528 $scope.running = true;
533 'open-ils.actor.container.user.apply_rollback',
534 egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.revert_me.id()
537 $scope.complete = true;
538 $scope.modal.close();
541 function (err) { console.log('User edit error: ' + err); },
543 last_stage = p.stage;
545 ngToast.warning(p.error);
547 if (p.stage == 'COMPLETE') return;
549 p.label = egCore.strings[p.stage];
554 $scope.states[p.ord] = p;
557 if (last_stage != 'COMPLETE')
558 ngToast.warning(egCore.strings.BATCH_FAILED);
561 return event.preventDefault();
567 $scope.updateAllUsers = function() {
568 bucketSvc.bucketNeedsRefresh = true;
571 templateUrl: './circ/patron/bucket/t_update_all',
574 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
575 $scope.running = false;
576 $scope.complete = false;
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() }
585 $scope.disable_home_org = function(org_id) {
587 var org = egCore.org.get(org_id);
591 org.ou_type().can_have_users() == 'f'
595 $scope.pgt_depth = function(grp) {
597 while (grp = egCore.env.pgt.map[grp.parent()]) d++;
601 if (egCore.env.cnal) {
602 $scope.net_access_levels = egCore.env.cnal.list;
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;
611 if (egCore.env.pgt) {
612 $scope.profiles = egCore.env.pgt.list;
613 annotate_groups($scope.profiles);
615 egCore.pcrud.search('pgt', {parent : null},
616 {flesh : -1, flesh_fields : {pgt : ['children']}}
619 egCore.env.absorbTree(tree, 'pgt')
620 $scope.profiles = egCore.env.pgt.list;
621 annotate_groups($scope.profiles);
626 $scope.unset_field = function (event,field) {
627 $scope.args[field] = null;
628 return event.preventDefault();
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
637 $scope.running = true;
639 // XXX fix up $scope.args values here
640 if ($scope.args.home_ou) {
641 $scope.args.home_ou = $scope.args.home_ou.id();
643 if ($scope.args.net_access_level) {
644 $scope.args.net_access_level = $scope.args.net_access_level.id();
646 if ($scope.args.profile) {
647 $scope.args.profile = $scope.args.profile.id();
649 if ($scope.args.expire_date) {
650 $scope.args.expire_date = $scope.args.expire_date.toJSON().substr(0,10);
653 for (var key in $scope.args) {
654 if (!$scope.args[key] && $scope.args[key] !== 0) {
655 delete $scope.args[key];
662 'open-ils.actor.container.user.batch_edit',
663 egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.args.name, $scope.args
666 $scope.complete = true;
667 $scope.modal.close();
670 function (err) { console.log('User edit error: ' + err); },
672 last_stage = p.stage;
674 ngToast.warning(p.error);
676 if (p.stage == 'COMPLETE') return;
678 p.label = egCore.strings[p.stage];
683 $scope.states[p.ord] = p;
686 if (last_stage != 'COMPLETE')
687 ngToast.warning(egCore.strings.BATCH_FAILED);
690 return event.preventDefault();
696 $scope.no_delete_perms = true;
697 $scope.noDeletePerms = function () { return $scope.no_delete_perms; }
699 egPerm.hasPermHere(['UPDATE_USER','DELETE_USER']).then(
701 if (Object.keys(hash).length == 0) return;
703 var one_false = false;
704 angular.forEach(hash, function(has) {
705 if (!has) one_false = true;
708 if (!one_false) $scope.no_delete_perms = false;
712 $scope.deleteAllUsers = function() {
713 bucketSvc.bucketNeedsRefresh = true;
716 templateUrl: './circ/patron/bucket/t_delete_all',
719 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
720 $scope.running = false;
721 $scope.complete = false;
724 $scope.focusMe = true;
725 $scope.modal = $uibModalInstance;
726 $scope.ok = function(args) { $uibModalInstance.close() }
727 $scope.cancel = function () { $uibModalInstance.dismiss() }
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
735 $scope.running = true;
740 'open-ils.actor.container.user.batch_delete',
741 egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.args.name, { deleted : 't' }
744 $scope.complete = true;
745 $scope.modal.close();
748 function (err) { console.log('User deletion error: ' + err); },
750 last_stage = p.stage;
752 ngToast.warning(p.error);
754 if (p.stage == 'COMPLETE') return;
756 p.label = egCore.strings[p.stage];
761 $scope.states[p.ord] = p;
764 if (last_stage != 'COMPLETE')
765 ngToast.warning(egCore.strings.BATCH_FAILED);
768 return event.preventDefault();
775 $scope.detachUsers = function(users) {
777 angular.forEach(users, function(rec) {
778 var item = bucketSvc.currentBucket.items().filter(
780 return (i.target_user() == rec.id)
784 promises.push(bucketSvc.detachUser(item[0].id()));
787 bucketSvc.bucketNeedsRefresh = true;
788 return $q.all(promises).then(drawBucket);
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') });
798 // fetch the bucket; on error show the not-allowed message
800 drawBucket()['catch'](function() { $scope.forbidden = true });