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?|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',
114 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
115 $scope.focusMe = true;
116 $scope.ok = function(args) { $uibModalInstance.close(args) }
117 $scope.cancel = function () { $uibModalInstance.dismiss() }
119 }).result.then(function (args) {
120 if (!args || !args.name) return;
121 bucketSvc.createBucket(args.name, args.desc).then(
124 bucketSvc.viewList = [];
125 bucketSvc.allBuckets = []; // reset
126 bucketSvc.currentBucket = null;
128 '/circ/patron/bucket/' + $scope.tab + '/' + id);
134 $scope.openEditBucketDialog = function() {
136 templateUrl: './circ/patron/bucket/t_bucket_edit',
138 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
139 $scope.focusMe = true;
141 name : bucketSvc.currentBucket.name(),
142 desc : bucketSvc.currentBucket.description(),
143 pub : bucketSvc.currentBucket.pub() == 't'
145 $scope.ok = function(args) {
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() });
153 $scope.cancel = function () { $uibModalInstance.dismiss() }
158 // opens the delete confirmation and deletes the current
159 // bucket if the user confirms.
160 $scope.openDeleteBucketDialog = function() {
162 templateUrl: './circ/patron/bucket/t_bucket_delete',
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() }
169 }).result.then(function () {
170 bucketSvc.deleteBucket(bucketSvc.currentBucket.id())
172 bucketSvc.allBuckets = [];
173 $location.path('/circ/patron/bucket/view');
178 // retrieves the requested bucket by ID
179 $scope.openSharedBucketDialog = function() {
181 templateUrl: './circ/patron/bucket/t_load_shared',
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)
190 $scope.cancel = function() { $uibModalInstance.dismiss() }
192 }).result.then(function(id) {
193 // RecordBucketCtrl $scope is not inherited by the
194 // modal, so we need to call loadBucket from the
196 $scope.loadBucket(id);
202 .controller('PendingCtrl',
203 ['$scope','$routeParams','bucketSvc','egGridDataProvider', 'egCore','ngToast','$q',
204 function($scope, $routeParams, bucketSvc , egGridDataProvider, egCore , ngToast , $q) {
205 $scope.setTab('add');
208 $scope.gridControls = {
209 setQuery : function(q) {
210 if (bucketSvc.pendingList.length)
211 return {id : bucketSvc.pendingList};
217 $scope.$watch('barcodesFromFile', function(newVal, oldVal) {
218 if (newVal && newVal != oldVal) {
220 // $scope.resetPendingList(); // ??? Add instead of replace
221 angular.forEach(newVal.split(/\n/), function(line) {
223 // scrub any trailing spaces or commas from the barcode
224 line = line.replace(/(.*?)($|\s.*|,.*)/,'$1');
225 promises.push(egCore.pcrud.search(
229 ).then(null, null, function(card) {
230 bucketSvc.pendingList.push(card.usr());
234 $q.all(promises).then(function () {
235 $scope.gridControls.setQuery({id : bucketSvc.pendingList});
240 $scope.search = function() {
241 bucketSvc.barcodeRecords = [];
245 {barcode : bucketSvc.barcodeString},
247 ).then(null, null, function(card) {
248 bucketSvc.pendingList.push(card.usr());
249 $scope.gridControls.setQuery({id : bucketSvc.pendingList});
251 bucketSvc.barcodeString = '';
254 $scope.resetPendingList = function() {
255 bucketSvc.pendingList = [];
256 $scope.gridControls.setQuery({});
259 $scope.$parent.resetPendingList = $scope.resetPendingList;
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);
270 $scope.gridControls.setQuery();
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) {
279 $scope.setTab('view');
280 $scope.bucketId = $routeParams.id;
283 $scope.gridControls = {
284 setQuery : function(q) {
290 $scope.modifyStatcats = function() {
291 bucketSvc.bucketNeedsRefresh = true;
294 templateUrl: './circ/patron/bucket/t_update_statcats',
296 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
297 $scope.running = false;
298 $scope.complete = false;
301 $scope.modal = $uibModalInstance;
302 $scope.ok = function(args) { $uibModalInstance.close() }
303 $scope.cancel = function () { $uibModalInstance.dismiss() }
305 $scope.current_bucket = bucketSvc.currentBucket;
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) {
316 cat.allow_freetext(parseInt(cat.allow_freetext())); // just to be sure
318 cat.entries().sort(function(a,b) {
319 return a.value() < b.value() ? -1 : 1
323 $scope.stat_cats = cats;
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
331 $scope.running = true;
333 var changes = {remove:[], apply:{}};
334 angular.forEach($scope.stat_cats, function (sc) {
336 changes.remove.push(sc.id());
337 } else if (sc.new_value) {
338 changes.apply[sc.id()] = sc.new_value;
344 'open-ils.actor.container.user.batch_statcat_apply',
345 egCore.auth.token(), bucketSvc.currentBucket.id(), changes
348 $scope.complete = true;
349 $scope.modal.close();
352 function (err) { console.log('User edit error: ' + err); },
355 ngToast.warning(p.error);
357 if (p.stage == 'COMPLETE') return;
359 p.label = egCore.strings[p.stage];
364 $scope.states[p.ord] = p;
368 return event.preventDefault();
375 function drawBucket() {
376 return bucketSvc.fetchBucket($scope.bucketId).then(
378 var ids = bucket.items().map(
379 function(i){return i.target_user()}
382 $scope.gridControls.setQuery({id : ids});
384 $scope.gridControls.setQuery({});
390 $scope.no_update_perms = true;
391 $scope.noUpdatePerms = function () { return $scope.no_update_perms; }
393 egPerm.hasPermHere(['UPDATE_USER']).then(
395 if (Object.keys(hash).length == 0) return;
397 var one_false = false;
398 angular.forEach(hash, function(has) {
399 if (!has) one_false = true;
402 if (!one_false) $scope.no_update_perms = false;
406 function annotate_groups(grps) {
407 angular.forEach(grps, function (g) {
408 if (!g.hasOwnProperty('cannot_use')) {
409 if (g.usergroup() == 'f') {
411 } else if (g.application_perm) {
412 egPerm.hasPermHere(['EVERYTHING',g.application_perm]).then(
414 if (Object.keys(hash).length == 0) {
419 var one_false = false;
420 angular.forEach(hash, function(has) {
421 if (has) g.cannot_use = false;
426 g.cannot_use = false;
432 $scope.viewChangesets = function() {
433 bucketSvc.bucketNeedsRefresh = true;
436 templateUrl: './circ/patron/bucket/t_changesets',
438 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
439 $scope.running = false;
440 $scope.complete = false;
443 $scope.focusMe = true;
444 $scope.modal = $uibModalInstance;
445 $scope.ok = function() { $uibModalInstance.close() }
446 $scope.cancel = function () { $uibModalInstance.dismiss() }
448 $scope.current_bucket = bucketSvc.currentBucket;
449 $scope.fieldset_groups = [];
451 $scope.deleteChangeset = function (grp) {
452 egCore.pcrud.remove(grp).then(
454 if (grp.rollback_group()) {
456 .retrieve('afsg',grp.rollback_group())
458 egCore.pcrud.remove(g)
459 .then( function () { refresh_groups() } );
464 return event.preventDefault();
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);
483 $scope.applyRollback = function() {
484 bucketSvc.bucketNeedsRefresh = true;
487 templateUrl: './circ/patron/bucket/t_rollback',
489 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
490 $scope.running = false;
491 $scope.complete = false;
493 $scope.revert_me = null;
495 $scope.focusMe = true;
496 $scope.modal = $uibModalInstance;
497 $scope.ok = function(args) { $uibModalInstance.close() }
498 $scope.cancel = function () { $uibModalInstance.dismiss() }
500 $scope.current_bucket = bucketSvc.currentBucket;
501 $scope.revertable_fieldset_groups = [];
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);
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
518 $scope.running = true;
523 'open-ils.actor.container.user.apply_rollback',
524 egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.revert_me.id()
527 $scope.complete = true;
528 $scope.modal.close();
531 function (err) { console.log('User edit error: ' + err); },
533 last_stage = p.stage;
535 ngToast.warning(p.error);
537 if (p.stage == 'COMPLETE') return;
539 p.label = egCore.strings[p.stage];
544 $scope.states[p.ord] = p;
547 if (last_stage != 'COMPLETE')
548 ngToast.warning(egCore.strings.BATCH_FAILED);
551 return event.preventDefault();
557 $scope.updateAllUsers = function() {
558 bucketSvc.bucketNeedsRefresh = true;
561 templateUrl: './circ/patron/bucket/t_update_all',
563 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
564 $scope.running = false;
565 $scope.complete = false;
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() }
574 $scope.disable_home_org = function(org_id) {
576 var org = egCore.org.get(org_id);
580 org.ou_type().can_have_users() == 'f'
584 $scope.pgt_depth = function(grp) {
586 while (grp = egCore.env.pgt.map[grp.parent()]) d++;
590 if (egCore.env.cnal) {
591 $scope.net_access_levels = egCore.env.cnal.list;
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;
600 if (egCore.env.pgt) {
601 $scope.profiles = egCore.env.pgt.list;
602 annotate_groups($scope.profiles);
604 egCore.pcrud.search('pgt', {parent : null},
605 {flesh : -1, flesh_fields : {pgt : ['children']}}
608 egCore.env.absorbTree(tree, 'pgt')
609 $scope.profiles = egCore.env.pgt.list;
610 annotate_groups($scope.profiles);
615 $scope.unset_field = function (event,field) {
616 $scope.args[field] = null;
617 return event.preventDefault();
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
626 $scope.running = true;
628 // XXX fix up $scope.args values here
629 if ($scope.args.home_ou) {
630 $scope.args.home_ou = $scope.args.home_ou.id();
632 if ($scope.args.net_access_level) {
633 $scope.args.net_access_level = $scope.args.net_access_level.id();
635 if ($scope.args.profile) {
636 $scope.args.profile = $scope.args.profile.id();
638 if ($scope.args.expire_date) {
639 $scope.args.expire_date = $scope.args.expire_date.toJSON().substr(0,10);
642 for (var key in $scope.args) {
643 if (!$scope.args[key] && $scope.args[key] !== 0) {
644 delete $scope.args[key];
651 'open-ils.actor.container.user.batch_edit',
652 egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.args.name, $scope.args
655 $scope.complete = true;
656 $scope.modal.close();
659 function (err) { console.log('User edit error: ' + err); },
661 last_stage = p.stage;
663 ngToast.warning(p.error);
665 if (p.stage == 'COMPLETE') return;
667 p.label = egCore.strings[p.stage];
672 $scope.states[p.ord] = p;
675 if (last_stage != 'COMPLETE')
676 ngToast.warning(egCore.strings.BATCH_FAILED);
679 return event.preventDefault();
685 $scope.no_delete_perms = true;
686 $scope.noDeletePerms = function () { return $scope.no_delete_perms; }
688 egPerm.hasPermHere(['UPDATE_USER','DELETE_USER']).then(
690 if (Object.keys(hash).length == 0) return;
692 var one_false = false;
693 angular.forEach(hash, function(has) {
694 if (!has) one_false = true;
697 if (!one_false) $scope.no_delete_perms = false;
701 $scope.deleteAllUsers = function() {
702 bucketSvc.bucketNeedsRefresh = true;
705 templateUrl: './circ/patron/bucket/t_delete_all',
707 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
708 $scope.running = false;
709 $scope.complete = false;
712 $scope.focusMe = true;
713 $scope.modal = $uibModalInstance;
714 $scope.ok = function(args) { $uibModalInstance.close() }
715 $scope.cancel = function () { $uibModalInstance.dismiss() }
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
723 $scope.running = true;
728 'open-ils.actor.container.user.batch_delete',
729 egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.args.name, { deleted : 't' }
732 $scope.complete = true;
733 $scope.modal.close();
736 function (err) { console.log('User deletion error: ' + err); },
738 last_stage = p.stage;
740 ngToast.warning(p.error);
742 if (p.stage == 'COMPLETE') return;
744 p.label = egCore.strings[p.stage];
749 $scope.states[p.ord] = p;
752 if (last_stage != 'COMPLETE')
753 ngToast.warning(egCore.strings.BATCH_FAILED);
756 return event.preventDefault();
763 $scope.detachUsers = function(users) {
765 angular.forEach(users, function(rec) {
766 var item = bucketSvc.currentBucket.items().filter(
768 return (i.target_user() == rec.id)
772 promises.push(bucketSvc.detachUser(item[0].id()));
775 bucketSvc.bucketNeedsRefresh = true;
776 return $q.all(promises).then(drawBucket);
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') });
786 // fetch the bucket; on error show the not-allowed message
788 drawBucket()['catch'](function() { $scope.forbidden = true });