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);
92 $scope.removeFromPendingList(rec.id);
95 'open-ils.actor.container.item.create',
96 egCore.auth.token(), 'user', item
97 ).then(function(resp) {
99 // HACK: add the IDs of the added items so that the size
100 // of the view list will grow (and update any UI looking at
101 // the list size). The data stored is inconsistent, but since
102 // we are forcing a bucket refresh on the next rendering of
103 // the view pane, the list will be repaired.
104 bucketSvc.currentBucket.items().push(resp);
108 //re-draw the pending list
109 $scope.resetPendingListQuery();
112 $scope.openCreateBucketDialog = function() {
114 templateUrl: './circ/patron/bucket/t_bucket_create',
117 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
118 $scope.focusMe = true;
119 $scope.ok = function(args) { $uibModalInstance.close(args) }
120 $scope.cancel = function () { $uibModalInstance.dismiss() }
122 }).result.then(function (args) {
123 if (!args || !args.name) return;
124 bucketSvc.createBucket(args.name, args.desc).then(
127 bucketSvc.viewList = [];
128 bucketSvc.allBuckets = []; // reset
129 bucketSvc.currentBucket = null;
131 '/circ/patron/bucket/' + $scope.tab + '/' + id);
137 $scope.openEditBucketDialog = function() {
139 templateUrl: './circ/patron/bucket/t_bucket_edit',
142 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
143 $scope.focusMe = true;
145 name : bucketSvc.currentBucket.name(),
146 desc : bucketSvc.currentBucket.description(),
147 pub : bucketSvc.currentBucket.pub() == 't'
149 $scope.ok = function(args) {
151 $scope.actionPending = true;
152 args.pub = args.pub ? 't' : 'f';
153 // close the dialog after edit has completed
154 bucketSvc.editBucket(args).then(
155 function() { $uibModalInstance.close() });
157 $scope.cancel = function () { $uibModalInstance.dismiss() }
162 // opens the delete confirmation and deletes the current
163 // bucket if the user confirms.
164 $scope.openDeleteBucketDialog = function() {
166 templateUrl: './circ/patron/bucket/t_bucket_delete',
169 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
170 $scope.bucket = function() { return bucketSvc.currentBucket }
171 $scope.ok = function() { $uibModalInstance.close() }
172 $scope.cancel = function() { $uibModalInstance.dismiss() }
174 }).result.then(function () {
175 bucketSvc.deleteBucket(bucketSvc.currentBucket.id())
177 bucketSvc.allBuckets = [];
178 $location.path('/circ/patron/bucket/view');
183 // retrieves the requested bucket by ID
184 $scope.openSharedBucketDialog = function() {
186 templateUrl: './circ/patron/bucket/t_load_shared',
189 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
190 $scope.focusMe = true;
191 $scope.ok = function(args) {
192 if (args && args.id) {
193 $uibModalInstance.close(args.id)
196 $scope.cancel = function() { $uibModalInstance.dismiss() }
198 }).result.then(function(id) {
199 // RecordBucketCtrl $scope is not inherited by the
200 // modal, so we need to call loadBucket from the
202 $scope.loadBucket(id);
208 .controller('PendingCtrl',
209 ['$scope','$routeParams','bucketSvc','egGridDataProvider', 'egCore','ngToast','$q',
210 function($scope, $routeParams, bucketSvc , egGridDataProvider, egCore , ngToast , $q) {
211 $scope.setTab('add');
214 $scope.gridControls = {
215 setQuery : function(q) {
216 if (bucketSvc.pendingList.length)
217 return {id : bucketSvc.pendingList};
223 $scope.$watch('barcodesFromFile', function(newVal, oldVal) {
224 if (newVal && newVal != oldVal) {
226 // $scope.resetPendingList(); // ??? Add instead of replace
227 angular.forEach(newVal.split(/\n/), function(line) {
229 // scrub any trailing spaces or commas from the barcode
230 line = line.replace(/(.*?)($|\s.*|,.*)/,'$1');
236 {barcode : barcodes},
240 $scope.gridControls.setQuery({id : bucketSvc.pendingList});
244 bucketSvc.pendingList.push(card.usr());
250 $scope.search = function() {
251 bucketSvc.barcodeRecords = [];
255 {barcode : bucketSvc.barcodeString},
257 ).then(null, null, function(card) {
258 bucketSvc.pendingList.push(card.usr());
259 $scope.gridControls.setQuery({id : bucketSvc.pendingList});
261 bucketSvc.barcodeString = '';
264 $scope.resetPendingList = function() {
265 bucketSvc.pendingList = [];
266 $scope.gridControls.setQuery({});
269 $scope.$parent.resetPendingList = $scope.resetPendingList;
271 //remove entry from PendingList
272 $scope.removeFromPendingList = function(usr) {
273 const index = bucketSvc.pendingList.indexOf(usr);
275 bucketSvc.pendingList.splice(index,1);
278 $scope.$parent.removeFromPendingList = $scope.removeFromPendingList;
280 $scope.resetPendingListQuery = function() {
281 $scope.gridControls.setQuery({id : bucketSvc.pendingList});
283 $scope.$parent.resetPendingListQuery = $scope.resetPendingListQuery;
285 if ($routeParams.id &&
286 (!bucketSvc.currentBucket ||
287 bucketSvc.currentBucket.id() != $routeParams.id)) {
288 // user has accessed this page cold with a bucket ID.
289 // fetch the bucket for display, then set the totalCount
290 // (also for display), but avoid fully fetching the bucket,
291 // since it's premature, in this UI.
292 bucketSvc.fetchBucket($routeParams.id);
294 $scope.gridControls.setQuery();
297 .controller('ViewCtrl',
298 ['$scope','$q','$routeParams','$timeout','$window','$uibModal','bucketSvc','egCore','egUser',
299 'egConfirmDialog','egPerm','ngToast','$filter',
300 function($scope, $q , $routeParams , $timeout , $window , $uibModal , bucketSvc , egCore , egUser ,
301 egConfirmDialog , egPerm , ngToast , $filter) {
303 $scope.setTab('view');
304 $scope.bucketId = $routeParams.id;
307 $scope.gridControls = {
308 setQuery : function(q) {
314 $scope.modifyStatcats = function() {
315 bucketSvc.bucketNeedsRefresh = true;
318 templateUrl: './circ/patron/bucket/t_update_statcats',
321 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
322 $scope.running = false;
323 $scope.complete = false;
326 $scope.modal = $uibModalInstance;
327 $scope.ok = function(args) { $uibModalInstance.close() }
328 $scope.cancel = function () { $uibModalInstance.dismiss() }
330 $scope.current_bucket = bucketSvc.currentBucket;
334 'open-ils.circ.stat_cat.actor.retrieve.all',
335 egCore.auth.token(), egCore.auth.user().ws_ou()
336 ).then(function(cats) {
337 cats = cats.sort(function(a, b) {
338 return a.name() < b.name() ? -1 : 1});
339 angular.forEach(cats, function(cat) {
341 cat.allow_freetext(parseInt(cat.allow_freetext())); // just to be sure
343 cat.entries().sort(function(a,b) {
344 return a.value() < b.value() ? -1 : 1
348 $scope.stat_cats = cats;
351 // This handels the progress magic instead of a normal close handler
352 $scope.$on('modal.closing', function(event, reason, closed) {
353 if (!closed) return; // dismissed
354 if ($scope.complete) return; // already done
356 $scope.running = true;
358 var changes = {remove:[], apply:{}};
359 angular.forEach($scope.stat_cats, function (sc) {
361 changes.remove.push(sc.id());
362 } else if (sc.new_value) {
363 changes.apply[sc.id()] = sc.new_value;
369 'open-ils.actor.container.user.batch_statcat_apply',
370 egCore.auth.token(), bucketSvc.currentBucket.id(), changes
373 $scope.complete = true;
374 $scope.modal.close();
377 function (err) { console.log('User edit error: ' + err); },
380 ngToast.warning(p.error);
382 if (p.stage == 'COMPLETE') return;
384 p.label = egCore.strings[p.stage];
389 $scope.states[p.ord] = p;
393 return event.preventDefault();
400 function drawBucket() {
401 return bucketSvc.fetchBucket($scope.bucketId).then(
403 var ids = bucket.items().map(
404 function(i){return i.target_user()}
407 $scope.gridControls.setQuery({id : ids});
409 $scope.gridControls.setQuery({});
415 $scope.no_update_perms = true;
416 $scope.noUpdatePerms = function () { return $scope.no_update_perms; }
418 egPerm.hasPermHere(['UPDATE_USER']).then(
420 if (Object.keys(hash).length == 0) return;
422 var one_false = false;
423 angular.forEach(hash, function(has) {
424 if (!has) one_false = true;
427 if (!one_false) $scope.no_update_perms = false;
431 function annotate_groups(grps) {
432 angular.forEach(grps, function (g) {
433 if (!g.hasOwnProperty('cannot_use')) {
434 if (g.usergroup() == 'f') {
436 } else if (g.application_perm) {
437 egPerm.hasPermHere(['EVERYTHING',g.application_perm]).then(
439 if (Object.keys(hash).length == 0) {
444 var one_false = false;
445 angular.forEach(hash, function(has) {
446 if (has) g.cannot_use = false;
451 g.cannot_use = false;
457 $scope.viewChangesets = function() {
458 bucketSvc.bucketNeedsRefresh = true;
461 templateUrl: './circ/patron/bucket/t_changesets',
464 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
465 $scope.running = false;
466 $scope.complete = false;
469 $scope.focusMe = true;
470 $scope.modal = $uibModalInstance;
471 $scope.ok = function() { $uibModalInstance.close() }
472 $scope.cancel = function () { $uibModalInstance.dismiss() }
474 $scope.current_bucket = bucketSvc.currentBucket;
475 $scope.fieldset_groups = [];
477 $scope.deleteChangeset = function (grp) {
478 egCore.pcrud.remove(grp).then(
480 if (grp.rollback_group()) {
482 .retrieve('afsg',grp.rollback_group())
484 egCore.pcrud.remove(g)
485 .then( function () { refresh_groups() } );
490 return event.preventDefault();
493 function refresh_groups () {
494 $scope.fieldset_groups = [];
495 egCore.pcrud.search('afsg',{
496 rollback_group : { '>' : 0 },
497 container : bucketSvc.currentBucket.id(),
498 container_type : 'user'
499 } ).then( null,null,function(g) {
500 $scope.fieldset_groups.push(g);
509 $scope.applyRollback = function() {
510 bucketSvc.bucketNeedsRefresh = true;
513 templateUrl: './circ/patron/bucket/t_rollback',
515 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
516 $scope.running = false;
517 $scope.complete = false;
519 $scope.revert_me = null;
521 $scope.focusMe = true;
522 $scope.modal = $uibModalInstance;
523 $scope.ok = function(args) { $uibModalInstance.close() }
524 $scope.cancel = function () { $uibModalInstance.dismiss() }
526 $scope.current_bucket = bucketSvc.currentBucket;
527 $scope.revertable_fieldset_groups = [];
529 egCore.pcrud.search('afsg',{
530 rollback_group : { '>' : 0},
531 rollback_time : null,
532 container : bucketSvc.currentBucket.id(),
533 container_type : 'user'
534 } ).then( null,null,function(g) {
535 $scope.revertable_fieldset_groups.push(g);
538 // This handels the progress magic instead of a normal close handler
539 $scope.$on('modal.closing', function(event, reason, closed) {
540 if (!$scope.revert_me) return;
541 if (!closed) return; // dismissed
542 if ($scope.complete) return; // already done
544 $scope.running = true;
549 'open-ils.actor.container.user.apply_rollback',
550 egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.revert_me.id()
553 $scope.complete = true;
554 $scope.modal.close();
557 function (err) { console.log('User edit error: ' + err); },
559 last_stage = p.stage;
561 ngToast.warning(p.error);
563 if (p.stage == 'COMPLETE') return;
565 p.label = egCore.strings[p.stage];
570 $scope.states[p.ord] = p;
573 if (last_stage != 'COMPLETE')
574 ngToast.warning(egCore.strings.BATCH_FAILED);
577 return event.preventDefault();
583 $scope.updateAllUsers = function() {
584 bucketSvc.bucketNeedsRefresh = true;
587 templateUrl: './circ/patron/bucket/t_update_all',
590 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
591 $scope.running = false;
592 $scope.complete = false;
594 $scope.home_ou_name = '';
595 $scope.args = {home_ou:null};
596 $scope.focusMe = true;
597 $scope.modal = $uibModalInstance;
598 $scope.ok = function(args) { $uibModalInstance.close() }
599 $scope.cancel = function () { $uibModalInstance.dismiss() }
601 $scope.disable_home_org = function(org_id) {
603 var org = egCore.org.get(org_id);
607 org.ou_type().can_have_users() == 'f'
611 $scope.pgt_depth = function(grp) {
613 while (grp = egCore.env.pgt.map[grp.parent()]) d++;
617 if (egCore.env.cnal) {
618 $scope.net_access_levels = egCore.env.cnal.list;
620 egCore.pcrud.retrieveAll('cnal', {}, {atomic : true})
621 .then(function(types) {
622 egCore.env.absorbList(types, 'cnal')
623 $scope.net_access_levels = egCore.env.cnal.list;
627 if (egCore.env.pgt) {
628 $scope.profiles = egCore.env.pgt.list;
629 annotate_groups($scope.profiles);
631 egCore.pcrud.search('pgt', {parent : null},
632 {flesh : -1, flesh_fields : {pgt : ['children']}}
635 egCore.env.absorbTree(tree, 'pgt')
636 $scope.profiles = egCore.env.pgt.list;
637 annotate_groups($scope.profiles);
642 $scope.unset_field = function (event,field) {
643 $scope.args[field] = null;
644 return event.preventDefault();
647 // This handels the progress magic instead of a normal close handler
648 $scope.$on('modal.closing', function(event, reason, closed) {
649 if (!$scope.args || !$scope.args.name) return;
650 if (!closed) return; // dismissed
651 if ($scope.complete) return; // already done
653 $scope.running = true;
655 // XXX fix up $scope.args values here
656 if ($scope.args.home_ou) {
657 $scope.args.home_ou = $scope.args.home_ou.id();
659 if ($scope.args.net_access_level) {
660 $scope.args.net_access_level = $scope.args.net_access_level.id();
662 if ($scope.args.profile) {
663 $scope.args.profile = $scope.args.profile.id();
665 if ($scope.args.expire_date) {
666 $scope.args.expire_date = $scope.args.expire_date.toJSON().substr(0,10);
669 for (var key in $scope.args) {
670 if (!$scope.args[key] && $scope.args[key] !== 0) {
671 delete $scope.args[key];
678 'open-ils.actor.container.user.batch_edit',
679 egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.args.name, $scope.args
682 $scope.complete = true;
683 $scope.modal.close();
686 function (err) { console.log('User edit error: ' + err); },
688 last_stage = p.stage;
690 ngToast.warning(p.error);
692 if (p.stage == 'COMPLETE') return;
694 p.label = egCore.strings[p.stage];
699 $scope.states[p.ord] = p;
702 if (last_stage != 'COMPLETE')
703 ngToast.warning(egCore.strings.BATCH_FAILED);
706 return event.preventDefault();
712 $scope.no_delete_perms = true;
713 $scope.noDeletePerms = function () { return $scope.no_delete_perms; }
715 egPerm.hasPermHere(['UPDATE_USER','DELETE_USER']).then(
717 if (Object.keys(hash).length == 0) return;
719 var one_false = false;
720 angular.forEach(hash, function(has) {
721 if (!has) one_false = true;
724 if (!one_false) $scope.no_delete_perms = false;
728 $scope.deleteAllUsers = function() {
729 bucketSvc.bucketNeedsRefresh = true;
732 templateUrl: './circ/patron/bucket/t_delete_all',
735 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
736 $scope.running = false;
737 $scope.complete = false;
740 $scope.focusMe = true;
741 $scope.modal = $uibModalInstance;
742 $scope.ok = function(args) { $uibModalInstance.close() }
743 $scope.cancel = function () { $uibModalInstance.dismiss() }
745 // This handels the progress magic instead of a normal close handler
746 $scope.$on('modal.closing', function(event, reason, closed) {
747 if (!$scope.args || !$scope.args.name) return;
748 if (!closed) return; // dismissed
749 if ($scope.complete) return; // already done
751 $scope.running = true;
756 'open-ils.actor.container.user.batch_delete',
757 egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.args.name, { deleted : 't' }
760 $scope.complete = true;
761 $scope.modal.close();
764 function (err) { console.log('User deletion error: ' + err); },
766 last_stage = p.stage;
768 ngToast.warning(p.error);
770 if (p.stage == 'COMPLETE') return;
772 p.label = egCore.strings[p.stage];
777 $scope.states[p.ord] = p;
780 if (last_stage != 'COMPLETE')
781 ngToast.warning(egCore.strings.BATCH_FAILED);
784 return event.preventDefault();
791 $scope.detachUsers = function(users) {
793 angular.forEach(users, function(rec) {
794 var item = bucketSvc.currentBucket.items().filter(
796 return (i.target_user() == rec.id)
800 promises.push(bucketSvc.detachUser(item[0].id()));
803 bucketSvc.bucketNeedsRefresh = true;
804 return $q.all(promises).then(drawBucket);
807 $scope.spawnUserEdit = function (users) {
808 angular.forEach($scope.gridControls.selectedItems(), function (i) {
809 var url = egCore.env.basePath + 'circ/patron/' + i.id + '/edit';
810 $timeout(function() { $window.open(url, '_blank') });
814 // fetch the bucket; on error show the not-allowed message
816 drawBucket()['catch'](function() { $scope.forbidden = true });