From: Galen Charlton Date: Thu, 24 Aug 2017 21:39:43 +0000 (-0400) Subject: Merge remote-tracking branch 'working/user/kmlussier/lp-1689608-patron_batch_edit_reb... X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=428c4bca83f2e5fd99f87471685d03d9c5f38b24;p=working%2FEvergreen.git Merge remote-tracking branch 'working/user/kmlussier/lp-1689608-patron_batch_edit_rebased' into user/gmcharlt/tmp-serials-batch-patron-edit-merge Conflicts: Open-ILS/src/templates/staff/circ/patron/index.tt2 Open-ILS/web/js/ui/default/staff/circ/patron/app.js --- 428c4bca83f2e5fd99f87471685d03d9c5f38b24 diff --cc Open-ILS/src/templates/staff/circ/patron/index.tt2 index d2908d39aa,799f467860..4983701aa7 --- a/Open-ILS/src/templates/staff/circ/patron/index.tt2 +++ b/Open-ILS/src/templates/staff/circ/patron/index.tt2 @@@ -62,7 -62,8 +63,9 @@@ angular.module('egCoreMod').run(['egStr s.PATRON_PURGE_OVERRIDE_PROMPT = "[% l('The account has open transactions (circulations and/or unpaid bills). Purge anyway?') %]"; s.OPT_IN_DIALOG_TITLE = "[% l('Verify Permission to Share Personal Information') %]"; s.OPT_IN_DIALOG = "[% l('Does patron [_1], [_2] from [_3] ([_4]) consent to having their personal information shared with your library?', '{{family_name}}', '{{first_given_name}}', '{{org_name}}', '{{org_shortname}}') %]"; + s.PATRON_EDIT_COLLISION = "[% l('Patron record was modified by another user while you were editing it. Your changes were not saved; please reapply them.') %]"; + s.BUCKET_ADD_SUCCESS = "[% l('Successfully added [_1] users to bucket [_2].', '{{count}}', '{{name}}') %]"; + s.BUCKET_ADD_FAIL = "[% l('Failed to add [_1] users to bucket [_2].', '{{count}}', '{{name}}') %]"; }]); diff --cc Open-ILS/web/js/ui/default/staff/circ/patron/app.js index cd8a36e7aa,3fe883a474..1ad4f6a26d --- a/Open-ILS/web/js/ui/default/staff/circ/patron/app.js +++ b/Open-ILS/web/js/ui/default/staff/circ/patron/app.js @@@ -4,9 -4,8 +4,9 @@@ * Search, checkout, items out, holds, bills, edit, etc. */ - angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', -angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', 'egUserBucketMod', - 'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod', 'ngToast']) ++angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap', 'egUserBucketMod', + 'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod', 'ngToast', + 'egPatronSearchMod']) .config(['ngToastProvider', function(ngToastProvider) { ngToastProvider.configure({ @@@ -527,23 -886,153 +527,81 @@@ function($scope , $location , egCore , * Manages patron search */ .controller('PatronSearchCtrl', - ['$scope','$q','$routeParams','$timeout','$window','$location','egCore', - '$filter','egUser', 'patronSvc','egGridDataProvider','$document', - 'egPatronMerge','egProgressDialog','$controller', - function($scope, $q, $routeParams, $timeout, $window, $location, egCore, - $filter, egUser, patronSvc , egGridDataProvider , $document, - egPatronMerge , egProgressDialog, $controller) { + ['$scope','$q','$routeParams','$timeout','$window','$location','egCore','ngToast', + '$filter','egUser', 'patronSvc','egGridDataProvider','$document','bucketSvc', - 'egPatronMerge','egProgressDialog','$interpolate','$uibModal', ++ 'egPatronMerge','egProgressDialog','$interpolate','$uibModal','$controller', + function($scope, $q, $routeParams, $timeout, $window, $location, egCore , ngToast, + $filter, egUser, patronSvc , egGridDataProvider , $document , bucketSvc, - egPatronMerge , egProgressDialog , $interpolate , $uibModal) { ++ egPatronMerge , egProgressDialog , $interpolate , $uibModal , $controller) { + angular.extend(this, $controller('BasePatronSearchCtrl', {$scope : $scope})); $scope.initTab('search'); - $scope.focusMe = true; - $scope.searchArgs = { - // default to searching globally - home_ou : egCore.org.tree() - }; - - // last used patron search form element - var lastFormElement; $scope.gridControls = { activateItem : function(item) { $location.path('/circ/patron/' + item.id() + '/checkout'); }, - selectedItems : function() {return []} + selectedItems : function() { return [] } + } + + $scope.bucketSvc = bucketSvc; + $scope.bucketSvc.fetchUserBuckets(); + $scope.addToBucket = function(item, data, recs) { + if (recs.length == 0) return; + var added_count = 0; + var failed_count = 0; + var p = []; + angular.forEach(recs, + function(rec) { + var item = new egCore.idl.cubi(); + item.bucket(data.id()); + item.target_user(rec.id()); + p.push(egCore.net.request( + 'open-ils.actor', + 'open-ils.actor.container.item.create', + egCore.auth.token(), 'user', item + ).then( + function(){ added_count++ }, + function(){ failed_count++ } + )); + } + ); + + $q.all(p).then( function () { + if (added_count) ngToast.create($interpolate(egCore.strings.BUCKET_ADD_SUCCESS)({ count: ''+added_count, name: data.name()} )); + if (failed_count) ngToast.warning($interpolate(egCore.strings.BUCKET_ADD_FAIL)({ count: ''+failed_count, name: data.name() } )); + }); + } + + var temp_scope = $scope; + $scope.openCreateBucketDialog = function() { + $uibModal.open({ + templateUrl: './circ/patron/bucket/t_bucket_create', + controller: + ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) { + $scope.focusMe = true; + $scope.ok = function(args) { $uibModalInstance.close(args) } + $scope.cancel = function () { $uibModalInstance.dismiss() } + }] + }).result.then(function (args) { + if (!args || !args.name) return; + bucketSvc.createBucket(args.name, args.desc).then( + function(id) { + if (id) { + $scope.bucketSvc.fetchBucket(id).then(function (b) { + $scope.addToBucket( + null, + b, + $scope.gridControls.selectedItems() + ); + $scope.bucketSvc.fetchUserBuckets(true); + }); + } + } + ); + }); } - // Handle URL-encoded searches - if ($location.search().search) { - console.log('URL search = ' + $location.search().search); - patronSvc.urlSearch = {search : JSON2js($location.search().search)}; - - // why the double-JSON encoded sort? - if (patronSvc.urlSearch.search.search_sort) { - patronSvc.urlSearch.sort = - JSON2js(patronSvc.urlSearch.search.search_sort); - } else { - patronSvc.urlSearch.sort = []; - } - delete patronSvc.urlSearch.search.search_sort; - - // include inactive patrons if "inactive" param - if ($location.search().inactive) { - patronSvc.urlSearch.inactive = $location.search().inactive; - } - } - - var propagate; - var propagate_inactive; - if (patronSvc.lastSearch) { - propagate = patronSvc.lastSearch.search; - // home_ou needs to be treated specially - propagate.home_ou = { - value : patronSvc.lastSearch.home_ou, - group : 0 - }; - } else if (patronSvc.urlSearch) { - propagate = patronSvc.urlSearch.search; - if (patronSvc.urlSearch.inactive) { - propagate_inactive = patronSvc.urlSearch.inactive; - } - } - - if (egCore.env.pgt) { - $scope.profiles = egCore.env.pgt.list; - } else { - egCore.pcrud.search('pgt', {parent : null}, - {flesh : -1, flesh_fields : {pgt : ['children']}} - ).then( - function(tree) { - egCore.env.absorbTree(tree, 'pgt') - $scope.profiles = egCore.env.pgt.list; - } - ); - } - - if (propagate) { - // populate the search form with our cached / preexisting search info - angular.forEach(propagate, function(val, key) { - if (key == 'profile') - val.value = $scope.profiles.filter(function(p) { return p.id() == val.value })[0]; - if (key == 'home_ou') - val.value = egCore.org.get(val.value); - $scope.searchArgs[key] = val.value; - }); - if (propagate_inactive) { - $scope.searchArgs.inactive = propagate_inactive; - } - } - - var provider = egGridDataProvider.instance({}); - $scope.$watch( function() {return $scope.gridControls.selectedItems()}, function(list) { @@@ -552,7 -1041,233 +610,11 @@@ }, true ); - - provider.get = function(offset, count) { - var deferred = $q.defer(); - - var fullSearch; - if (patronSvc.urlSearch) { - fullSearch = patronSvc.urlSearch; - // enusre the urlSearch only runs once. - delete patronSvc.urlSearch; - - } else { - patronSvc.search_barcode = $scope.searchArgs.card; - - var search = compileSearch($scope.searchArgs); - if (Object.keys(search) == 0) return $q.when(); - - var home_ou = search.home_ou; - delete search.home_ou; - var inactive = search.inactive; - delete search.inactive; - - fullSearch = { - search : search, - sort : compileSort(), - inactive : inactive, - home_ou : home_ou, - }; - } - - fullSearch.count = count; - fullSearch.offset = offset; - - if (patronSvc.lastSearch) { - // search repeated, return the cached results - if (angular.equals(fullSearch, patronSvc.lastSearch)) { - console.log('patron search returning ' + - patronSvc.patrons.length + ' cached results'); - - // notify has to happen after returning the promise - $timeout( - function() { - angular.forEach(patronSvc.patrons, function(user) { - deferred.notify(user); - }); - deferred.resolve(); - } - ); - return deferred.promise; - } - } - - patronSvc.lastSearch = fullSearch; - - if (fullSearch.search.id) { - // search by user id performs a direct ID lookup - var userId = fullSearch.search.id.value; - $timeout( - function() { - egUser.get(userId).then(function(user) { - patronSvc.localFlesh(user); - patronSvc.patrons = [user]; - deferred.notify(user); - deferred.resolve(); - }); - } - ); - return deferred.promise; - } - - if (!Object.keys(fullSearch.search).length) { - // Empty searches are rejected by the server. Avoid - // running the the empty search that runs on page load. - return $q.when(); - } - - egProgressDialog.open(); // Indeterminate - - patronSvc.patrons = []; - var which_sound = 'success'; - egCore.net.request( - 'open-ils.actor', - 'open-ils.actor.patron.search.advanced.fleshed', - egCore.auth.token(), - fullSearch.search, - fullSearch.count, - fullSearch.sort, - fullSearch.inactive, - fullSearch.home_ou, - egUser.defaultFleshFields, - fullSearch.offset - - ).then( - function() { - deferred.resolve(); - }, - function() { // onerror - which_sound = 'error'; - }, - function(user) { - // hide progress bar as soon as the first result appears. - egProgressDialog.close(); - patronSvc.localFlesh(user); // inline - patronSvc.patrons.push(user); - deferred.notify(user); - } - )['finally'](function() { // close on 0-hits or error - if (which_sound == 'success' && patronSvc.patrons.length == 0) { - which_sound = 'warning'; - } - egCore.audio.play(which_sound + '.patron.by_search'); - egProgressDialog.close(); - }); - - return deferred.promise; - }; - - $scope.patronSearchGridProvider = provider; - - // determine the tree depth of the profile group - $scope.pgt_depth = function(grp) { - var d = 0; - while (grp = egCore.env.pgt.map[grp.parent()]) d++; - return d; - } - - $scope.clearForm = function () { - $scope.searchArgs={}; - if (lastFormElement) lastFormElement.focus(); - } - - $scope.applyShowExtras = function($event, bool) { - if (bool) { - $scope.showExtras = true; - egCore.hatch.setItem('eg.circ.patron.search.show_extras', true); - } else { - $scope.showExtras = false; - egCore.hatch.removeItem('eg.circ.patron.search.show_extras'); - } - if (lastFormElement) lastFormElement.focus(); - $event.preventDefault(); - } - - egCore.hatch.getItem('eg.circ.patron.search.show_extras') - .then(function(val) {$scope.showExtras = val}); - - // map form arguments into search params - function compileSearch(args) { - var search = {}; - angular.forEach(args, function(val, key) { - if (!val) return; - if (key == 'profile' && args.profile) { - search.profile = {value : args.profile.id(), group : 0}; - } else if (key == 'home_ou' && args.home_ou) { - search.home_ou = args.home_ou.id(); // passed separately - } else if (key == 'inactive') { - search.inactive = val; - } else { - search[key] = {value : val, group : 0}; - } - if (key.match(/phone|ident/)) { - search[key].group = 2; - } else { - if (key.match(/street|city|state|post_code/)) { - search[key].group = 1; - } else if (key == 'card') { - search[key].group = 3 - } - } - }); - - return search; - } - - function compileSort() { - - if (!provider.sort.length) { - return [ // default - "family_name ASC", - "first_given_name ASC", - "second_given_name ASC", - "dob DESC" - ]; - } - - var sort = []; - angular.forEach( - provider.sort, - function(sortdef) { - if (angular.isObject(sortdef)) { - var name = Object.keys(sortdef)[0]; - var dir = sortdef[name]; - sort.push(name + ' ' + dir); - } else { - sort.push(sortdef); - } - } - ); - - return sort; - } - - $scope.setLastFormElement = function() { - lastFormElement = $document[0].activeElement; - } - - // search form submit action; tells the results grid to - // refresh itself. - $scope.search = function(args) { // args === $scope.searchArgs - if (args && Object.keys(args).length) - $scope.gridControls.refresh(); - if (lastFormElement) lastFormElement.focus(); - } - - // TODO: move this into the (forthcoming) grid row activate action - $scope.onPatronDblClick = function($event, user) { - $location.path('/circ/patron/' + user.id() + '/checkout'); - } - - if (patronSvc.urlSearch) { - // force the grid to load the url-based search on page load - provider.refresh(); - } + $scope.need_one_selected = function() { + var items = $scope.gridControls.selectedItems(); + return (items.length > 0) ? false : true; + } $scope.need_two_selected = function() { var items = $scope.gridControls.selectedItems(); return (items.length == 2) ? false : true; diff --cc Open-ILS/web/js/ui/default/staff/test/karma.conf.js index 049f677dff,855fde1fe8..a5e9d26722 --- a/Open-ILS/web/js/ui/default/staff/test/karma.conf.js +++ b/Open-ILS/web/js/ui/default/staff/test/karma.conf.js @@@ -44,8 -44,8 +44,9 @@@ module.exports = function(config) 'services/ui.js', 'services/grid.js', 'services/op_change.js', + 'services/patron_search.js', 'services/navbar.js', 'services/date.js', + 'services/user-bucket.js', // load app scripts 'app.js', 'circ/**/*.js',