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