]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/services/patron_search.js
LP 2061136 follow-up: ng lint --fix
[Evergreen.git] / Open-ILS / web / js / ui / default / staff / services / patron_search.js
1 /**
2  * Patron Search module
3  */
4
5 angular.module('egPatronSearchMod', ['ngRoute', 'ui.bootstrap', 
6     'egCoreMod', 'egUiMod', 'egGridMod', 'egUserMod'])
7
8 /**
9  * Patron service
10  */
11 .factory('patronSvc',
12        ['$q','$timeout','$location','egCore','egUser','egConfirmDialog','$locale',
13 function($q , $timeout , $location , egCore,  egUser , egConfirmDialog , $locale) {
14
15     var service = {
16         // cached patron search results
17         patrons : [],
18
19         // currently selected patron object
20         current : null, 
21
22         // patron circ stats (overdues, fines, holds)
23         patron_stats : null,
24
25         // event types manually overridden, which should always be
26         // overridden for checkouts to this patron for this instance of
27         // the interface.
28         checkout_overrides : {},        
29         //holds the searched barcode
30         search_barcode : null,      
31     };
32
33     // when we change the default patron, we need to clear out any
34     // data collected on that patron
35     service.resetPatronLists = function() {
36         service.checkouts = [];
37         service.items_out = []
38         service.items_out_ids = [];
39         service.holds = [];
40         service.hold_ids = [];
41         service.checkout_overrides = {};
42         service.patron_stats = null;
43         service.noncat_ids = [];
44         service.hasAlerts = false;
45         service.patronExpired = false;
46         service.patronExpiresSoon = false;
47         service.invalidAddresses = false;
48     }
49     service.resetPatronLists();  // initialize
50
51     egCore.startup.go().then(
52         function() {
53             // Max recents setting is loaded and scrubbed during egStartup.
54             // Copy it to a local variable here for ease of local access
55             // after startup has run.
56             egCore.org.settings('ui.staff.max_recent_patrons')
57             .then(function(s) {
58                 service.maxRecentPatrons = s['ui.staff.max_recent_patrons'];
59             });
60
61             // This call requires orgs to be loaded, because it
62             // calls egCore.org.ancestors(), so call it after startup
63             egCore.pcrud.search('actsc',
64                 {owner : egCore.org.ancestors(
65                     egCore.auth.user().ws_ou(), true)},
66                 {}, {atomic : true}
67             ).then(function(cats) {
68                 egCore.env.absorbList(cats, 'actsc');
69             });
70         }
71     );
72
73     // Returns true if the last alerted patron matches the current
74     // patron.  Otherwise, the last alerted patron is set to the 
75     // current patron and false is returned.
76     service.alertsShown = function() {
77         var key = 'eg.circ.last_alerted_patron';
78         var last_id = egCore.hatch.getSessionItem(key);
79         if (last_id && last_id == service.current.id()) return true;
80         egCore.hatch.setSessionItem(key, service.current.id());
81         return false;
82     }
83
84     // shortcut to force-reload the current primary
85     service.refreshPrimary = function() {
86         if (!service.current) return $q.when();
87         return service.setPrimary(service.current.id(), null, true);
88     }
89
90     // clear the currently focused user
91     service.clearPrimary = function() {
92         // reset with no patron
93         service.resetPatronLists();
94         service.current = null;
95         service.patron_stats = null;
96         return $q.when();
97     }
98
99     service.getRecentPatrons = function() {
100         // avoid getting stuck in a show-recent loop
101         service.showRecent = false;
102
103         if (service.maxRecentPatrons < 1) return $q.when();
104         var patrons = 
105             egCore.hatch.getLoginSessionItem('eg.circ.recent_patrons') || [];
106
107         // Ensure the cached list is no bigger than the current config.
108         // This can happen if the setting changes while logged in.
109         patrons = patrons.slice(0, service.maxRecentPatrons);
110
111         // add home_ou to the list of fleshed fields for recent patrons
112         var fleshFields = egUser.defaultFleshFields.slice(0);
113         fleshFields.push('home_ou');
114
115         var deferred = $q.defer();
116         function getNext() {
117             if (patrons.length == 0) {
118                 deferred.resolve();
119                 return;
120             }
121             egUser.get(patrons[0], {useFields : fleshFields}).then(
122                 function(usr) { // fetch first user
123                     deferred.notify(usr);
124                     patrons.splice(0, 1); // remove first user from list
125                     getNext();
126                 }
127             );
128         }
129
130         getNext();
131         return deferred.promise;
132     }
133
134     service.addRecentPatron = function(user_id) {
135         if (service.maxRecentPatrons < 1) return;
136
137         // ensure ID is a number if pulled from route data
138         user_id = Number(user_id);
139
140         // no need to re-track same user
141         if (service.current && service.current.id() == user_id) return;
142
143         var patrons = 
144             egCore.hatch.getLoginSessionItem('eg.circ.recent_patrons') || [];
145
146         // remove potential existing duplicates
147         patrons = patrons.filter(function(id) {
148             return user_id !== id
149         });
150         patrons.splice(0, 0, user_id);  // put this user at front
151         patrons.splice(service.maxRecentPatrons); // remove excess
152
153         egCore.hatch.setLoginSessionItem('eg.circ.recent_patrons', patrons);
154     }
155
156     // sets the primary display user, fetching data as necessary.
157     service.setPrimary = function(id, user, force) {
158         var user_id = id ? id : (user ? user.id() : null);
159
160         console.debug('setting primary user to: ' + user_id);
161
162         if (!user_id) return $q.reject();
163
164         service.addRecentPatron(user_id);
165
166         // avoid running multiple retrievals for the same patron, which
167         // can happen during dbl-click by maintaining a single running
168         // data retrieval promise
169         if (service.primaryUserPromise) {
170             if (service.primaryUserId == user_id) {
171                 return service.primaryUserPromise.promise;
172             } else {
173                 service.primaryUserPromise = null;
174             }
175         }
176
177         service.primaryUserPromise = $q.defer();
178         service.primaryUserId = user_id;
179
180         service.getPrimary(id, user, force)
181         .then(function() {
182             service.checkAlerts();
183             var p = service.primaryUserPromise;
184             service.primaryUserId = null;
185             // clear before resolution just to be safe.
186             service.primaryUserPromise = null;
187             p.resolve();
188         });
189
190         return service.primaryUserPromise.promise;
191     }
192
193     service.getPrimary = function(id, user, force) {
194
195         if (user) {
196             if (!force && service.current && 
197                 service.current.id() == user.id()) {
198                 if (service.patron_stats) {
199                     return $q.when();
200                 } else {
201                     return service.fetchUserStats();
202                 }
203             }
204
205             service.resetPatronLists();
206
207             return service.checkOptIn(user).then(
208                 function() {
209                     service.current = user;
210                     service.localFlesh(user);
211                     return service.fetchUserStats();
212                 },
213                 function() {
214                     return $q.reject();
215                 }
216             );
217
218         } else if (id) {
219             if (!force && service.current && service.current.id() == id) {
220                 if (service.patron_stats) {
221                     return $q.when();
222                 } else {
223                     return service.fetchUserStats();
224                 }
225             }
226
227             service.resetPatronLists();
228
229             return egUser.get(id).then(
230                 function(user) {
231                     return service.checkOptIn(user).then(
232                         function() {
233                             service.current = user;
234                             service.localFlesh(user);
235                             return service.fetchUserStats();
236                         },
237                         function() {
238                             return $q.reject();
239                         }
240                     );
241                 },
242                 function(err) {
243                     console.error(
244                         "unable to fetch user "+id+': '+js2JSON(err))
245                 }
246             );
247         } else {
248
249             // fetching a null user clears the primary user.
250             // NOTE: this should probably reject() and log an error, 
251             // but calling clear for backwards compat for now.
252             return service.clearPrimary();
253         }
254     }
255
256     // flesh some additional user fields locally
257     service.localFlesh = function(user) {
258         if (!angular.isObject(typeof user.home_ou()))
259             user.home_ou(egCore.org.get(user.home_ou()));
260
261         angular.forEach(
262             user.standing_penalties(),
263             function(penalty) {
264                 if (!angular.isObject(penalty.org_unit()))
265                     // clone the org unit IDL object and set children to an empty array
266                     org_unit = egCore.idl.Clone(egCore.org.get(penalty.org_unit()));
267                     org_unit.children([])
268                     penalty.org_unit(org_unit);
269             }
270         );
271
272         // stat_cat_entries == stat_cat_entry_user_map
273         angular.forEach(user.stat_cat_entries(), function(map) {
274             if (angular.isObject(map.stat_cat())) return;
275             // At page load, we only retrieve org-visible stat cats.
276             // For the common case, ignore entries for remote stat cats.
277             var cat = egCore.env.actsc.map[map.stat_cat()];
278             if (cat) {
279                 map.stat_cat(cat);
280                 cat.owner(egCore.org.get(cat.owner()));
281             }
282         });
283     }
284
285     // resolves to true if the patron account has expired or will
286     // expire soon, based on YAOUS circ.patron_expires_soon_warning
287     // note: returning a promise is no longer strictly necessary
288     // (no more async activity) if the calling function is changed too.
289     service.testExpire = function() {
290
291         var expire = Date.parse(service.current.expire_date());
292         if (expire < new Date()) {
293             return $q.when(service.patronExpired = true);
294         }
295
296         var soon = egCore.env.aous['circ.patron_expires_soon_warning'];
297         if (Number(soon)) {
298             var preExpire = new Date();
299             preExpire.setDate(preExpire.getDate() + Number(soon));
300             if (expire < preExpire) 
301                 return $q.when(service.patronExpiresSoon = true);
302         }
303
304         return $q.when(false);
305     }
306
307     // resolves to true if the patron account has any invalid addresses.
308     service.testInvalidAddrs = function() {
309
310         if (service.invalidAddresses)
311             return $q.when(true);
312
313         var fail = false;
314
315         angular.forEach(
316             service.current.addresses(), 
317             function(addr) { if (addr.valid() == 'f') fail = true }
318         );
319
320         return $q.when(fail);
321     }
322     //resolves to true if the patron was fetched with an inactive card
323     service.fetchedWithInactiveCard = function() {
324         var bc = service.search_barcode
325         var cards = service.current.cards();
326         var card = cards.filter(function(c) { return c.barcode() == bc })[0];
327         return (card && card.active() == 'f');
328     }   
329     // resolves to true if there is any aspect of the patron account
330     // which should produce a message in the alerts panel
331     service.checkAlerts = function() {
332
333         if (service.hasAlerts) // already checked
334             return $q.when(true); 
335
336         var deferred = $q.defer();
337         var p = service.current;
338
339         if (service.alert_penalties.length ||
340             p.active() == 'f' ||
341             p.barred() == 't' ||
342             service.patron_stats.holds.ready) {
343
344             service.hasAlerts = true;
345         }
346
347         // see if the user was retrieved with an inactive card
348         if(service.fetchedWithInactiveCard()){
349             service.hasAlerts = true;
350         }
351
352         // regardless of whether we know of alerts, we still need 
353         // to test/fetch the expire data for display
354         service.testExpire().then(function(bool) {
355             if (bool) service.hasAlerts = true;
356             deferred.resolve(service.hasAlerts);
357         });
358
359         service.testInvalidAddrs().then(function(bool) {
360             if (bool) service.invalidAddresses = true;
361             deferred.resolve(service.invalidAddresses);
362         });
363
364         return deferred.promise;
365     }
366
367     service.fetchGroupFines = function() {
368         return egCore.net.request(
369             'open-ils.actor',
370             'open-ils.actor.usergroup.members.balance_owed',
371             egCore.auth.token(), service.current.usrgroup()
372         ).then(function(list) {
373             var total = 0;
374             angular.forEach(list, function(u) { 
375                 total += 100 * Number(u.balance_owed)
376             });
377             service.patron_stats.fines.group_balance_owed = total / 100;
378         });
379     }
380
381     service.getUserStats = function(id) {
382         return egCore.net.request(
383             'open-ils.actor',
384             'open-ils.actor.user.opac.vital_stats.authoritative', 
385             egCore.auth.token(), id
386         ).then(
387             function(stats) {
388                 // force numeric to ensure correct boolean handling in templates
389                 stats.fines.balance_owed = Number(stats.fines.balance_owed);
390                 stats.checkouts.overdue = Number(stats.checkouts.overdue);
391                 stats.checkouts.claims_returned = 
392                     Number(stats.checkouts.claims_returned);
393                 stats.checkouts.lost = Number(stats.checkouts.lost);
394                 stats.checkouts.out = Number(stats.checkouts.out);
395                 stats.checkouts.total_out = 
396                     stats.checkouts.out + stats.checkouts.overdue;
397
398                 stats.checkouts.total_out += Number(stats.checkouts.long_overdue);
399
400                 if (!egCore.env.aous['circ.do_not_tally_claims_returned'])
401                     stats.checkouts.total_out += stats.checkouts.claims_returned;
402
403                 if (egCore.env.aous['circ.tally_lost'])
404                     stats.checkouts.total_out += stats.checkouts.lost
405
406                 return stats;
407             }
408         );
409     }
410
411     // Fetches the IDs of any active non-cat checkouts for the current
412     // user.  Also sets the patron_stats non_cat count value to match.
413     service.getUserNonCats = function(id) {
414         return egCore.net.request(
415             'open-ils.circ',
416             'open-ils.circ.open_non_cataloged_circulation.user.authoritative',
417             egCore.auth.token(), id
418         ).then(function(noncat_ids) {
419             service.noncat_ids = noncat_ids;
420             service.patron_stats.checkouts.noncat = noncat_ids.length;
421         });
422     }
423
424     // grab additional circ info
425     service.fetchUserStats = function() {
426         return service.getUserStats(service.current.id())
427         .then(function(stats) {
428             service.patron_stats = stats
429             service.alert_penalties = service.current.standing_penalties()
430                 .filter(function(pen) { 
431                 return pen.standing_penalty().staff_alert() == 't' 
432             });
433
434             service.summary_stat_cats = [];
435             angular.forEach(service.current.stat_cat_entries(), 
436                 function(map) {
437                     if (angular.isObject(map.stat_cat()) &&
438                         map.stat_cat().usr_summary() == 't') {
439                         service.summary_stat_cats.push(map);
440                     }
441                 }
442             );
443
444             // run these two in parallel
445             var p1 = service.getUserNonCats(service.current.id());
446             var p2 = service.fetchGroupFines();
447             return $q.all([p1, p2]);
448         });
449     }
450
451     service.createOptIn = function(user_id) {
452         return egCore.net.request(
453             'open-ils.actor',
454             'open-ils.actor.user.org_unit_opt_in.create',
455             egCore.auth.token(), user_id);
456     }
457
458     service.checkOptIn = function(user) {
459         var deferred = $q.defer();
460         egCore.net.request(
461             'open-ils.actor',
462             'open-ils.actor.user.org_unit_opt_in.check',
463             egCore.auth.token(), user.id())
464         .then(function(optInResp) {
465             if (eg_evt = egCore.evt.parse(optInResp)) {
466                 deferred.reject();
467                 console.log('error on opt-in check: ' + eg_evt);
468             } else if (optInResp == 2) {
469                 // opt-in disallowed at this location by patron's home library
470                 deferred.reject();
471                 alert(egCore.strings.OPT_IN_RESTRICTED);
472             } else if (optInResp == 1) {
473                 // opt-in handled or not needed, do nothing
474                 deferred.resolve();
475             } else {
476                 // opt-in needed, show the opt-in dialog
477                 var org = egCore.org.get(user.home_ou());
478                 egConfirmDialog.open(
479                     egCore.strings.OPT_IN_DIALOG_TITLE,
480                     egCore.strings.OPT_IN_DIALOG,
481                     {   family_name : user.family_name(),
482                         first_given_name : user.first_given_name(),
483                         org_name : org.name(),
484                         org_shortname : org.shortname(),
485                         ok : function() {
486                             service.createOptIn(user.id())
487                             .then(function(resp) {
488                                 if (evt = egCore.evt.parse(resp)) {
489                                     deferred.reject();
490                                     alert(evt);
491                                 } else {
492                                     deferred.resolve();
493                                 }
494                             });
495                         },
496                         cancel : function() { deferred.reject(); }
497                     }
498                 );
499             }
500         });
501         return deferred.promise;
502     }
503
504     // Avoid using parens [e.g. (1.23)] to indicate negative numbers, 
505     // which is the Angular default.
506     // http://stackoverflow.com/questions/17441254/why-angularjs-currency-filter-formats-negative-numbers-with-parenthesis
507     // FIXME: This change needs to be moved into a project-wide collection
508     // of locale overrides.
509     $locale.NUMBER_FORMATS.PATTERNS[1].negPre = '-';
510     $locale.NUMBER_FORMATS.PATTERNS[1].negSuf = '';
511
512     return service;
513 }])
514
515 /**
516  * Manages patron search
517  */
518 .controller('BasePatronSearchCtrl',
519        ['$scope','$q','$routeParams','$timeout','$window','$location','egCore',
520        '$filter','egUser', 'patronSvc','egGridDataProvider','$document',
521        'egProgressDialog',
522 function($scope,  $q,  $routeParams,  $timeout,  $window,  $location,  egCore,
523         $filter,  egUser,  patronSvc , egGridDataProvider , $document,
524         egProgressDialog) {
525
526     $scope.focusMe = true;
527     $scope.searchArgs = {
528         // default to searching globally
529         home_ou : egCore.org.tree()
530     };
531
532     // last used patron search form element
533     var lastFormElement;
534
535     $scope.gridControls = {
536         selectedItems : function() {return []}
537     }
538
539     // The first time we encounter the show-recent CGI param, put the
540     // service into show-recent mode.  The first time recents are shown,
541     // the service is taken out of show-recent mode so the page does not
542     // get stuck in a show-recent loop.
543     if (patronSvc.showRecent === undefined 
544         && Boolean($location.path().match(/search/))
545         && Boolean($location.search().show_recent)) {
546         patronSvc.showRecent = true;
547     }
548
549     // Handle URL-encoded searches
550     if ($location.search().search) {
551         console.log('URL search = ' + $location.search().search);
552         patronSvc.urlSearch = {search : JSON2js($location.search().search)};
553
554         // why the double-JSON encoded sort?
555         if (patronSvc.urlSearch.search.search_sort) {
556             patronSvc.urlSearch.sort = 
557                 JSON2js(patronSvc.urlSearch.search.search_sort);
558         } else {
559             patronSvc.urlSearch.sort = [];
560         }
561         delete patronSvc.urlSearch.search.search_sort;
562
563         // include inactive patrons if "inactive" param
564         if ($location.search().inactive) {
565             patronSvc.urlSearch.inactive = $location.search().inactive;
566         }
567     }
568
569     var propagate;
570     var propagate_inactive;
571     if (patronSvc.lastSearch && !patronSvc.showRecent) {
572         propagate = patronSvc.lastSearch.search;
573         // home_ou needs to be treated specially
574         propagate.home_ou = {
575             value : patronSvc.lastSearch.home_ou,
576             group : 0
577         };
578     } else if (patronSvc.urlSearch) {
579         propagate = patronSvc.urlSearch.search;
580         if (patronSvc.urlSearch.inactive) {
581             propagate_inactive = patronSvc.urlSearch.inactive;
582         }
583     }
584
585     if (egCore.env.pgt) {
586         $scope.profiles = egCore.env.pgt.list;
587     } else {
588         egCore.pcrud.search('pgt', {parent : null}, 
589             {flesh : -1, flesh_fields : {pgt : ['children']}}
590         ).then(
591             function(tree) {
592                 egCore.env.absorbTree(tree, 'pgt')
593                 $scope.profiles = egCore.env.pgt.list;
594             }
595         );
596     }
597
598     if (propagate) {
599         // populate the search form with our cached / preexisting search info
600         angular.forEach(propagate, function(val, key) {
601             if (key == 'profile')
602                 val.value = $scope.profiles.filter(function(p) { return p.id() == val.value })[0];
603             if (key == 'home_ou')
604                 val.value = egCore.org.get(val.value);
605             $scope.searchArgs[key] = val.value;
606         });
607         if (propagate_inactive) {
608             $scope.searchArgs.inactive = propagate_inactive;
609         }
610     }
611
612     var provider = egGridDataProvider.instance({});
613
614     provider.get = function(offset, count) {
615         var deferred = $q.defer();
616
617         if (patronSvc.showRecent) {
618             // avoid getting stuck in show-recent mode
619             return patronSvc.getRecentPatrons();
620         }
621
622         var fullSearch;
623         if (patronSvc.urlSearch) {
624             fullSearch = patronSvc.urlSearch;
625             // enusre the urlSearch only runs once.
626             delete patronSvc.urlSearch;
627
628         } else {
629             patronSvc.search_barcode = $scope.searchArgs.card;
630             
631             var search = compileSearch($scope.searchArgs);
632             if (Object.keys(search) == 0) return $q.when();
633
634             var home_ou = search.home_ou;
635             delete search.home_ou;
636             var inactive = search.inactive;
637             delete search.inactive;
638
639             fullSearch = {
640                 search : search,
641                 sort : compileSort(),
642                 inactive : inactive,
643                 home_ou : home_ou,
644             };
645         }
646
647         fullSearch.count = count;
648         fullSearch.offset = offset;
649
650         if (patronSvc.lastSearch) {
651             // search repeated, return the cached results
652             if (angular.equals(fullSearch, patronSvc.lastSearch)) {
653                 console.log('patron search returning ' + 
654                     patronSvc.patrons.length + ' cached results');
655                 
656                 // notify has to happen after returning the promise
657                 $timeout(
658                     function() {
659                         angular.forEach(patronSvc.patrons, function(user) {
660                             deferred.notify(user);
661                         });
662                         deferred.resolve();
663                     }
664                 );
665                 return deferred.promise;
666             }
667         }
668
669         patronSvc.lastSearch = fullSearch;
670
671         if (fullSearch.search.id) {
672             // search by user id performs a direct ID lookup
673             var userId = fullSearch.search.id.value;
674             $timeout(
675                 function() {
676                     egUser.get(userId).then(function(user) {
677                         patronSvc.localFlesh(user);
678                         patronSvc.patrons = [user];
679                         deferred.notify(user);
680                         deferred.resolve();
681                     });
682                 }
683             );
684             return deferred.promise;
685         }
686
687         if (!Object.keys(fullSearch.search).length) {
688             // Empty searches are rejected by the server.  Avoid 
689             // running the the empty search that runs on page load. 
690             return $q.when();
691         }
692
693         var fleshFields = egUser.defaultFleshFields.slice(0);
694         if (fleshFields.indexOf('profile') == -1)
695             fleshFields.push('profile');
696
697         egProgressDialog.open(); // Indeterminate
698
699         patronSvc.patrons = [];
700         var which_sound = 'success';
701         egCore.net.request(
702             'open-ils.actor',
703             'open-ils.actor.patron.search.advanced.fleshed',
704             egCore.auth.token(), 
705             fullSearch.search, 
706             fullSearch.count,
707             fullSearch.sort,
708             fullSearch.inactive,
709             fullSearch.home_ou,
710             fleshFields,
711             fullSearch.offset
712
713         ).then(
714             function() {
715                 deferred.resolve();
716             },
717             function() { // onerror
718                 which_sound = 'error';
719             },
720             function(user) {
721                 // hide progress bar as soon as the first result appears.
722                 egProgressDialog.close();
723                 patronSvc.localFlesh(user); // inline
724                 patronSvc.patrons.push(user);
725                 deferred.notify(user);
726             }
727         )['finally'](function() { // close on 0-hits or error
728             if (which_sound == 'success' && patronSvc.patrons.length == 0) {
729                 which_sound = 'warning';
730             }
731             egCore.audio.play(which_sound + '.patron.by_search');
732             egProgressDialog.close();
733         });
734
735         return deferred.promise;
736     };
737
738     $scope.patronSearchGridProvider = provider;
739
740     // determine the tree depth of the profile group
741     $scope.pgt_depth = function(grp) {
742         var d = 0;
743         while (grp = egCore.env.pgt.map[grp.parent()]) d++;
744         return d;
745     }
746
747     $scope.clearForm = function () {
748         $scope.searchArgs={};
749         if (lastFormElement) lastFormElement.focus();
750     }
751
752     $scope.applyShowExtras = function($event, bool) {
753         if (bool) {
754             $scope.showExtras = true;
755             egCore.hatch.setItem('eg.circ.patron.search.show_extras', true);
756         } else {
757             $scope.showExtras = false;
758             egCore.hatch.removeItem('eg.circ.patron.search.show_extras');
759         }
760         if (lastFormElement) lastFormElement.focus();
761         $event.preventDefault();
762     }
763
764     egCore.hatch.getItem('eg.circ.patron.search.show_extras')
765     .then(function(val) {$scope.showExtras = val});
766
767     // check searchArgs.inactive setting
768     egCore.hatch.getItem('eg.circ.patron.search.include_inactive')
769                 .then(function(searchInactive){
770                     if (searchInactive) $scope.searchArgs.inactive = searchInactive;
771                 });
772
773      $scope.onSearchInactiveChanged = function() {
774         egCore.hatch.setItem('eg.circ.patron.search.include_inactive', $scope.searchArgs.inactive);
775     }
776
777     // map form arguments into search params
778     function compileSearch(args) {
779         var search = {};
780         angular.forEach(args, function(val, key) {
781             if (!val) return;
782             if (key == 'profile' && args.profile) {
783                 search.profile = {value : args.profile.id(), group : 5};
784             } else if (key == 'home_ou' && args.home_ou) {
785                 search.home_ou = args.home_ou.id(); // passed separately
786             } else if (key == 'inactive') {
787                 search.inactive = val;
788             } else if (key == 'name') { // name keywords search
789                 search.name = {value: val};
790             } else {
791                 search[key] = {value : val, group : 0};
792             }
793             if (key.match(/phone|ident/)) {
794                 search[key].group = 2;
795             } else {
796                 if (key.match(/street|city|state|post_code/)) {
797                     search[key].group = 1;
798                 } else if (key == 'card') {
799                     search[key].group = 3
800                 } else if (key.match(/dob_/)) {
801                     // DOB should always be numeric
802                     search[key].value = search[key].value.replace(/\D/g,'');
803                     if (search[key].value.length == 0) {
804                         delete search[key];
805                     }
806                     else {
807                         if (!key.match(/year/)) {
808                             search[key].value = ('0'+search[key].value).slice(-2);
809                         }
810                         search[key].group = 4;
811                     }
812                 }
813             }
814         });
815
816         return search;
817     }
818
819     function compileSort() {
820
821         if (!provider.sort.length) {
822             return [ // default
823                 "family_name ASC",
824                 "first_given_name ASC",
825                 "second_given_name ASC",
826                 "dob DESC"
827             ];
828         }
829
830         var sort = [];
831         angular.forEach(
832             provider.sort,
833             function(sortdef) {
834                 if (angular.isObject(sortdef)) {
835                     var name = Object.keys(sortdef)[0];
836                     var dir = sortdef[name];
837                     sort.push(name + ' ' + dir);
838                 } else {
839                     sort.push(sortdef);
840                 }
841             }
842         );
843
844         return sort;
845     }
846
847     $scope.setLastFormElement = function() {
848         lastFormElement = $document[0].activeElement;
849     }
850
851     // search form submit action; tells the results grid to
852     // refresh itself.
853     $scope.search = function(args) { // args === $scope.searchArgs
854         if (args && Object.keys(args).length) 
855             $scope.gridControls.refresh();
856         if (lastFormElement) lastFormElement.focus();
857     }
858
859     // TODO: move this into the (forthcoming) grid row activate action
860     $scope.onPatronDblClick = function($event, user) {
861         $location.path('/circ/patron/' + user.id() + '/checkout');
862     }
863
864     if (patronSvc.urlSearch) {
865         // force the grid to load the url-based search on page load
866         provider.refresh();
867     }
868
869     $scope.need_two_selected = function() {
870         var items = $scope.gridControls.selectedItems();
871         return (items.length == 2) ? false : true;
872     }
873    
874 }])
875