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