*/
angular.module('egPatronApp', ['ngRoute', 'ui.bootstrap',
- 'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod', 'ngToast'])
+ 'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod', 'ngToast',
+ 'egPatronSearchMod'])
.config(['ngToastProvider', function(ngToastProvider) {
ngToastProvider.configure({
})
/**
- * Patron service
- */
-.factory('patronSvc',
- ['$q','$timeout','$location','egCore','egUser','$locale',
-function($q , $timeout , $location , egCore, egUser , $locale) {
-
- var service = {
- // cached patron search results
- patrons : [],
-
- // currently selected patron object
- current : null,
-
- // patron circ stats (overdues, fines, holds)
- patron_stats : null,
-
- // event types manually overridden, which should always be
- // overridden for checkouts to this patron for this instance of
- // the interface.
- checkout_overrides : {},
- //holds the searched barcode
- search_barcode : null,
- };
-
- // when we change the default patron, we need to clear out any
- // data collected on that patron
- service.resetPatronLists = function() {
- service.checkouts = [];
- service.items_out = []
- service.items_out_ids = [];
- service.holds = [];
- service.hold_ids = [];
- service.checkout_overrides = {};
- service.patron_stats = null;
- service.noncat_ids = [];
- service.hasAlerts = false;
- service.patronExpired = false;
- service.patronExpiresSoon = false;
- service.invalidAddresses = false;
- }
- service.resetPatronLists(); // initialize
-
- // Returns true if the last alerted patron matches the current
- // patron. Otherwise, the last alerted patron is set to the
- // current patron and false is returned.
- service.alertsShown = function() {
- var key = 'eg.circ.last_alerted_patron';
- var last_id = egCore.hatch.getSessionItem(key);
- if (last_id && last_id == service.current.id()) return true;
- egCore.hatch.setSessionItem(key, service.current.id());
- return false;
- }
-
- // shortcut to force-reload the current primary
- service.refreshPrimary = function() {
- if (!service.current) return $q.when();
- return service.setPrimary(service.current.id(), null, true);
- }
-
- // clear the currently focused user
- service.clearPrimary = function() {
- // reset with no patron
- service.resetPatronLists();
- service.current = null;
- service.patron_stats = null;
- return $q.when();
- }
-
- // sets the primary display user, fetching data as necessary.
- service.setPrimary = function(id, user, force) {
- var user_id = id ? id : (user ? user.id() : null);
-
- console.debug('setting primary user to: ' + user_id);
-
- if (!user_id) return $q.reject();
-
- // when loading a new patron, update the last patron setting
- if (!service.current || service.current.id() != user_id)
- egCore.hatch.setLoginSessionItem('eg.circ.last_patron', user_id);
-
- // avoid running multiple retrievals for the same patron, which
- // can happen during dbl-click by maintaining a single running
- // data retrieval promise
- if (service.primaryUserPromise) {
- if (service.primaryUserId == user_id) {
- return service.primaryUserPromise.promise;
- } else {
- service.primaryUserPromise = null;
- }
- }
-
- service.primaryUserPromise = $q.defer();
- service.primaryUserId = user_id;
-
- service.getPrimary(id, user, force)
- .then(function() {
- service.checkAlerts();
- var p = service.primaryUserPromise;
- service.primaryUserId = null;
- // clear before resolution just to be safe.
- service.primaryUserPromise = null;
- p.resolve();
- });
-
- return service.primaryUserPromise.promise;
- }
-
- service.getPrimary = function(id, user, force) {
-
- if (user) {
- if (!force && service.current &&
- service.current.id() == user.id()) {
- if (service.patron_stats) {
- return $q.when();
- } else {
- return service.fetchUserStats();
- }
- }
-
- service.resetPatronLists();
- service.current = user;
- service.localFlesh(user);
- return service.fetchUserStats();
-
- } else if (id) {
- if (!force && service.current && service.current.id() == id) {
- if (service.patron_stats) {
- return $q.when();
- } else {
- return service.fetchUserStats();
- }
- }
-
- service.resetPatronLists();
-
- return egUser.get(id).then(
- function(user) {
- service.current = user;
- service.localFlesh(user);
- return service.fetchUserStats();
- },
- function(err) {
- console.error(
- "unable to fetch user "+id+': '+js2JSON(err))
- }
- );
- } else {
-
- // fetching a null user clears the primary user.
- // NOTE: this should probably reject() and log an error,
- // but calling clear for backwards compat for now.
- return service.clearPrimary();
- }
- }
-
- // flesh some additional user fields locally
- service.localFlesh = function(user) {
- if (!angular.isObject(typeof user.home_ou()))
- user.home_ou(egCore.org.get(user.home_ou()));
-
- angular.forEach(
- user.standing_penalties(),
- function(penalty) {
- if (!angular.isObject(penalty.org_unit()))
- penalty.org_unit(egCore.org.get(penalty.org_unit()));
- }
- );
-
- // stat_cat_entries == stat_cat_entry_user_map
- angular.forEach(user.stat_cat_entries(), function(map) {
- if (angular.isObject(map.stat_cat())) return;
- // At page load, we only retrieve org-visible stat cats.
- // For the common case, ignore entries for remote stat cats.
- var cat = egCore.env.actsc.map[map.stat_cat()];
- if (cat) {
- map.stat_cat(cat);
- cat.owner(egCore.org.get(cat.owner()));
- }
- });
- }
-
- // resolves to true if the patron account has expired or will
- // expire soon, based on YAOUS circ.patron_expires_soon_warning
- // note: returning a promise is no longer strictly necessary
- // (no more async activity) if the calling function is changed too.
- service.testExpire = function() {
-
- var expire = Date.parse(service.current.expire_date());
- if (expire < new Date()) {
- return $q.when(service.patronExpired = true);
- }
-
- var soon = egCore.env.aous['circ.patron_expires_soon_warning'];
- if (Number(soon)) {
- var preExpire = new Date();
- preExpire.setDate(preExpire.getDate() + Number(soon));
- if (expire < preExpire)
- return $q.when(service.patronExpiresSoon = true);
- }
-
- return $q.when(false);
- }
-
- // resolves to true if the patron account has any invalid addresses.
- service.testInvalidAddrs = function() {
-
- if (service.invalidAddresses)
- return $q.when(true);
-
- var fail = false;
-
- angular.forEach(
- service.current.addresses(),
- function(addr) { if (addr.valid() == 'f') fail = true }
- );
-
- return $q.when(fail);
- }
- //resolves to true if the patron was fetched with an inactive card
- service.fetchedWithInactiveCard = function() {
- var bc = service.search_barcode
- var cards = service.current.cards();
- var card = cards.filter(function(c) { return c.barcode() == bc })[0];
- return (card && card.active() == 'f');
- }
- // resolves to true if there is any aspect of the patron account
- // which should produce a message in the alerts panel
- service.checkAlerts = function() {
-
- if (service.hasAlerts) // already checked
- return $q.when(true);
-
- var deferred = $q.defer();
- var p = service.current;
-
- if (service.alert_penalties.length ||
- p.alert_message() ||
- p.active() == 'f' ||
- p.barred() == 't' ||
- service.patron_stats.holds.ready) {
-
- service.hasAlerts = true;
- }
-
- // see if the user was retrieved with an inactive card
- if(service.fetchedWithInactiveCard()){
- service.hasAlerts = true;
- }
-
- // regardless of whether we know of alerts, we still need
- // to test/fetch the expire data for display
- service.testExpire().then(function(bool) {
- if (bool) service.hasAlerts = true;
- deferred.resolve(service.hasAlerts);
- });
-
- service.testInvalidAddrs().then(function(bool) {
- if (bool) service.invalidAddresses = true;
- deferred.resolve(service.invalidAddresses);
- });
-
- return deferred.promise;
- }
-
- service.fetchGroupFines = function() {
- return egCore.net.request(
- 'open-ils.actor',
- 'open-ils.actor.usergroup.members.balance_owed',
- egCore.auth.token(), service.current.usrgroup()
- ).then(function(list) {
- var total = 0;
- angular.forEach(list, function(u) {
- total += 100 * Number(u.balance_owed)
- });
- service.patron_stats.fines.group_balance_owed = total / 100;
- });
- }
-
- service.getUserStats = function(id) {
- return egCore.net.request(
- 'open-ils.actor',
- 'open-ils.actor.user.opac.vital_stats.authoritative',
- egCore.auth.token(), id
- ).then(
- function(stats) {
- // force numeric to ensure correct boolean handling in templates
- stats.fines.balance_owed = Number(stats.fines.balance_owed);
- stats.checkouts.overdue = Number(stats.checkouts.overdue);
- stats.checkouts.claims_returned =
- Number(stats.checkouts.claims_returned);
- stats.checkouts.lost = Number(stats.checkouts.lost);
- stats.checkouts.out = Number(stats.checkouts.out);
- stats.checkouts.total_out =
- stats.checkouts.out + stats.checkouts.overdue;
-
- stats.checkouts.total_out += Number(stats.checkouts.long_overdue);
-
- if (!egCore.env.aous['circ.do_not_tally_claims_returned'])
- stats.checkouts.total_out += stats.checkouts.claims_returned;
-
- if (egCore.env.aous['circ.tally_lost'])
- stats.checkouts.total_out += stats.checkouts.lost
-
- return stats;
- }
- );
- }
-
- // Fetches the IDs of any active non-cat checkouts for the current
- // user. Also sets the patron_stats non_cat count value to match.
- service.getUserNonCats = function(id) {
- return egCore.net.request(
- 'open-ils.circ',
- 'open-ils.circ.open_non_cataloged_circulation.user.authoritative',
- egCore.auth.token(), id
- ).then(function(noncat_ids) {
- service.noncat_ids = noncat_ids;
- service.patron_stats.checkouts.noncat = noncat_ids.length;
- });
- }
-
- // grab additional circ info
- service.fetchUserStats = function() {
- return service.getUserStats(service.current.id())
- .then(function(stats) {
- service.patron_stats = stats
- service.alert_penalties = service.current.standing_penalties()
- .filter(function(pen) {
- return pen.standing_penalty().staff_alert() == 't'
- });
-
- service.summary_stat_cats = [];
- angular.forEach(service.current.stat_cat_entries(),
- function(map) {
- if (angular.isObject(map.stat_cat()) &&
- map.stat_cat().usr_summary() == 't') {
- service.summary_stat_cats.push(map);
- }
- }
- );
-
- // run these two in parallel
- var p1 = service.getUserNonCats(service.current.id());
- var p2 = service.fetchGroupFines();
- return $q.all([p1, p2]);
- });
- }
-
- // Avoid using parens [e.g. (1.23)] to indicate negative numbers,
- // which is the Angular default.
- // http://stackoverflow.com/questions/17441254/why-angularjs-currency-filter-formats-negative-numbers-with-parenthesis
- // FIXME: This change needs to be moved into a project-wide collection
- // of locale overrides.
- $locale.NUMBER_FORMATS.PATTERNS[1].negPre = '-';
- $locale.NUMBER_FORMATS.PATTERNS[1].negSuf = '';
-
- return service;
-}])
-
-/**
* Manages tabbed patron view.
* This is the parent scope of all patron tab scopes.
*
.controller('PatronSearchCtrl',
['$scope','$q','$routeParams','$timeout','$window','$location','egCore',
'$filter','egUser', 'patronSvc','egGridDataProvider','$document',
- 'egPatronMerge','egProgressDialog',
+ 'egPatronMerge','egProgressDialog','$controller',
function($scope, $q, $routeParams, $timeout, $window, $location, egCore,
$filter, egUser, patronSvc , egGridDataProvider , $document,
- egPatronMerge , egProgressDialog) {
+ egPatronMerge , egProgressDialog, $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) {
selectedItems : function() {return []}
}
- // 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) {
},
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_two_selected = function() {
var items = $scope.gridControls.selectedItems();
--- /dev/null
+/**
+ * Patron Search module
+ */
+
+angular.module('egPatronSearchMod', ['ngRoute', 'ui.bootstrap',
+ 'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod'])
+
+/**
+ * Patron service
+ */
+.factory('patronSvc',
+ ['$q','$timeout','$location','egCore','egUser','$locale',
+function($q , $timeout , $location , egCore, egUser , $locale) {
+
+ var service = {
+ // cached patron search results
+ patrons : [],
+
+ // currently selected patron object
+ current : null,
+
+ // patron circ stats (overdues, fines, holds)
+ patron_stats : null,
+
+ // event types manually overridden, which should always be
+ // overridden for checkouts to this patron for this instance of
+ // the interface.
+ checkout_overrides : {},
+ //holds the searched barcode
+ search_barcode : null,
+ };
+
+ // when we change the default patron, we need to clear out any
+ // data collected on that patron
+ service.resetPatronLists = function() {
+ service.checkouts = [];
+ service.items_out = []
+ service.items_out_ids = [];
+ service.holds = [];
+ service.hold_ids = [];
+ service.checkout_overrides = {};
+ service.patron_stats = null;
+ service.noncat_ids = [];
+ service.hasAlerts = false;
+ service.patronExpired = false;
+ service.patronExpiresSoon = false;
+ service.invalidAddresses = false;
+ }
+ service.resetPatronLists(); // initialize
+
+ // Returns true if the last alerted patron matches the current
+ // patron. Otherwise, the last alerted patron is set to the
+ // current patron and false is returned.
+ service.alertsShown = function() {
+ var key = 'eg.circ.last_alerted_patron';
+ var last_id = egCore.hatch.getSessionItem(key);
+ if (last_id && last_id == service.current.id()) return true;
+ egCore.hatch.setSessionItem(key, service.current.id());
+ return false;
+ }
+
+ // shortcut to force-reload the current primary
+ service.refreshPrimary = function() {
+ if (!service.current) return $q.when();
+ return service.setPrimary(service.current.id(), null, true);
+ }
+
+ // clear the currently focused user
+ service.clearPrimary = function() {
+ // reset with no patron
+ service.resetPatronLists();
+ service.current = null;
+ service.patron_stats = null;
+ return $q.when();
+ }
+
+ // sets the primary display user, fetching data as necessary.
+ service.setPrimary = function(id, user, force) {
+ var user_id = id ? id : (user ? user.id() : null);
+
+ console.debug('setting primary user to: ' + user_id);
+
+ if (!user_id) return $q.reject();
+
+ // when loading a new patron, update the last patron setting
+ if (!service.current || service.current.id() != user_id)
+ egCore.hatch.setLoginSessionItem('eg.circ.last_patron', user_id);
+
+ // avoid running multiple retrievals for the same patron, which
+ // can happen during dbl-click by maintaining a single running
+ // data retrieval promise
+ if (service.primaryUserPromise) {
+ if (service.primaryUserId == user_id) {
+ return service.primaryUserPromise.promise;
+ } else {
+ service.primaryUserPromise = null;
+ }
+ }
+
+ service.primaryUserPromise = $q.defer();
+ service.primaryUserId = user_id;
+
+ service.getPrimary(id, user, force)
+ .then(function() {
+ service.checkAlerts();
+ var p = service.primaryUserPromise;
+ service.primaryUserId = null;
+ // clear before resolution just to be safe.
+ service.primaryUserPromise = null;
+ p.resolve();
+ });
+
+ return service.primaryUserPromise.promise;
+ }
+
+ service.getPrimary = function(id, user, force) {
+
+ if (user) {
+ if (!force && service.current &&
+ service.current.id() == user.id()) {
+ if (service.patron_stats) {
+ return $q.when();
+ } else {
+ return service.fetchUserStats();
+ }
+ }
+
+ service.resetPatronLists();
+ service.current = user;
+ service.localFlesh(user);
+ return service.fetchUserStats();
+
+ } else if (id) {
+ if (!force && service.current && service.current.id() == id) {
+ if (service.patron_stats) {
+ return $q.when();
+ } else {
+ return service.fetchUserStats();
+ }
+ }
+
+ service.resetPatronLists();
+
+ return egUser.get(id).then(
+ function(user) {
+ service.current = user;
+ service.localFlesh(user);
+ return service.fetchUserStats();
+ },
+ function(err) {
+ console.error(
+ "unable to fetch user "+id+': '+js2JSON(err))
+ }
+ );
+ } else {
+
+ // fetching a null user clears the primary user.
+ // NOTE: this should probably reject() and log an error,
+ // but calling clear for backwards compat for now.
+ return service.clearPrimary();
+ }
+ }
+
+ // flesh some additional user fields locally
+ service.localFlesh = function(user) {
+ if (!angular.isObject(typeof user.home_ou()))
+ user.home_ou(egCore.org.get(user.home_ou()));
+
+ angular.forEach(
+ user.standing_penalties(),
+ function(penalty) {
+ if (!angular.isObject(penalty.org_unit()))
+ penalty.org_unit(egCore.org.get(penalty.org_unit()));
+ }
+ );
+
+ // stat_cat_entries == stat_cat_entry_user_map
+ angular.forEach(user.stat_cat_entries(), function(map) {
+ if (angular.isObject(map.stat_cat())) return;
+ // At page load, we only retrieve org-visible stat cats.
+ // For the common case, ignore entries for remote stat cats.
+ var cat = egCore.env.actsc.map[map.stat_cat()];
+ if (cat) {
+ map.stat_cat(cat);
+ cat.owner(egCore.org.get(cat.owner()));
+ }
+ });
+ }
+
+ // resolves to true if the patron account has expired or will
+ // expire soon, based on YAOUS circ.patron_expires_soon_warning
+ // note: returning a promise is no longer strictly necessary
+ // (no more async activity) if the calling function is changed too.
+ service.testExpire = function() {
+
+ var expire = Date.parse(service.current.expire_date());
+ if (expire < new Date()) {
+ return $q.when(service.patronExpired = true);
+ }
+
+ var soon = egCore.env.aous['circ.patron_expires_soon_warning'];
+ if (Number(soon)) {
+ var preExpire = new Date();
+ preExpire.setDate(preExpire.getDate() + Number(soon));
+ if (expire < preExpire)
+ return $q.when(service.patronExpiresSoon = true);
+ }
+
+ return $q.when(false);
+ }
+
+ // resolves to true if the patron account has any invalid addresses.
+ service.testInvalidAddrs = function() {
+
+ if (service.invalidAddresses)
+ return $q.when(true);
+
+ var fail = false;
+
+ angular.forEach(
+ service.current.addresses(),
+ function(addr) { if (addr.valid() == 'f') fail = true }
+ );
+
+ return $q.when(fail);
+ }
+ //resolves to true if the patron was fetched with an inactive card
+ service.fetchedWithInactiveCard = function() {
+ var bc = service.search_barcode
+ var cards = service.current.cards();
+ var card = cards.filter(function(c) { return c.barcode() == bc })[0];
+ return (card && card.active() == 'f');
+ }
+ // resolves to true if there is any aspect of the patron account
+ // which should produce a message in the alerts panel
+ service.checkAlerts = function() {
+
+ if (service.hasAlerts) // already checked
+ return $q.when(true);
+
+ var deferred = $q.defer();
+ var p = service.current;
+
+ if (service.alert_penalties.length ||
+ p.alert_message() ||
+ p.active() == 'f' ||
+ p.barred() == 't' ||
+ service.patron_stats.holds.ready) {
+
+ service.hasAlerts = true;
+ }
+
+ // see if the user was retrieved with an inactive card
+ if(service.fetchedWithInactiveCard()){
+ service.hasAlerts = true;
+ }
+
+ // regardless of whether we know of alerts, we still need
+ // to test/fetch the expire data for display
+ service.testExpire().then(function(bool) {
+ if (bool) service.hasAlerts = true;
+ deferred.resolve(service.hasAlerts);
+ });
+
+ service.testInvalidAddrs().then(function(bool) {
+ if (bool) service.invalidAddresses = true;
+ deferred.resolve(service.invalidAddresses);
+ });
+
+ return deferred.promise;
+ }
+
+ service.fetchGroupFines = function() {
+ return egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.usergroup.members.balance_owed',
+ egCore.auth.token(), service.current.usrgroup()
+ ).then(function(list) {
+ var total = 0;
+ angular.forEach(list, function(u) {
+ total += 100 * Number(u.balance_owed)
+ });
+ service.patron_stats.fines.group_balance_owed = total / 100;
+ });
+ }
+
+ service.getUserStats = function(id) {
+ return egCore.net.request(
+ 'open-ils.actor',
+ 'open-ils.actor.user.opac.vital_stats.authoritative',
+ egCore.auth.token(), id
+ ).then(
+ function(stats) {
+ // force numeric to ensure correct boolean handling in templates
+ stats.fines.balance_owed = Number(stats.fines.balance_owed);
+ stats.checkouts.overdue = Number(stats.checkouts.overdue);
+ stats.checkouts.claims_returned =
+ Number(stats.checkouts.claims_returned);
+ stats.checkouts.lost = Number(stats.checkouts.lost);
+ stats.checkouts.out = Number(stats.checkouts.out);
+ stats.checkouts.total_out =
+ stats.checkouts.out + stats.checkouts.overdue;
+
+ stats.checkouts.total_out += Number(stats.checkouts.long_overdue);
+
+ if (!egCore.env.aous['circ.do_not_tally_claims_returned'])
+ stats.checkouts.total_out += stats.checkouts.claims_returned;
+
+ if (egCore.env.aous['circ.tally_lost'])
+ stats.checkouts.total_out += stats.checkouts.lost
+
+ return stats;
+ }
+ );
+ }
+
+ // Fetches the IDs of any active non-cat checkouts for the current
+ // user. Also sets the patron_stats non_cat count value to match.
+ service.getUserNonCats = function(id) {
+ return egCore.net.request(
+ 'open-ils.circ',
+ 'open-ils.circ.open_non_cataloged_circulation.user.authoritative',
+ egCore.auth.token(), id
+ ).then(function(noncat_ids) {
+ service.noncat_ids = noncat_ids;
+ service.patron_stats.checkouts.noncat = noncat_ids.length;
+ });
+ }
+
+ // grab additional circ info
+ service.fetchUserStats = function() {
+ return service.getUserStats(service.current.id())
+ .then(function(stats) {
+ service.patron_stats = stats
+ service.alert_penalties = service.current.standing_penalties()
+ .filter(function(pen) {
+ return pen.standing_penalty().staff_alert() == 't'
+ });
+
+ service.summary_stat_cats = [];
+ angular.forEach(service.current.stat_cat_entries(),
+ function(map) {
+ if (angular.isObject(map.stat_cat()) &&
+ map.stat_cat().usr_summary() == 't') {
+ service.summary_stat_cats.push(map);
+ }
+ }
+ );
+
+ // run these two in parallel
+ var p1 = service.getUserNonCats(service.current.id());
+ var p2 = service.fetchGroupFines();
+ return $q.all([p1, p2]);
+ });
+ }
+
+ // Avoid using parens [e.g. (1.23)] to indicate negative numbers,
+ // which is the Angular default.
+ // http://stackoverflow.com/questions/17441254/why-angularjs-currency-filter-formats-negative-numbers-with-parenthesis
+ // FIXME: This change needs to be moved into a project-wide collection
+ // of locale overrides.
+ $locale.NUMBER_FORMATS.PATTERNS[1].negPre = '-';
+ $locale.NUMBER_FORMATS.PATTERNS[1].negSuf = '';
+
+ return service;
+}])
+
+/**
+ * Manages patron search
+ */
+.controller('BasePatronSearchCtrl',
+ ['$scope','$q','$routeParams','$timeout','$window','$location','egCore',
+ '$filter','egUser', 'patronSvc','egGridDataProvider','$document',
+ 'egProgressDialog',
+function($scope, $q, $routeParams, $timeout, $window, $location, egCore,
+ $filter, egUser, patronSvc , egGridDataProvider , $document,
+ egProgressDialog) {
+
+ $scope.focusMe = true;
+ $scope.searchArgs = {
+ // default to searching globally
+ home_ou : egCore.org.tree()
+ };
+
+ // last used patron search form element
+ var lastFormElement;
+
+ $scope.gridControls = {
+ selectedItems : function() {return []}
+ }
+
+ // 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({});
+
+ 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_two_selected = function() {
+ var items = $scope.gridControls.selectedItems();
+ return (items.length == 2) ? false : true;
+ }
+
+}])
+