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','egProgressDialog',
59 function($scope, $location, $q, $timeout, $uibModal,
60 $window, egCore, bucketSvc , ngToast , egProgressDialog) {
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;
87 egProgressDialog.open();
88 var ids = recs.map(function(rec) {
89 $scope.removeFromPendingList(rec.id);
95 'open-ils.actor.container.item.create.batch',
96 egCore.auth.token(), 'user',
97 bucketSvc.currentBucket.id(), ids
100 egProgressDialog.close();
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);
112 //re-draw the pending list
113 $scope.resetPendingListQuery();
116 $scope.openCreateBucketDialog = function() {
118 templateUrl: './circ/patron/bucket/t_bucket_create',
121 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
122 $scope.focusMe = true;
123 $scope.ok = function(args) { $uibModalInstance.close(args) }
124 $scope.cancel = function () { $uibModalInstance.dismiss() }
126 }).result.then(function (args) {
127 if (!args || !args.name) return;
128 bucketSvc.createBucket(args.name, args.desc).then(
131 bucketSvc.viewList = [];
132 bucketSvc.allBuckets = []; // reset
133 bucketSvc.currentBucket = null;
135 '/circ/patron/bucket/' + $scope.tab + '/' + id);
141 $scope.openEditBucketDialog = function() {
143 templateUrl: './circ/patron/bucket/t_bucket_edit',
146 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
147 $scope.focusMe = true;
149 name : bucketSvc.currentBucket.name(),
150 desc : bucketSvc.currentBucket.description(),
151 pub : bucketSvc.currentBucket.pub() == 't'
153 $scope.ok = function(args) {
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() });
161 $scope.cancel = function () { $uibModalInstance.dismiss() }
166 // opens the delete confirmation and deletes the current
167 // bucket if the user confirms.
168 $scope.openDeleteBucketDialog = function() {
170 templateUrl: './circ/patron/bucket/t_bucket_delete',
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() }
178 }).result.then(function () {
179 bucketSvc.deleteBucket(bucketSvc.currentBucket.id())
181 bucketSvc.allBuckets = [];
182 $location.path('/circ/patron/bucket/view');
187 // retrieves the requested bucket by ID
188 $scope.openSharedBucketDialog = function() {
190 templateUrl: './circ/patron/bucket/t_load_shared',
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)
200 $scope.cancel = function() { $uibModalInstance.dismiss() }
202 }).result.then(function(id) {
203 // RecordBucketCtrl $scope is not inherited by the
204 // modal, so we need to call loadBucket from the
206 $scope.loadBucket(id);
212 .controller('PendingCtrl',
213 ['$scope','$routeParams','bucketSvc','egGridDataProvider', 'egCore','ngToast','$q',
214 function($scope, $routeParams, bucketSvc , egGridDataProvider, egCore , ngToast , $q) {
215 $scope.setTab('add');
218 $scope.gridControls = {
219 setQuery : function(q) {
220 if (bucketSvc.pendingList.length)
221 return {id : bucketSvc.pendingList};
227 $scope.$watch('barcodesFromFile', function(newVal, oldVal) {
228 if (newVal && newVal != oldVal) {
230 // $scope.resetPendingList(); // ??? Add instead of replace
231 angular.forEach(newVal.split(/\n/), function(line) {
233 // scrub any trailing spaces or commas from the barcode
234 line = line.replace(/(.*?)($|\s.*|,.*)/,'$1');
240 {barcode : barcodes},
244 $scope.gridControls.setQuery({id : bucketSvc.pendingList});
248 bucketSvc.pendingList.push(card.usr());
254 $scope.search = function() {
255 bucketSvc.barcodeRecords = [];
259 {barcode : bucketSvc.barcodeString},
261 ).then(null, null, function(card) {
262 bucketSvc.pendingList.push(card.usr());
263 $scope.gridControls.setQuery({id : bucketSvc.pendingList});
265 bucketSvc.barcodeString = '';
268 $scope.resetPendingList = function() {
269 bucketSvc.pendingList = [];
270 $scope.gridControls.setQuery({});
273 $scope.$parent.resetPendingList = $scope.resetPendingList;
275 //remove entry from PendingList
276 $scope.removeFromPendingList = function(usr) {
277 const index = bucketSvc.pendingList.indexOf(usr);
279 bucketSvc.pendingList.splice(index,1);
282 $scope.$parent.removeFromPendingList = $scope.removeFromPendingList;
284 $scope.resetPendingListQuery = function() {
285 $scope.gridControls.setQuery({id : bucketSvc.pendingList});
287 $scope.$parent.resetPendingListQuery = $scope.resetPendingListQuery;
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);
298 $scope.gridControls.setQuery();
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) {
307 $scope.setTab('view');
308 $scope.bucketId = $routeParams.id;
311 $scope.gridControls = {
312 setQuery : function(q) {
318 $scope.modifyStatcats = function() {
319 bucketSvc.bucketNeedsRefresh = true;
322 templateUrl: './circ/patron/bucket/t_update_statcats',
325 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
326 $scope.running = false;
327 $scope.complete = false;
330 $scope.modal = $uibModalInstance;
331 $scope.ok = function(args) { $uibModalInstance.close() }
332 $scope.cancel = function () { $uibModalInstance.dismiss() }
334 $scope.current_bucket = bucketSvc.currentBucket;
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) {
345 cat.allow_freetext(parseInt(cat.allow_freetext())); // just to be sure
347 cat.entries().sort(function(a,b) {
348 return a.value() < b.value() ? -1 : 1
352 $scope.stat_cats = cats;
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
360 $scope.running = true;
362 var changes = {remove:[], apply:{}};
363 angular.forEach($scope.stat_cats, function (sc) {
365 changes.remove.push(sc.id());
366 } else if (sc.new_value) {
367 changes.apply[sc.id()] = sc.new_value;
373 'open-ils.actor.container.user.batch_statcat_apply',
374 egCore.auth.token(), bucketSvc.currentBucket.id(), changes
377 $scope.complete = true;
378 $scope.modal.close();
381 function (err) { console.log('User edit error: ' + err); },
384 ngToast.warning(p.error);
386 if (p.stage == 'COMPLETE') return;
388 p.label = egCore.strings[p.stage];
393 $scope.states[p.ord] = p;
397 return event.preventDefault();
404 function drawBucket() {
405 return bucketSvc.fetchBucket($scope.bucketId).then(
407 var ids = bucket.items().map(
408 function(i){return i.target_user()}
411 $scope.gridControls.setQuery({id : ids});
413 $scope.gridControls.setQuery({});
419 $scope.no_update_perms = true;
420 $scope.noUpdatePerms = function () { return $scope.no_update_perms; }
422 egPerm.hasPermHere(['UPDATE_USER']).then(
424 if (Object.keys(hash).length == 0) return;
426 var one_false = false;
427 angular.forEach(hash, function(has) {
428 if (!has) one_false = true;
431 if (!one_false) $scope.no_update_perms = false;
435 function annotate_groups(grps) {
436 angular.forEach(grps, function (g) {
437 if (!g.hasOwnProperty('cannot_use')) {
438 if (g.usergroup() == 'f') {
440 } else if (g.application_perm) {
441 egPerm.hasPermHere(['EVERYTHING',g.application_perm]).then(
443 if (Object.keys(hash).length == 0) {
448 var one_false = false;
449 angular.forEach(hash, function(has) {
450 if (has) g.cannot_use = false;
455 g.cannot_use = false;
461 $scope.viewChangesets = function() {
462 bucketSvc.bucketNeedsRefresh = true;
465 templateUrl: './circ/patron/bucket/t_changesets',
468 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
469 $scope.running = false;
470 $scope.complete = false;
473 $scope.focusMe = true;
474 $scope.modal = $uibModalInstance;
475 $scope.ok = function() { $uibModalInstance.close() }
476 $scope.cancel = function () { $uibModalInstance.dismiss() }
478 $scope.current_bucket = bucketSvc.currentBucket;
479 $scope.fieldset_groups = [];
481 $scope.deleteChangeset = function (grp) {
482 egCore.pcrud.remove(grp).then(
484 if (grp.rollback_group()) {
486 .retrieve('afsg',grp.rollback_group())
488 egCore.pcrud.remove(g)
489 .then( function () { refresh_groups() } );
494 return event.preventDefault();
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);
513 $scope.applyRollback = function() {
514 bucketSvc.bucketNeedsRefresh = true;
517 templateUrl: './circ/patron/bucket/t_rollback',
519 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
520 $scope.running = false;
521 $scope.complete = false;
523 $scope.revert_me = null;
525 $scope.focusMe = true;
526 $scope.modal = $uibModalInstance;
527 $scope.ok = function(args) { $uibModalInstance.close() }
528 $scope.cancel = function () { $uibModalInstance.dismiss() }
530 $scope.current_bucket = bucketSvc.currentBucket;
531 $scope.revertable_fieldset_groups = [];
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);
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
548 $scope.running = true;
553 'open-ils.actor.container.user.apply_rollback',
554 egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.revert_me.id()
557 $scope.complete = true;
558 $scope.modal.close();
561 function (err) { console.log('User edit error: ' + err); },
563 last_stage = p.stage;
565 ngToast.warning(p.error);
567 if (p.stage == 'COMPLETE') return;
569 p.label = egCore.strings[p.stage];
574 $scope.states[p.ord] = p;
577 if (last_stage != 'COMPLETE')
578 ngToast.warning(egCore.strings.BATCH_FAILED);
581 return event.preventDefault();
587 $scope.updateAllUsers = function() {
588 bucketSvc.bucketNeedsRefresh = true;
591 templateUrl: './circ/patron/bucket/t_update_all',
594 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
595 $scope.running = false;
596 $scope.complete = false;
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() }
605 $scope.disable_home_org = function(org_id) {
607 var org = egCore.org.get(org_id);
611 org.ou_type().can_have_users() == 'f'
615 $scope.pgt_depth = function(grp) {
617 while (grp = egCore.env.pgt.map[grp.parent()]) d++;
621 if (egCore.env.cnal) {
622 $scope.net_access_levels = egCore.env.cnal.list;
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;
631 if (egCore.env.pgt) {
632 $scope.profiles = egCore.env.pgt.list;
633 annotate_groups($scope.profiles);
635 egCore.pcrud.search('pgt', {parent : null},
636 {flesh : -1, flesh_fields : {pgt : ['children']}}
639 egCore.env.absorbTree(tree, 'pgt')
640 $scope.profiles = egCore.env.pgt.list;
641 annotate_groups($scope.profiles);
646 $scope.unset_field = function (event,field) {
647 $scope.args[field] = null;
648 return event.preventDefault();
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
657 $scope.running = true;
659 // XXX fix up $scope.args values here
660 if ($scope.args.home_ou) {
661 $scope.args.home_ou = $scope.args.home_ou.id();
663 if ($scope.args.net_access_level) {
664 $scope.args.net_access_level = $scope.args.net_access_level.id();
666 if ($scope.args.profile) {
667 $scope.args.profile = $scope.args.profile.id();
669 if ($scope.args.expire_date) {
670 $scope.args.expire_date = $scope.args.expire_date.toJSON().substr(0,10);
673 for (var key in $scope.args) {
674 if (!$scope.args[key] && $scope.args[key] !== 0) {
675 delete $scope.args[key];
682 'open-ils.actor.container.user.batch_edit',
683 egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.args.name, $scope.args
686 $scope.complete = true;
687 $scope.modal.close();
690 function (err) { console.log('User edit error: ' + err); },
692 last_stage = p.stage;
694 ngToast.warning(p.error);
696 if (p.stage == 'COMPLETE') return;
698 p.label = egCore.strings[p.stage];
703 $scope.states[p.ord] = p;
706 if (last_stage != 'COMPLETE')
707 ngToast.warning(egCore.strings.BATCH_FAILED);
710 return event.preventDefault();
716 $scope.no_delete_perms = true;
717 $scope.noDeletePerms = function () { return $scope.no_delete_perms; }
719 egPerm.hasPermHere(['UPDATE_USER','DELETE_USER']).then(
721 if (Object.keys(hash).length == 0) return;
723 var one_false = false;
724 angular.forEach(hash, function(has) {
725 if (!has) one_false = true;
728 if (!one_false) $scope.no_delete_perms = false;
732 $scope.deleteAllUsers = function() {
733 bucketSvc.bucketNeedsRefresh = true;
736 templateUrl: './circ/patron/bucket/t_delete_all',
739 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
740 $scope.running = false;
741 $scope.complete = false;
744 $scope.focusMe = true;
745 $scope.modal = $uibModalInstance;
746 $scope.ok = function(args) { $uibModalInstance.close() }
747 $scope.cancel = function () { $uibModalInstance.dismiss() }
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
755 $scope.running = true;
760 'open-ils.actor.container.user.batch_delete',
761 egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.args.name, { deleted : 't' }
764 $scope.complete = true;
765 $scope.modal.close();
768 function (err) { console.log('User deletion error: ' + err); },
770 last_stage = p.stage;
772 ngToast.warning(p.error);
774 if (p.stage == 'COMPLETE') return;
776 p.label = egCore.strings[p.stage];
781 $scope.states[p.ord] = p;
784 if (last_stage != 'COMPLETE')
785 ngToast.warning(egCore.strings.BATCH_FAILED);
788 return event.preventDefault();
795 $scope.detachUsers = function(users) {
796 var promise = $q.when();
798 $scope.running = true;
804 angular.forEach(users, function(rec) {
805 var item = bucketSvc.currentBucket.items().filter(
807 return (i.target_user() == rec.id)
811 promise = promise.then(function() {
812 return bucketSvc.detachUser(item[0].id())
813 .then(function() { $scope.progress.count++; });
818 bucketSvc.bucketNeedsRefresh = true;
820 .then(function() { $scope.running = false; })
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') });
831 // fetch the bucket; on error show the not-allowed message
833 drawBucket()['catch'](function() { $scope.forbidden = true });