]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js
LP#1452950 Support free-text stat cats
[Evergreen.git] / Open-ILS / web / js / ui / default / staff / circ / patron / regctl.js
1
2 angular.module('egCoreMod')
3 // toss tihs onto egCoreMod since the page app may vary
4
5 .factory('patronRegSvc', ['$q', 'egCore', function($q, egCore) {
6
7     var service = {
8         field_doc : {},            // config.idl_field_doc
9         profiles : [],             // permission groups
10         edit_profiles : [],        // perm groups we can modify
11         sms_carriers : [],
12         user_settings : {},        // applied user settings
13         user_setting_types : {},   // config.usr_setting_type
14         opt_in_setting_types : {}, // config.usr_setting_type for event-def opt-in
15         surveys : [],
16         survey_questions : {},
17         survey_answers : {},
18         survey_responses : {},     // survey.responses for loaded patron in progress
19         stat_cats : [],
20         stat_cat_entry_maps : {},   // cat.id to selected value
21         virt_id : -1,               // virtual ID for new objects
22         init_done : false           // have we loaded our initialization data?
23     };
24
25     // launch a series of parallel data retrieval calls
26     service.init = function(scope) {
27
28         // Data loaded here only needs to be retrieved the first time this
29         // tab becomes active within the current instance of the patron app.
30         // In other words, navigating between patron tabs will not cause
31         // all of this data to be reloaded.  Navigating to a separate app
32         // and returning will cause the data to be reloaded.
33         if (service.init_done) return $q.when();
34         service.init_done = true;
35
36         return $q.all([
37             service.get_field_doc(),
38             service.get_perm_groups(),
39             service.get_ident_types(),
40             service.get_user_settings(),
41             service.get_org_settings(),
42             service.get_stat_cats(),
43             service.get_surveys(),
44             service.get_net_access_levels()
45         ]);
46     };
47
48     //service.check_grp_app_perm = function(grp_id) {
49
50     // determine which user groups our user is not allowed to modify
51     service.set_edit_profiles = function() {
52         var all_app_perms = [];
53         var failed_perms = [];
54
55         // extract the application permissions
56         angular.forEach(service.profiles, function(grp) {
57             if (grp.application_perm())
58                 all_app_perms.push(grp.application_perm());
59         }); 
60
61         // fill in service.edit_profiles by inspecting failed_perms
62         function traverse_grp_tree(grp, failed) {
63             failed = failed || 
64                 failed_perms.indexOf(grp.application_perm()) > -1;
65
66             if (!failed) service.edit_profiles.push(grp);
67
68             angular.forEach(
69                 service.profiles.filter( // children of grp
70                     function(p) { return p.parent() == grp.id() }),
71                 function(child) {traverse_grp_tree(child, failed)}
72             );
73         }
74
75         return egCore.perm.hasPermAt(all_app_perms, true).then(
76             function(perm_orgs) {
77                 angular.forEach(all_app_perms, function(p) {
78                     if (perm_orgs[p].length == 0)
79                         failed_perms.push(p);
80                 });
81
82                 traverse_grp_tree(egCore.env.pgt.tree);
83             }
84         );
85     }
86
87     service.has_group_link_perms = function(org_id) {
88         return egCore.perm.hasPermAt('CREATE_USER_GROUP_LINK', true)
89         .then(function(p) { return p.indexOf(org_id) > -1; });
90     }
91
92     service.get_surveys = function() {
93         var org_ids = egCore.org.fullPath(egCore.auth.user().ws_ou(), true);
94
95         return egCore.pcrud.search('asv', {
96                 owner : org_ids,
97                 start_date : {'<=' : 'now'},
98                 end_date : {'>=' : 'now'}
99             }, {   
100                 flesh : 2, 
101                 flesh_fields : {
102                     asv : ['questions'], 
103                     asvq : ['answers']
104                 }
105             }, 
106             {atomic : true}
107         ).then(function(surveys) {
108             surveys = surveys.sort(function(a,b) {
109                 return a.name() < b.name() ? -1 : 1 });
110             service.surveys = surveys;
111             angular.forEach(surveys, function(survey) {
112                 angular.forEach(survey.questions(), function(question) {
113                     service.survey_questions[question.id()] = question;
114                     angular.forEach(question.answers(), function(answer) {
115                         service.survey_answers[answer.id()] = answer;
116                     });
117                 });
118             });
119         });
120     }
121
122     service.get_stat_cats = function() {
123         return egCore.net.request(
124             'open-ils.circ',
125             'open-ils.circ.stat_cat.actor.retrieve.all',
126             egCore.auth.token(), egCore.auth.user().ws_ou()
127         ).then(function(cats) {
128             cats = cats.sort(function(a, b) {
129                 return a.name() < b.name() ? -1 : 1});
130             angular.forEach(cats, function(cat) {
131                 cat.entries(
132                     cat.entries().sort(function(a,b) {
133                         return a.value() < b.value() ? -1 : 1
134                     })
135                 );
136             });
137             service.stat_cats = cats;
138         });
139     };
140
141     service.get_org_settings = function() {
142         return egCore.org.settings([
143             'global.password_regex',
144             'global.juvenile_age_threshold',
145             'patron.password.use_phone',
146             'ui.patron.default_inet_access_level',
147             'ui.patron.default_ident_type',
148             'ui.patron.default_country',
149             'ui.patron.registration.require_address',
150             'circ.holds.behind_desk_pickup_supported',
151             'circ.patron_edit.clone.copy_address',
152             'ui.patron.edit.au.prefix.require',
153             'ui.patron.edit.au.prefix.show',
154             'ui.patron.edit.au.prefix.suggest',
155             'ui.patron.edit.ac.barcode.regex',
156             'ui.patron.edit.au.second_given_name.show',
157             'ui.patron.edit.au.second_given_name.suggest',
158             'ui.patron.edit.au.suffix.show',
159             'ui.patron.edit.au.suffix.suggest',
160             'ui.patron.edit.au.alias.show',
161             'ui.patron.edit.au.alias.suggest',
162             'ui.patron.edit.au.dob.require',
163             'ui.patron.edit.au.dob.show',
164             'ui.patron.edit.au.dob.suggest',
165             'ui.patron.edit.au.dob.calendar',
166             'ui.patron.edit.au.juvenile.show',
167             'ui.patron.edit.au.juvenile.suggest',
168             'ui.patron.edit.au.ident_value.show',
169             'ui.patron.edit.au.ident_value.suggest',
170             'ui.patron.edit.au.ident_value2.show',
171             'ui.patron.edit.au.ident_value2.suggest',
172             'ui.patron.edit.au.email.require',
173             'ui.patron.edit.au.email.show',
174             'ui.patron.edit.au.email.suggest',
175             'ui.patron.edit.au.email.regex',
176             'ui.patron.edit.au.email.example',
177             'ui.patron.edit.au.day_phone.require',
178             'ui.patron.edit.au.day_phone.show',
179             'ui.patron.edit.au.day_phone.suggest',
180             'ui.patron.edit.au.day_phone.regex',
181             'ui.patron.edit.au.day_phone.example',
182             'ui.patron.edit.au.evening_phone.require',
183             'ui.patron.edit.au.evening_phone.show',
184             'ui.patron.edit.au.evening_phone.suggest',
185             'ui.patron.edit.au.evening_phone.regex',
186             'ui.patron.edit.au.evening_phone.example',
187             'ui.patron.edit.au.other_phone.require',
188             'ui.patron.edit.au.other_phone.show',
189             'ui.patron.edit.au.other_phone.suggest',
190             'ui.patron.edit.au.other_phone.regex',
191             'ui.patron.edit.au.other_phone.example',
192             'ui.patron.edit.phone.regex',
193             'ui.patron.edit.phone.example',
194             'ui.patron.edit.au.active.show',
195             'ui.patron.edit.au.active.suggest',
196             'ui.patron.edit.au.barred.show',
197             'ui.patron.edit.au.barred.suggest',
198             'ui.patron.edit.au.master_account.show',
199             'ui.patron.edit.au.master_account.suggest',
200             'ui.patron.edit.au.claims_returned_count.show',
201             'ui.patron.edit.au.claims_returned_count.suggest',
202             'ui.patron.edit.au.claims_never_checked_out_count.show',
203             'ui.patron.edit.au.claims_never_checked_out_count.suggest',
204             'ui.patron.edit.au.alert_message.show',
205             'ui.patron.edit.au.alert_message.suggest',
206             'ui.patron.edit.aua.post_code.regex',
207             'ui.patron.edit.aua.post_code.example',
208             'ui.patron.edit.aua.county.require',
209             'format.date',
210             'ui.patron.edit.default_suggested',
211             'opac.barcode_regex',
212             'opac.username_regex',
213             'sms.enable',
214             'ui.patron.edit.aua.state.require',
215             'ui.patron.edit.aua.state.suggest',
216             'ui.patron.edit.aua.state.show'
217         ]).then(function(settings) {
218             service.org_settings = settings;
219             return service.process_org_settings(settings);
220         });
221     };
222
223     // some org settings require the retrieval of additional data
224     service.process_org_settings = function(settings) {
225
226         var promises = [];
227
228         if (settings['sms.enable']) {
229             // fetch SMS carriers
230             promises.push(
231                 egCore.pcrud.search('csc', 
232                     {active: 'true'}, 
233                     {'order_by':[
234                         {'class':'csc', 'field':'name'},
235                         {'class':'csc', 'field':'region'}
236                     ]}, {atomic : true}
237                 ).then(function(carriers) {
238                     service.sms_carriers = carriers;
239                 })
240             );
241         } else {
242             // if other promises are added below, this is not necessary.
243             promises.push($q.when());  
244         }
245
246         // other post-org-settings processing goes here,
247         // adding to promises as needed.
248
249         return $q.all(promises);
250     };
251
252     service.get_ident_types = function() {
253         if (egCore.env.cit) {
254             service.ident_types = egCore.env.cit.list;
255             return $q.when();
256         } else {
257             return egCore.pcrud.retrieveAll('cit', {}, {atomic : true})
258             .then(function(types) { 
259                 egCore.env.absorbList(types, 'cit')
260                 service.ident_types = types 
261             });
262         }
263     };
264
265     service.get_net_access_levels = function() {
266         if (egCore.env.cnal) {
267             service.net_access_levels = egCore.env.cnal.list;
268             return $q.when();
269         } else {
270             return egCore.pcrud.retrieveAll('cnal', {}, {atomic : true})
271             .then(function(levels) { 
272                 egCore.env.absorbList(levels, 'cnal')
273                 service.net_access_levels = levels 
274             });
275         }
276     }
277
278     service.get_perm_groups = function() {
279         if (egCore.env.pgt) {
280             service.profiles = egCore.env.pgt.list;
281             return service.set_edit_profiles();
282         } else {
283             return egCore.pcrud.search('pgt', {parent : null}, 
284                 {flesh : -1, flesh_fields : {pgt : ['children']}}
285             ).then(
286                 function(tree) {
287                     egCore.env.absorbTree(tree, 'pgt')
288                     service.profiles = egCore.env.pgt.list;
289                     return service.set_edit_profiles();
290                 }
291             );
292         }
293     }
294
295     service.get_field_doc = function() {
296         return egCore.pcrud.search('fdoc', {
297             fm_class: ['au', 'ac', 'aua', 'actsc', 'asv', 'asvq', 'asva']})
298         .then(null, null, function(doc) {
299             if (!service.field_doc[doc.fm_class()]) {
300                 service.field_doc[doc.fm_class()] = {};
301             }
302             service.field_doc[doc.fm_class()][doc.field()] = doc;
303         });
304     };
305
306     service.get_user_settings = function() {
307         var org_ids = egCore.org.ancestors(egCore.auth.user().ws_ou(), true);
308
309         var static_types = [
310             'circ.holds_behind_desk', 
311             'circ.collections.exempt', 
312             'opac.hold_notify', 
313             'opac.default_phone', 
314             'opac.default_pickup_location', 
315             'opac.default_sms_carrier', 
316             'opac.default_sms_notify'];
317
318         return egCore.pcrud.search('cust', {
319             '-or' : [
320                 {name : static_types}, // common user settings
321                 {name : { // opt-in notification user settings
322                     'in': {
323                         select : {atevdef : ['opt_in_setting']}, 
324                         from : 'atevdef',
325                         // we only care about opt-in settings for 
326                         // event_defs our users encounter
327                         where : {'+atevdef' : {owner : org_ids}}
328                     }
329                 }}
330             ]
331         }, {}, {atomic : true}).then(function(setting_types) {
332
333             angular.forEach(setting_types, function(stype) {
334                 service.user_setting_types[stype.name()] = stype;
335                 if (static_types.indexOf(stype.name()) == -1) {
336                     service.opt_in_setting_types[stype.name()] = stype;
337                 }
338             });
339
340             if (service.patron_id) {
341                 // retrieve applied values for the current user 
342                 // for the setting types we care about.
343
344                 var setting_names = 
345                     setting_types.map(function(obj) { return obj.name() });
346
347                 return egCore.net.request(
348                     'open-ils.actor', 
349                     'open-ils.actor.patron.settings.retrieve.authoritative',
350                     egCore.auth.token(),
351                     service.patron_id,
352                     setting_names
353                 ).then(function(settings) {
354                     service.user_settings = settings;
355                 });
356             } else {
357
358                 // apply default user setting values
359                 angular.forEach(setting_types, function(stype, index) {
360                     if (stype.reg_default() != undefined) {
361                         service.user_settings[setting.name()] = 
362                             setting.reg_default();
363                     }
364                 });
365             }
366         });
367     }
368
369     service.invalidate_field = function(patron, field) {
370         console.log('Invalidating patron field ' + field);
371
372         return egCore.net.request(
373             'open-ils.actor',
374             'open-ils.actor.invalidate.' + field,
375             egCore.auth.token(), patron.id, null, patron.home_ou.id()
376
377         ).then(function(res) {
378             // clear the invalid value from the form
379             patron[field] = '';
380
381             // update last_xact_id so future save operations
382             // on this patron will be allowed
383             patron.last_xact_id = res.payload.last_xact_id[patron.id];
384         });
385     }
386
387     service.dupe_patron_search = function(patron, type, value) {
388         var search;
389
390         console.log('Dupe search called with "' + 
391             type +"' and value " + value);
392
393         switch (type) {
394
395             case 'name':
396                 var fname = patron.first_given_name;   
397                 var lname = patron.family_name;   
398                 if (!(fname && lname)) return;
399                 search = {
400                     first_given_name : {value : fname, group : 0},
401                     family_name : {value : lname, group : 0}
402                 };
403                 break;
404
405             case 'email':
406                 search = {email : {value : value, group : 0}};
407                 break;
408
409             case 'ident':
410                 search = {ident : {value : value, group : 2}};
411                 break;
412
413             case 'phone':
414                 search = {phone : {value : value, group : 2}};
415                 break;
416
417             case 'address':
418                 search = {};
419                 angular.forEach(['street1', 'street2', 'city', 'post_code'],
420                     function(field) {
421                         if(value[field])
422                             search[field] = {value : value[field], group: 1};
423                     }
424                 );
425                 break;
426         }
427
428         return egCore.net.request( 
429             'open-ils.actor', 
430             'open-ils.actor.patron.search.advanced',
431             egCore.auth.token(), search, null, null, 1
432         ).then(function(res) {
433             res = res.filter(function(id) {return id != patron.id});
434             return {
435                 count : res.length,
436                 search : search
437             };
438         });
439     }
440
441     service.init_patron = function(current) {
442
443         if (!current)
444             return service.init_new_patron();
445
446         service.patron = current;
447         return service.init_existing_patron(current)
448     }
449
450     service.ingest_address = function(patron, addr) {
451         addr.valid = addr.valid == 't';
452         addr.within_city_limits = addr.within_city_limits == 't';
453         addr._is_mailing = (patron.mailing_address && 
454             addr.id == patron.mailing_address.id);
455         addr._is_billing = (patron.billing_address && 
456             addr.id == patron.billing_address.id);
457     }
458
459     /*
460      * Existing patron objects reqire some data munging before insertion
461      * into the scope.
462      *
463      * 1. Turn everything into a hash
464      * 2. ... Except certain fields (selectors) whose widgets require objects
465      * 3. Bools must be Boolean, not t/f.
466      */
467     service.init_existing_patron = function(current) {
468
469         var patron = egCore.idl.toHash(current);
470
471         patron.home_ou = egCore.org.get(patron.home_ou.id);
472         patron.expire_date = new Date(Date.parse(patron.expire_date));
473         patron.dob = patron.dob ?
474             new Date(Date.parse(patron.dob)) : null;
475         patron.profile = current.profile(); // pre-hash version
476         patron.net_access_level = current.net_access_level();
477         patron.ident_type = current.ident_type();
478         patron.groups = current.groups(); // pre-hash
479
480         angular.forEach(
481             ['juvenile', 'barred', 'active', 'master_account'],
482             function(field) { patron[field] = patron[field] == 't'; }
483         );
484
485         angular.forEach(patron.cards, function(card) {
486             card.active = card.active == 't';
487             if (card.id == patron.card.id) {
488                 patron.card = card;
489                 card._primary = 'on';
490             }
491         });
492
493         angular.forEach(patron.addresses, 
494             function(addr) { service.ingest_address(patron, addr) });
495
496         // toss entries for existing stat cat maps into our living 
497         // stat cat entry map, which is modified within the template.
498         angular.forEach(patron.stat_cat_entries, function(map) {
499             service.stat_cat_entry_maps[map.stat_cat.id] = map.stat_cat_entry;
500         });
501
502         return patron;
503     }
504
505     service.init_new_patron = function() {
506         var addr = {
507             id : service.virt_id--,
508             isnew : true,
509             valid : true,
510             address_type : egCore.strings.REG_ADDR_TYPE,
511             _is_mailing : true,
512             _is_billing : true,
513             within_city_limits : false,
514             stat_cat_entries : []
515         };
516
517         var card = {
518             id : service.virt_id--,
519             isnew : true,
520             active : true,
521             _primary : 'on'
522         };
523
524         return {
525             isnew : true,
526             active : true,
527             card : card,
528             cards : [card],
529             home_ou : egCore.org.get(egCore.auth.user().ws_ou()),
530                         
531             // TODO default profile group?
532             addresses : [addr]
533         };
534     }
535
536     // translate the patron back into IDL form
537     service.save_user = function(phash) {
538
539         var patron = egCore.idl.fromHash('au', phash);
540
541         patron.home_ou(patron.home_ou().id());
542         patron.expire_date(patron.expire_date().toISOString());
543         patron.profile(patron.profile().id());
544         if (patron.dob()) 
545             patron.dob(patron.dob().toISOString().replace(/T.*/,''));
546         if (patron.ident_type()) 
547             patron.ident_type(patron.ident_type().id());
548         if (patron.net_access_level())
549             patron.net_access_level(patron.net_access_level().id());
550
551         angular.forEach(
552             ['juvenile', 'barred', 'active', 'master_account'],
553             function(field) { patron[field](phash[field] ? 't' : 'f'); }
554         );
555
556         var card_hashes = patron.cards();
557         patron.cards([]);
558         angular.forEach(card_hashes, function(chash) {
559             var card = egCore.idl.fromHash('ac', chash)
560             card.usr(patron.id());
561             card.active(chash.active ? 't' : 'f');
562             patron.cards().push(card);
563             if (chash._primary) {
564                 patron.card(card);
565             }
566         });
567
568         var addr_hashes = patron.addresses();
569         patron.addresses([]);
570         angular.forEach(addr_hashes, function(addr_hash) {
571             if (!addr_hash.isnew && !addr_hash.isdeleted) 
572                 addr_hash.ischanged = true;
573             var addr = egCore.idl.fromHash('aua', addr_hash);
574             patron.addresses().push(addr);
575             addr.valid(addr.valid() ? 't' : 'f');
576             addr.within_city_limits(addr.within_city_limits() ? 't' : 'f');
577             if (addr_hash._is_mailing) patron.mailing_address(addr);
578             if (addr_hash._is_billing) patron.billing_address(addr);
579         });
580
581         patron.survey_responses([]);
582         angular.forEach(service.survey_responses, function(answer) {
583             var question = service.survey_questions[answer.question()];
584             var resp = new egCore.idl.asvr();
585             resp.isnew(true);
586             resp.survey(question.survey());
587             resp.question(question.id());
588             resp.answer(answer.id());
589             resp.usr(patron.id());
590             resp.answer_date('now');
591             patron.survey_responses().push(resp);
592         });
593         
594         // re-object-ify the patron stat cat entry maps
595         var maps = [];
596         angular.forEach(patron.stat_cat_entries(), function(entry) {
597             var e = egCore.idl.fromHash('actscecm', entry);
598             e.stat_cat(e.stat_cat().id);
599             maps.push(e);
600         });
601         patron.stat_cat_entries(maps);
602
603         // service.stat_cat_entry_maps maps stats to values
604         // patron.stat_cat_entries is an array of stat_cat_entry_usr_map's
605         angular.forEach(
606             service.stat_cat_entry_maps, function(value, cat_id) {
607
608             // see if we already have a mapping for this entry
609             var existing = patron.stat_cat_entries().filter(
610                 function(e) { return e.stat_cat() == cat_id })[0];
611
612             if (existing) { // we have a mapping
613                 // if the existing mapping matches the new one,
614                 // there' nothing left to do
615                 if (existing.stat_cat_entry() == value) return;
616
617                 // mappings differ.  delete the old one and create
618                 // a new one below.
619                 existing.isdeleted(true);
620             }
621
622             var newmap = new egCore.idl.actscecm();
623             newmap.target_usr(patron.id());
624             newmap.isnew(true);
625             newmap.stat_cat(cat_id);
626             newmap.stat_cat_entry(value);
627             patron.stat_cat_entries().push(newmap);
628         });
629
630         if (!patron.isnew()) patron.ischanged(true);
631
632         return egCore.net.request(
633             'open-ils.actor', 
634             'open-ils.actor.patron.update',
635             egCore.auth.token(), patron);
636     }
637
638     service.save_user_settings = function(new_user, user_settings) {
639         // user_settings contains the values from the scope/form.
640         // service.user_settings contain the values from page load time.
641
642         var settings = {};
643         if (service.patron_id) {
644             // only update modified settings for existing patrons
645             angular.forEach(user_settings, function(val, key) {
646                 if (val !== service.user_settings[key])
647                     settings[key] = val;
648             });
649
650         } else {
651             // all non-null setting values are updated for new patrons
652             angular.forEach(user_settings, function(val, key) {
653                 if (val !== null) settings[key] = val;
654             });
655         }
656
657         if (Object.keys(settings).length == 0) return $q.when();
658
659         return egCore.net.request(
660             'open-ils.actor',
661             'open-ils.actor.patron.settings.update',
662             egCore.auth.token(), new_user.id(), settings
663         ).then(function(resp) {
664             console.log('settings returned ' + resp);
665             return resp;
666         });
667     }
668
669     return service;
670 }]);
671
672
673 function PatronRegCtrl($scope, $routeParams, 
674     $q, $modal, $window, egCore, patronSvc, patronRegSvc, egUnloadPrompt) {
675
676     $scope.page_data_loaded = false;
677     $scope.clone_id = $routeParams.clone_id;
678     $scope.stage_username = $routeParams.stage_username;
679     $scope.patron_id = 
680         patronRegSvc.patron_id = $routeParams.edit_id || $routeParams.id;
681
682     // for existing patrons, disable barcode input by default
683     $scope.disable_bc = $scope.focus_usrname = Boolean($scope.patron_id);
684     $scope.focus_bc = !Boolean($scope.patron_id);
685     $scope.dupe_counts = {};
686
687     if (!$scope.edit_passthru) {
688         // in edit more, scope.edit_passthru is delivered to us by
689         // the enclosing controller.  In register mode, there is 
690         // no enclosing controller, so we create our own.
691         $scope.edit_passthru = {};
692     }
693
694     // 0=all, 1=suggested, 2=all
695     $scope.edit_passthru.vis_level = 0; 
696     // TODO: add save/clone handlers here
697
698     var modify_tracked = false;
699     $scope.field_modified = function() {
700         if (modify_tracked) return;
701         modify_tracked = true;
702         egUnloadPrompt.attach($scope);
703     }
704
705     // Apply default values for new patrons during initial registration
706     // prs is shorthand for patronSvc
707     function set_new_patron_defaults(prs) {
708         $scope.generate_password();
709         $scope.hold_notify_phone = true;
710         $scope.hold_notify_email = true;
711
712         if (prs.org_settings['ui.patron.default_ident_type']) {
713             // $scope.patron needs this field to be an object
714             var id = prs.org_settings['ui.patron.default_ident_type'];
715             var ident_type = $scope.ident_types.filter(
716                 function(type) { return type.id() == id })[0];
717             $scope.patron.ident_type = ident_type;
718         }
719         if (prs.org_settings['ui.patron.default_inet_access_level']) {
720             // $scope.patron needs this field to be an object
721             var id = prs.org_settings['ui.patron.default_inet_access_level'];
722             var level = $scope.net_access_levels.filter(
723                 function(lvl) { return lvl.id() == id })[0];
724             $scope.patron.net_access_level = level;
725         }
726         if (prs.org_settings['ui.patron.default_country']) {
727             $scope.patron.addresses[0].country = 
728                 prs.org_settings['ui.patron.default_country'];
729         }
730     }
731
732     function handle_home_org_changed() {
733         org_id = $scope.patron.home_ou.id();
734
735         patronRegSvc.has_group_link_perms(org_id)
736         .then(function(bool) {$scope.has_group_link_perm = bool});
737     }
738
739     $q.all([
740
741         $scope.initTab ? // initTab comes from patron app
742             $scope.initTab('edit', $routeParams.id) : $q.when(),
743
744         patronRegSvc.init()
745
746     ]).then(function() {
747         // called after initTab and patronRegSvc.init have completed
748
749         var prs = patronRegSvc; // brevity
750         // in standalone mode, we have no patronSvc
751         $scope.patron = prs.init_patron(patronSvc ? patronSvc.current : null);
752         $scope.field_doc = prs.field_doc;
753         $scope.edit_profiles = prs.edit_profiles;
754         $scope.ident_types = prs.ident_types;
755         $scope.net_access_levels = prs.net_access_levels;
756         $scope.user_setting_types = prs.user_setting_types;
757         $scope.opt_in_setting_types = prs.opt_in_setting_types;
758         $scope.org_settings = prs.org_settings;
759         $scope.sms_carriers = prs.sms_carriers;
760         $scope.stat_cats = prs.stat_cats;
761         $scope.surveys = prs.surveys;
762         $scope.survey_responses = prs.survey_responses;
763         $scope.stat_cat_entry_maps = prs.stat_cat_entry_maps;
764
765         $scope.user_settings = prs.user_settings;
766         // clone the user settings back into the patronRegSvc so
767         // we have a copy of the original state of the settings.
768         prs.user_settings = {};
769         angular.forEach($scope.user_settings, function(val, key) {
770             prs.user_settings[key] = val;
771         });
772
773         extract_hold_notify();
774         handle_home_org_changed();
775
776         if ($scope.org_settings['ui.patron.edit.default_suggested'])
777             $scope.edit_passthru.vis_level = 1;
778
779         if ($scope.patron.isnew) 
780             set_new_patron_defaults(prs);
781
782         $scope.page_data_loaded = true;
783     });
784
785     // update the currently displayed field documentation
786     $scope.set_selected_field_doc = function(cls, field) {
787         $scope.selected_field_doc = $scope.field_doc[cls][field];
788     }
789
790     // returns the tree depth of the selected profile group tree node.
791     $scope.pgt_depth = function(grp) {
792         var d = 0;
793         while (grp = egCore.env.pgt.map[grp.parent()]) d++;
794         return d;
795     }
796
797     // IDL fields used for labels in the UI.
798     $scope.idl_fields = {
799         au  : egCore.idl.classes.au.field_map,
800         ac  : egCore.idl.classes.ac.field_map,
801         aua : egCore.idl.classes.aua.field_map
802     };
803
804     // field visibility cache.  Some fields are universally required.
805     var field_visibility = {
806         'ac.barcode' : 2,
807         'au.usrname' : 2,
808         'au.passwd' :  2,
809         // TODO passwd2 2,
810         'au.first_given_name' : 2,
811         'au.family_name' : 2,
812         'au.ident_type' : 2,
813         'au.home_ou' : 2,
814         'au.profile' : 2,
815         'au.expire_date' : 2,
816         'au.net_access_level' : 2,
817         'aua.address_type' : 2,
818         'aua.post_code' : 2,
819         'aua.street1' : 2,
820         'aua.street2' : 2,
821         'aua.city' : 2,
822         'aua.county' : 2,
823         'aua.state' : 2,
824         'aua.country' : 2,
825         'aua.valid' : 2,
826         'aua.within_city_limits' : 2,
827         'stat_cats' : 1,
828         'surveys' : 1
829     }; 
830
831     // returns true if the selected field should be visible
832     // given the current required/suggested/all setting.
833     $scope.show_field = function(field_key) {
834
835         if (field_visibility[field_key] == undefined) {
836             // compile and cache the visibility for the selected field
837
838             // org settings have not been received yet.
839             if (!$scope.org_settings) return false;
840
841             var req_set = 'ui.patron.edit.' + field_key + '.require';
842             var sho_set = 'ui.patron.edit.' + field_key + '.show';
843             var sug_set = 'ui.patron.edit.' + field_key + '.suggest';
844
845             if ($scope.org_settings[req_set]) {
846                 field_visibility[field_key] = 2;
847             } else if ($scope.org_settings[sho_set]) {
848                 field_visibility[field_key] = 2;
849             } else if ($scope.org_settings[sug_set]) {
850                 field_visibility[field_key] = 1;
851             } else {
852                 field_visibility[field_key] = 0;
853             }
854         }
855
856         return field_visibility[field_key] >= $scope.edit_passthru.vis_level;
857     }
858
859     // generates a random 4-digit password
860     $scope.generate_password = function() {
861         $scope.patron.passwd = Math.floor(Math.random()*9000) + 1000;
862     }
863
864     $scope.set_expire_date = function() {
865         if (!$scope.patron.profile) return;
866         var seconds = egCore.date.intervalToSeconds(
867             $scope.patron.profile.perm_interval());
868         var now_epoch = new Date().getTime();
869         $scope.patron.expire_date = new Date(
870             now_epoch + (seconds * 1000 /* milliseconds */))
871     }
872
873     // grp is the pgt object
874     $scope.set_profile = function(grp) {
875         $scope.patron.profile = grp;
876         $scope.set_expire_date();
877         $scope.field_modified();
878     }
879
880     $scope.new_address = function() {
881         var addr = egCore.idl.toHash(new egCore.idl.aua());
882         patronRegSvc.ingest_address($scope.patron, addr);
883         addr.id = patronRegSvc.virt_id--;
884         addr.isnew = true;
885         addr.valid = true;
886         addr.within_city_limits = true;
887         $scope.patron.addresses.push(addr);
888     }
889
890     // keep deleted addresses out of the patron object so
891     // they won't appear in the UI.  They'll be re-inserted
892     // when the patron is updated.
893     deleted_addresses = [];
894     $scope.delete_address = function(id) {
895         var addresses = [];
896         angular.forEach($scope.patron.addresses, function(addr) {
897             if (addr.id == id) {
898                 if (id > 0) {
899                     addr.isdeleted = true;
900                     deleted_addresses.push(addr);
901                 }
902             } else {
903                 addresses.push(addr);
904             }
905         });
906         $scope.patron.addresses = addresses;
907     } 
908
909     $scope.post_code_changed = function(addr) { 
910         egCore.net.request(
911             'open-ils.search', 'open-ils.search.zip', addr.post_code)
912         .then(function(resp) {
913             if (!resp) return;
914             if (resp.city) addr.city = resp.city;
915             if (resp.state) addr.state = resp.state;
916             if (resp.county) addr.county = resp.county;
917             if (resp.alert) alert(resp.alert);
918         });
919     }
920
921     $scope.replace_card = function() {
922         $scope.patron.card.active = false;
923         $scope.patron.card.ischanged = true;
924         $scope.disable_bc = false;
925
926         var new_card = egCore.idl.toHash(new egCore.idl.ac());
927         new_card.id = patronRegSvc.virt_id--;
928         new_card.isnew = true;
929         new_card.active = true;
930         new_card._primary = 'on';
931         $scope.patron.card = new_card;
932         $scope.patron.cards.push(new_card);
933     }
934
935     $scope.day_phone_changed = function(phone) {
936         if (phone && $scope.patron.isnew && 
937             $scope.org_settings['patron.password.use_phone']) {
938             $scope.patron.passwd = phone.substr(-4);
939         }
940     }
941
942     $scope.barcode_changed = function(bc) {
943         if (!bc) return;
944         egCore.net.request(
945             'open-ils.actor',
946             'open-ils.actor.barcode.exists',
947             egCore.auth.token(), bc
948         ).then(function(resp) {
949             if (resp == '1') {
950                 console.log('duplicate barcode detected: ' + bc);
951                 // DUPLICATE CARD
952             } else {
953                 if (!$scope.patron.usrname)
954                     $scope.patron.usrname = bc;
955                 // No dupe -- A-OK
956             }
957         });
958     }
959
960     $scope.cards_dialog = function() {
961         $modal.open({
962             templateUrl: './circ/patron/t_patron_cards_dialog',
963             controller: 
964                    ['$scope','$modalInstance','cards',
965             function($scope , $modalInstance , cards) {
966                 // scope here is the modal-level scope
967                 $scope.args = {cards : cards};
968                 $scope.ok = function() { $modalInstance.close($scope.args) }
969                 $scope.cancel = function () { $modalInstance.dismiss() }
970             }],
971             resolve : {
972                 cards : function() {
973                     // scope here is the controller-level scope
974                     return $scope.patron.cards;
975                 }
976             }
977         }).result.then(
978             function(args) {
979                 angular.forEach(args.cards, function(card) {
980                     card.ischanged = true; // assume cards need updating, OK?
981                     if (card._primary == 'on' && 
982                         card.id != $scope.patron.card.id) {
983                         $scope.patron.card = card;
984                     }
985                 });
986             }
987         );
988     }
989
990     $scope.set_addr_type = function(addr, type) {
991         var addrs = $scope.patron.addresses;
992         if (addr['_is_'+type]) {
993             angular.forEach(addrs, function(a) {
994                 if (a.id != addr.id) a['_is_'+type] = false;
995             });
996         } else {
997             // unchecking mailing/billing means we have to randomly
998             // select another address to fill that role.  Select the
999             // first address in the list (that does not match the
1000             // modifed address)
1001             for (var i = 0; i < addrs.length; i++) {
1002                 if (addrs[i].id != addr.id) {
1003                     addrs[i]['_is_' + type] = true;
1004                     break;
1005                 }
1006             }
1007         }
1008     }
1009
1010
1011     // Translate hold notify preferences from the form/scope back into a 
1012     // single user setting value for opac.hold_notify.
1013     function compress_hold_notify() {
1014         var hold_notify = '';
1015         var splitter = '';
1016         if ($scope.hold_notify_phone) {
1017             hold_notify = 'phone';
1018             splitter = ':';
1019         }
1020         if ($scope.hold_notify_email) {
1021             hold_notify = splitter + 'email';
1022             splitter = ':';
1023         }
1024         if ($scope.hold_notify_sms) {
1025             hold_notify = splitter + 'sms';
1026             splitter = ':';
1027         }
1028         $scope.user_settings['opac.hold_notify'] = hold_notify;
1029     }
1030
1031     // dialog for selecting additional permission groups
1032     $scope.secondary_groups_dialog = function() {
1033         $modal.open({
1034             templateUrl: './circ/patron/t_patron_groups_dialog',
1035             controller: 
1036                    ['$scope','$modalInstance','linked_groups','pgt_depth',
1037             function($scope , $modalInstance , linked_groups , pgt_depth) {
1038
1039                 $scope.pgt_depth = pgt_depth;
1040                 $scope.args = {
1041                     linked_groups : linked_groups,
1042                     edit_profiles : patronRegSvc.edit_profiles,
1043                     new_profile   : patronRegSvc.edit_profiles[0]
1044                 };
1045
1046                 // add a new group to the linked groups list
1047                 $scope.link_group = function($event, grp) {
1048                     var found = false; // avoid duplicates
1049                     angular.forEach($scope.args.linked_groups, 
1050                         function(g) {if (g.id() == grp.id()) found = true});
1051                     if (!found) $scope.args.linked_groups.push(grp);
1052                     $event.preventDefault(); // avoid close
1053                 }
1054
1055                 // remove a group from the linked groups list
1056                 $scope.unlink_group = function($event, grp) {
1057                     $scope.args.linked_groups = 
1058                         $scope.args.linked_groups.filter(function(g) {
1059                         return g.id() != grp.id()
1060                     });
1061                     $event.preventDefault(); // avoid close
1062                 }
1063
1064                 $scope.ok = function() { $modalInstance.close($scope.args) }
1065                 $scope.cancel = function () { $modalInstance.dismiss() }
1066             }],
1067             resolve : {
1068                 linked_groups : function() { return $scope.patron.groups },
1069                 pgt_depth : function() { return $scope.pgt_depth }
1070             }
1071         }).result.then(
1072             function(args) {
1073                 var ids = args.linked_groups.map(function(g) {return g.id()});
1074                 console.log('linking permission groups ' + ids);
1075                 return egCore.net.request(
1076                     'open-ils.actor',
1077                     'open-ils.actor.user.set_groups',
1078                     egCore.auth.token(), $scope.patron.id, ids)
1079                 .then(function(resp) {
1080                     if (resp == 1) {
1081                         $scope.patron.groups = args.linked_groups;
1082                     } else {
1083                         // debugging -- should be no events
1084                         alert('linked groups failure ' + egCore.evt.parse(resp));
1085                     }
1086                 });
1087             }
1088         );
1089     }
1090
1091     function extract_hold_notify() {
1092         notify = $scope.user_settings['opac.hold_notify'];
1093         if (!notify) return;
1094         $scope.hold_notify_phone = Boolean(notify.match(/phone/));
1095         $scope.hold_notify_email = Boolean(notify.match(/email/));
1096         $scope.hold_notify_sms = Boolean(notify.match(/sms/));
1097     }
1098
1099     $scope.invalidate_field = function(field) {
1100         patronRegSvc.invalidate_field($scope.patron, field);
1101     }
1102
1103     $scope.dupe_value_changed = function(type, value) {
1104         $scope.dupe_counts[type] = 0;
1105         patronRegSvc.dupe_patron_search($scope.patron, type, value)
1106         .then(function(res) {
1107             $scope.dupe_counts[type] = res.count;
1108             $scope.dupe_search_encoded = 
1109                 encodeURIComponent(js2JSON(res.search));
1110         });
1111     }
1112
1113     $scope.edit_passthru.save = function() {
1114
1115         // remove page unload warning prompt
1116         egUnloadPrompt.clear();
1117
1118         // toss the deleted addresses back into the patron's list of
1119         // addresses so it's included in the update
1120         $scope.patron.addresses = 
1121             $scope.patron.addresses.concat(deleted_addresses);
1122         
1123         compress_hold_notify();
1124
1125         patronRegSvc.save_user($scope.patron)
1126         .then(function(new_user) { 
1127             if (new_user && new_user.classname) {
1128                 return patronRegSvc.save_user_settings(
1129                     new_user, $scope.user_settings); 
1130             } else {
1131                 alert('Patron update failed. \n\n' + js2JSON(new_user));
1132                 return true; // ensure page reloads to reset
1133             }
1134         }).then(function(keep_going) {
1135             // reloading the page means potentially losing some information
1136             // (e.g. last patron search), but is the only way to ensure all
1137             // components are properly updated to reflect the modified patron.
1138             $window.location.href = location.href;
1139         });
1140     }
1141 }
1142
1143 // This controller may be loaded from different modules (patron edit vs.
1144 // register new patron), so we have to inject the controller params manually.
1145 PatronRegCtrl.$inject = ['$scope', '$routeParams', '$q', '$modal', 
1146     '$window', 'egCore', 'patronSvc', 'patronRegSvc', 'egUnloadPrompt'];
1147