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',
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');
229 promises.push(egCore.pcrud.search(
233 ).then(null, null, function(card) {
234 bucketSvc.pendingList.push(card.usr());
238 $q.all(promises).then(function () {
239 $scope.gridControls.setQuery({id : bucketSvc.pendingList});
244 $scope.search = function() {
245 bucketSvc.barcodeRecords = [];
249 {barcode : bucketSvc.barcodeString},
251 ).then(null, null, function(card) {
252 bucketSvc.pendingList.push(card.usr());
253 $scope.gridControls.setQuery({id : bucketSvc.pendingList});
255 bucketSvc.barcodeString = '';
258 $scope.resetPendingList = function() {
259 bucketSvc.pendingList = [];
260 $scope.gridControls.setQuery({});
263 $scope.$parent.resetPendingList = $scope.resetPendingList;
265 if ($routeParams.id &&
266 (!bucketSvc.currentBucket ||
267 bucketSvc.currentBucket.id() != $routeParams.id)) {
268 // user has accessed this page cold with a bucket ID.
269 // fetch the bucket for display, then set the totalCount
270 // (also for display), but avoid fully fetching the bucket,
271 // since it's premature, in this UI.
272 bucketSvc.fetchBucket($routeParams.id);
274 $scope.gridControls.setQuery();
277 .controller('ViewCtrl',
278 ['$scope','$q','$routeParams','$timeout','$window','$uibModal','bucketSvc','egCore','egUser',
279 'egConfirmDialog','egPerm','ngToast','$filter',
280 function($scope, $q , $routeParams , $timeout , $window , $uibModal , bucketSvc , egCore , egUser ,
281 egConfirmDialog , egPerm , ngToast , $filter) {
283 $scope.setTab('view');
284 $scope.bucketId = $routeParams.id;
287 $scope.gridControls = {
288 setQuery : function(q) {
294 $scope.modifyStatcats = function() {
295 bucketSvc.bucketNeedsRefresh = true;
298 templateUrl: './circ/patron/bucket/t_update_statcats',
301 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
302 $scope.running = false;
303 $scope.complete = false;
306 $scope.modal = $uibModalInstance;
307 $scope.ok = function(args) { $uibModalInstance.close() }
308 $scope.cancel = function () { $uibModalInstance.dismiss() }
310 $scope.current_bucket = bucketSvc.currentBucket;
314 'open-ils.circ.stat_cat.actor.retrieve.all',
315 egCore.auth.token(), egCore.auth.user().ws_ou()
316 ).then(function(cats) {
317 cats = cats.sort(function(a, b) {
318 return a.name() < b.name() ? -1 : 1});
319 angular.forEach(cats, function(cat) {
321 cat.allow_freetext(parseInt(cat.allow_freetext())); // just to be sure
323 cat.entries().sort(function(a,b) {
324 return a.value() < b.value() ? -1 : 1
328 $scope.stat_cats = cats;
331 // This handels the progress magic instead of a normal close handler
332 $scope.$on('modal.closing', function(event, reason, closed) {
333 if (!closed) return; // dismissed
334 if ($scope.complete) return; // already done
336 $scope.running = true;
338 var changes = {remove:[], apply:{}};
339 angular.forEach($scope.stat_cats, function (sc) {
341 changes.remove.push(sc.id());
342 } else if (sc.new_value) {
343 changes.apply[sc.id()] = sc.new_value;
349 'open-ils.actor.container.user.batch_statcat_apply',
350 egCore.auth.token(), bucketSvc.currentBucket.id(), changes
353 $scope.complete = true;
354 $scope.modal.close();
357 function (err) { console.log('User edit error: ' + err); },
360 ngToast.warning(p.error);
362 if (p.stage == 'COMPLETE') return;
364 p.label = egCore.strings[p.stage];
369 $scope.states[p.ord] = p;
373 return event.preventDefault();
380 function drawBucket() {
381 return bucketSvc.fetchBucket($scope.bucketId).then(
383 var ids = bucket.items().map(
384 function(i){return i.target_user()}
387 $scope.gridControls.setQuery({id : ids});
389 $scope.gridControls.setQuery({});
395 $scope.no_update_perms = true;
396 $scope.noUpdatePerms = function () { return $scope.no_update_perms; }
398 egPerm.hasPermHere(['UPDATE_USER']).then(
400 if (Object.keys(hash).length == 0) return;
402 var one_false = false;
403 angular.forEach(hash, function(has) {
404 if (!has) one_false = true;
407 if (!one_false) $scope.no_update_perms = false;
411 function annotate_groups(grps) {
412 angular.forEach(grps, function (g) {
413 if (!g.hasOwnProperty('cannot_use')) {
414 if (g.usergroup() == 'f') {
416 } else if (g.application_perm) {
417 egPerm.hasPermHere(['EVERYTHING',g.application_perm]).then(
419 if (Object.keys(hash).length == 0) {
424 var one_false = false;
425 angular.forEach(hash, function(has) {
426 if (has) g.cannot_use = false;
431 g.cannot_use = false;
437 $scope.viewChangesets = function() {
438 bucketSvc.bucketNeedsRefresh = true;
441 templateUrl: './circ/patron/bucket/t_changesets',
444 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
445 $scope.running = false;
446 $scope.complete = false;
449 $scope.focusMe = true;
450 $scope.modal = $uibModalInstance;
451 $scope.ok = function() { $uibModalInstance.close() }
452 $scope.cancel = function () { $uibModalInstance.dismiss() }
454 $scope.current_bucket = bucketSvc.currentBucket;
455 $scope.fieldset_groups = [];
457 $scope.deleteChangeset = function (grp) {
458 egCore.pcrud.remove(grp).then(
460 if (grp.rollback_group()) {
462 .retrieve('afsg',grp.rollback_group())
464 egCore.pcrud.remove(g)
465 .then( function () { refresh_groups() } );
470 return event.preventDefault();
473 function refresh_groups () {
474 $scope.fieldset_groups = [];
475 egCore.pcrud.search('afsg',{
476 rollback_group : { '>' : 0 },
477 container : bucketSvc.currentBucket.id(),
478 container_type : 'user'
479 } ).then( null,null,function(g) {
480 $scope.fieldset_groups.push(g);
489 $scope.applyRollback = function() {
490 bucketSvc.bucketNeedsRefresh = true;
493 templateUrl: './circ/patron/bucket/t_rollback',
495 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
496 $scope.running = false;
497 $scope.complete = false;
499 $scope.revert_me = null;
501 $scope.focusMe = true;
502 $scope.modal = $uibModalInstance;
503 $scope.ok = function(args) { $uibModalInstance.close() }
504 $scope.cancel = function () { $uibModalInstance.dismiss() }
506 $scope.current_bucket = bucketSvc.currentBucket;
507 $scope.revertable_fieldset_groups = [];
509 egCore.pcrud.search('afsg',{
510 rollback_group : { '>' : 0},
511 rollback_time : null,
512 container : bucketSvc.currentBucket.id(),
513 container_type : 'user'
514 } ).then( null,null,function(g) {
515 $scope.revertable_fieldset_groups.push(g);
518 // This handels the progress magic instead of a normal close handler
519 $scope.$on('modal.closing', function(event, reason, closed) {
520 if (!$scope.revert_me) return;
521 if (!closed) return; // dismissed
522 if ($scope.complete) return; // already done
524 $scope.running = true;
529 'open-ils.actor.container.user.apply_rollback',
530 egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.revert_me.id()
533 $scope.complete = true;
534 $scope.modal.close();
537 function (err) { console.log('User edit error: ' + err); },
539 last_stage = p.stage;
541 ngToast.warning(p.error);
543 if (p.stage == 'COMPLETE') return;
545 p.label = egCore.strings[p.stage];
550 $scope.states[p.ord] = p;
553 if (last_stage != 'COMPLETE')
554 ngToast.warning(egCore.strings.BATCH_FAILED);
557 return event.preventDefault();
563 $scope.updateAllUsers = function() {
564 bucketSvc.bucketNeedsRefresh = true;
567 templateUrl: './circ/patron/bucket/t_update_all',
570 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
571 $scope.running = false;
572 $scope.complete = false;
574 $scope.home_ou_name = '';
575 $scope.args = {home_ou:null};
576 $scope.focusMe = true;
577 $scope.modal = $uibModalInstance;
578 $scope.ok = function(args) { $uibModalInstance.close() }
579 $scope.cancel = function () { $uibModalInstance.dismiss() }
581 $scope.disable_home_org = function(org_id) {
583 var org = egCore.org.get(org_id);
587 org.ou_type().can_have_users() == 'f'
591 $scope.pgt_depth = function(grp) {
593 while (grp = egCore.env.pgt.map[grp.parent()]) d++;
597 if (egCore.env.cnal) {
598 $scope.net_access_levels = egCore.env.cnal.list;
600 egCore.pcrud.retrieveAll('cnal', {}, {atomic : true})
601 .then(function(types) {
602 egCore.env.absorbList(types, 'cnal')
603 $scope.net_access_levels = egCore.env.cnal.list;
607 if (egCore.env.pgt) {
608 $scope.profiles = egCore.env.pgt.list;
609 annotate_groups($scope.profiles);
611 egCore.pcrud.search('pgt', {parent : null},
612 {flesh : -1, flesh_fields : {pgt : ['children']}}
615 egCore.env.absorbTree(tree, 'pgt')
616 $scope.profiles = egCore.env.pgt.list;
617 annotate_groups($scope.profiles);
622 $scope.unset_field = function (event,field) {
623 $scope.args[field] = null;
624 return event.preventDefault();
627 // This handels the progress magic instead of a normal close handler
628 $scope.$on('modal.closing', function(event, reason, closed) {
629 if (!$scope.args || !$scope.args.name) return;
630 if (!closed) return; // dismissed
631 if ($scope.complete) return; // already done
633 $scope.running = true;
635 // XXX fix up $scope.args values here
636 if ($scope.args.home_ou) {
637 $scope.args.home_ou = $scope.args.home_ou.id();
639 if ($scope.args.net_access_level) {
640 $scope.args.net_access_level = $scope.args.net_access_level.id();
642 if ($scope.args.profile) {
643 $scope.args.profile = $scope.args.profile.id();
645 if ($scope.args.expire_date) {
646 $scope.args.expire_date = $scope.args.expire_date.toJSON().substr(0,10);
649 for (var key in $scope.args) {
650 if (!$scope.args[key] && $scope.args[key] !== 0) {
651 delete $scope.args[key];
658 'open-ils.actor.container.user.batch_edit',
659 egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.args.name, $scope.args
662 $scope.complete = true;
663 $scope.modal.close();
666 function (err) { console.log('User edit error: ' + err); },
668 last_stage = p.stage;
670 ngToast.warning(p.error);
672 if (p.stage == 'COMPLETE') return;
674 p.label = egCore.strings[p.stage];
679 $scope.states[p.ord] = p;
682 if (last_stage != 'COMPLETE')
683 ngToast.warning(egCore.strings.BATCH_FAILED);
686 return event.preventDefault();
692 $scope.no_delete_perms = true;
693 $scope.noDeletePerms = function () { return $scope.no_delete_perms; }
695 egPerm.hasPermHere(['UPDATE_USER','DELETE_USER']).then(
697 if (Object.keys(hash).length == 0) return;
699 var one_false = false;
700 angular.forEach(hash, function(has) {
701 if (!has) one_false = true;
704 if (!one_false) $scope.no_delete_perms = false;
708 $scope.deleteAllUsers = function() {
709 bucketSvc.bucketNeedsRefresh = true;
712 templateUrl: './circ/patron/bucket/t_delete_all',
715 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
716 $scope.running = false;
717 $scope.complete = false;
720 $scope.focusMe = true;
721 $scope.modal = $uibModalInstance;
722 $scope.ok = function(args) { $uibModalInstance.close() }
723 $scope.cancel = function () { $uibModalInstance.dismiss() }
725 // This handels the progress magic instead of a normal close handler
726 $scope.$on('modal.closing', function(event, reason, closed) {
727 if (!$scope.args || !$scope.args.name) return;
728 if (!closed) return; // dismissed
729 if ($scope.complete) return; // already done
731 $scope.running = true;
736 'open-ils.actor.container.user.batch_delete',
737 egCore.auth.token(), bucketSvc.currentBucket.id(), $scope.args.name, { deleted : 't' }
740 $scope.complete = true;
741 $scope.modal.close();
744 function (err) { console.log('User deletion error: ' + err); },
746 last_stage = p.stage;
748 ngToast.warning(p.error);
750 if (p.stage == 'COMPLETE') return;
752 p.label = egCore.strings[p.stage];
757 $scope.states[p.ord] = p;
760 if (last_stage != 'COMPLETE')
761 ngToast.warning(egCore.strings.BATCH_FAILED);
764 return event.preventDefault();
771 $scope.detachUsers = function(users) {
773 angular.forEach(users, function(rec) {
774 var item = bucketSvc.currentBucket.items().filter(
776 return (i.target_user() == rec.id)
780 promises.push(bucketSvc.detachUser(item[0].id()));
783 bucketSvc.bucketNeedsRefresh = true;
784 return $q.all(promises).then(drawBucket);
787 $scope.spawnUserEdit = function (users) {
788 angular.forEach($scope.gridControls.selectedItems(), function (i) {
789 var url = egCore.env.basePath + 'circ/patron/' + i.id + '/edit';
790 $timeout(function() { $window.open(url, '_blank') });
794 // fetch the bucket; on error show the not-allowed message
796 drawBucket()['catch'](function() { $scope.forbidden = true });