From fc2cd9bfeb9a1ef73cda5402c1332899daec85f7 Mon Sep 17 00:00:00 2001 From: blake Date: Wed, 9 Aug 2017 14:28:41 +0000 Subject: [PATCH] LP1655158 Patron Search by Date of Birth Adds three UI boxes to the WBSC "Show Extra" patron search. One for the year, month and day. The javascript on the page is altered to deliver group "4" to the backend. Local javascript strips out non-numeric user entered data. The backend is updated to handle the new group. SQL is genereated using the DATE_PART postgres function. 1. Open the web based staff client and browse to the patron search UI. 2. Click the show more down arrow button. Notice the lack of birth date field. 3. Apply the patch, repeat step one. Notice the addition of birth date boxes. 4. Type 1975 into the birth year box and press enter. Notice search results. 5. Try searching for partial names and partial birthdates. 6. Try entering non-numeric data into the birth date boxes. 7. Try searching for patrons without including the dob. Try with only the dob. Try a mix. Signed-off-by: blake --- .../OpenILS/Application/Storage/Publisher/actor.pm | 16 +- .../src/templates/staff/circ/patron/t_search.tt2 | 164 ++++++++++++++- .../web/js/ui/default/staff/circ/patron/app.js | 223 ++++++++++++++++++++- 3 files changed, 386 insertions(+), 17 deletions(-) diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/actor.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/actor.pm index a1e634ddab..9631f889ab 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/actor.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/Publisher/actor.pm @@ -698,21 +698,11 @@ sub patron_search { @usrv = map { "^" . _clean_regex_chars($$search{$_}{value}) } grep { ''.$$search{$_}{group} eq '0' } keys %$search; } - while (($key, $value) = each (%$search)) { - if($$search{$key}{group} eq '4') { - my $tval = $key; - $tval =~ s/dob_//g; - my $right = "RIGHT('0'|| "; - my $end = ", 2)"; - $end = $right = '' if lc $tval eq 'year'; - $dob .= $right."CAST(DATE_PART('$tval', dob) AS text)$end ~ ? AND "; - } - } - # Trim the last " AND " - $dob = substr($dob,0,-4); + $dob = join ' AND ', map { ("CAST (DATE_PART('" . ( s/year//g ? 'year' : ( s/month//g ? 'month' : 'day' ) ) . "', dob) AS text) ~ ?") } grep { ''.$$search{$_}{group} eq '4' } keys %$search; @dobv = map { _clean_regex_chars($$search{$_}{value}) } grep { ''.$$search{$_}{group} eq '4' } keys %$search; + $usr .= ' AND ' if ( $usr && $dob ); - $usr .= $dob if $dob; # $dob not in-line above in case $usr doesn't have any search vals (only searched for dob) + $usr .= $dob if $dob; push(@usrv, @dobv) if @dobv; my $addr = join ' AND ', map { "evergreen.lowercase(CAST($_ AS text)) ~ ?" } grep { ''.$$search{$_}{group} eq '1' } keys %$search; diff --git a/Open-ILS/src/templates/staff/circ/patron/t_search.tt2 b/Open-ILS/src/templates/staff/circ/patron/t_search.tt2 index c67291a7d9..16e9189b1f 100644 --- a/Open-ILS/src/templates/staff/circ/patron/t_search.tt2 +++ b/Open-ILS/src/templates/staff/circ/patron/t_search.tt2 @@ -1,4 +1,166 @@ -[% INCLUDE 'staff/share/t_patron_search_form.tt2' %] + + +
+
+
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+ +
+ +
+ + +
+ + +
+
+ +
+ + +
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+

diff --git a/Open-ILS/web/js/ui/default/staff/circ/patron/app.js b/Open-ILS/web/js/ui/default/staff/circ/patron/app.js index e7d6430453..219aad75e1 100644 --- a/Open-ILS/web/js/ui/default/staff/circ/patron/app.js +++ b/Open-ILS/web/js/ui/default/staff/circ/patron/app.js @@ -611,9 +611,226 @@ function($scope, $q, $routeParams, $timeout, $window, $location, egCore , true ); - $scope.need_one_selected = function() { - var items = $scope.gridControls.selectedItems(); - return (items.length > 0) ? false : true; + } 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 + } else if (key.match(/dob_/)) { + // DOB should always be numeric + search[key].value = search[key].value.replace(/\D/g,''); + if (search[key].value.length == 0) { + delete search[key]; + } + else { + search[key].group = 4; + } + } + } + }); + + 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(); -- 2.11.0