]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/circ/patron/regctl.js
LP#1452950 Patron reg avoid data re-fetch
[working/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 entry object map
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             var entry;
500             angular.forEach(service.stat_cats, function(cat) {
501                 angular.forEach(cat.entries(), function(ent) {
502                     if (ent.id() == map.stat_cat_entry)
503                         entry = ent;
504                 });
505             });
506             service.stat_cat_entry_maps[map.stat_cat.id] = entry;
507         });
508
509         return patron;
510     }
511
512     service.init_new_patron = function() {
513         var addr = {
514             id : service.virt_id--,
515             isnew : true,
516             valid : true,
517             address_type : egCore.strings.REG_ADDR_TYPE,
518             _is_mailing : true,
519             _is_billing : true,
520             within_city_limits : false,
521             stat_cat_entries : []
522         };
523
524         var card = {
525             id : service.virt_id--,
526             isnew : true,
527             active : true,
528             _primary : 'on'
529         };
530
531         return {
532             isnew : true,
533             active : true,
534             card : card,
535             cards : [card],
536             home_ou : egCore.org.get(egCore.auth.user().ws_ou()),
537                         
538             // TODO default profile group?
539             addresses : [addr]
540         };
541     }
542
543     // translate the patron back into IDL form
544     service.save_user = function(phash) {
545
546         var patron = egCore.idl.fromHash('au', phash);
547
548         patron.home_ou(patron.home_ou().id());
549         patron.expire_date(patron.expire_date().toISOString());
550         patron.profile(patron.profile().id());
551         if (patron.dob()) 
552             patron.dob(patron.dob().toISOString().replace(/T.*/,''));
553         if (patron.ident_type()) 
554             patron.ident_type(patron.ident_type().id());
555         if (patron.net_access_level())
556             patron.net_access_level(patron.net_access_level().id());
557
558         angular.forEach(
559             ['juvenile', 'barred', 'active', 'master_account'],
560             function(field) { patron[field](phash[field] ? 't' : 'f'); }
561         );
562
563         var card_hashes = patron.cards();
564         patron.cards([]);
565         angular.forEach(card_hashes, function(chash) {
566             var card = egCore.idl.fromHash('ac', chash)
567             card.usr(patron.id());
568             card.active(chash.active ? 't' : 'f');
569             patron.cards().push(card);
570             if (chash._primary) {
571                 patron.card(card);
572             }
573         });
574
575         var addr_hashes = patron.addresses();
576         patron.addresses([]);
577         angular.forEach(addr_hashes, function(addr_hash) {
578             if (!addr_hash.isnew && !addr_hash.isdeleted) 
579                 addr_hash.ischanged = true;
580             var addr = egCore.idl.fromHash('aua', addr_hash);
581             patron.addresses().push(addr);
582             addr.valid(addr.valid() ? 't' : 'f');
583             addr.within_city_limits(addr.within_city_limits() ? 't' : 'f');
584             if (addr_hash._is_mailing) patron.mailing_address(addr);
585             if (addr_hash._is_billing) patron.billing_address(addr);
586         });
587
588         patron.survey_responses([]);
589         angular.forEach(service.survey_responses, function(answer) {
590             var question = service.survey_questions[answer.question()];
591             var resp = new egCore.idl.asvr();
592             resp.isnew(true);
593             resp.survey(question.survey());
594             resp.question(question.id());
595             resp.answer(answer.id());
596             resp.usr(patron.id());
597             resp.answer_date('now');
598             patron.survey_responses().push(resp);
599         });
600         
601         // re-object-ify the patron stat cat entry maps
602         var maps = [];
603         angular.forEach(patron.stat_cat_entries(), function(entry) {
604             var e = egCore.idl.fromHash('actscecm', entry);
605             e.stat_cat(e.stat_cat().id);
606             maps.push(e);
607         });
608         patron.stat_cat_entries(maps);
609
610         // service.stat_cat_entry_maps maps stats to entries
611         // patron.stat_cat_entries is an array of stat_cat_entry_usr_map's
612         angular.forEach(service.stat_cat_entry_maps, function(entry) {
613
614             // see if we already have a mapping for this entry
615             var existing = patron.stat_cat_entries().filter(function(e) {
616                 return e.stat_cat() == entry.stat_cat();
617             })[0];
618
619             if (existing) { // we have a mapping
620                 // if the existing mapping matches the new one,
621                 // there' nothing left to do
622                 if (existing.stat_cat_entry() == entry.id()) return;
623
624                 // mappings differ.  delete the old one and create
625                 // a new one below.
626                 existing.isdeleted(true);
627             }
628
629             var newmap = new egCore.idl.actscecm();
630             newmap.target_usr(patron.id());
631             newmap.isnew(true);
632             newmap.stat_cat(entry.stat_cat());
633             newmap.stat_cat_entry(entry.id());
634             patron.stat_cat_entries().push(newmap);
635         });
636
637         angular.forEach(patron.stat_cat_entries(), function(entry) {
638             console.log(egCore.idl.toString(entry));
639         });
640
641         if (!patron.isnew()) patron.ischanged(true);
642
643         return egCore.net.request(
644             'open-ils.actor', 
645             'open-ils.actor.patron.update',
646             egCore.auth.token(), patron);
647     }
648
649     service.save_user_settings = function(new_user, user_settings) {
650         // user_settings contains the values from the scope/form.
651         // service.user_settings contain the values from page load time.
652
653         var settings = {};
654         if (service.patron_id) {
655             // only update modified settings for existing patrons
656             angular.forEach(user_settings, function(val, key) {
657                 if (val !== service.user_settings[key])
658                     settings[key] = val;
659             });
660
661         } else {
662             // all non-null setting values are updated for new patrons
663             angular.forEach(user_settings, function(val, key) {
664                 if (val !== null) settings[key] = val;
665             });
666         }
667
668         if (Object.keys(settings).length == 0) return $q.when();
669
670         return egCore.net.request(
671             'open-ils.actor',
672             'open-ils.actor.patron.settings.update',
673             egCore.auth.token(), new_user.id(), settings
674         ).then(function(resp) {
675             console.log('settings returned ' + resp);
676             return resp;
677         });
678     }
679
680     return service;
681 }]);
682
683
684 function PatronRegCtrl($scope, $routeParams, 
685     $q, $modal, $window, egCore, patronSvc, patronRegSvc, egUnloadPrompt) {
686
687     $scope.page_data_loaded = false;
688     $scope.clone_id = $routeParams.clone_id;
689     $scope.stage_username = $routeParams.stage_username;
690     $scope.patron_id = 
691         patronRegSvc.patron_id = $routeParams.edit_id || $routeParams.id;
692
693     // for existing patrons, disable barcode input by default
694     $scope.disable_bc = $scope.focus_usrname = Boolean($scope.patron_id);
695     $scope.focus_bc = !Boolean($scope.patron_id);
696     $scope.dupe_counts = {};
697
698     if (!$scope.edit_passthru) {
699         // in edit more, scope.edit_passthru is delivered to us by
700         // the enclosing controller.  In register mode, there is 
701         // no enclosing controller, so we create our own.
702         $scope.edit_passthru = {};
703     }
704
705     // 0=all, 1=suggested, 2=all
706     $scope.edit_passthru.vis_level = 0; 
707     // TODO: add save/clone handlers here
708
709     // TODO: call attach() on the first instance of a modified value
710     //egUnloadPrompt.attach($scope);
711
712     // Apply default values for new patrons during initial registration
713     // prs is shorthand for patronSvc
714     function set_new_patron_defaults(prs) {
715         $scope.generate_password();
716         $scope.hold_notify_phone = true;
717         $scope.hold_notify_email = true;
718
719         if (prs.org_settings['ui.patron.default_ident_type']) {
720             // $scope.patron needs this field to be an object
721             var id = prs.org_settings['ui.patron.default_ident_type'];
722             var ident_type = $scope.ident_types.filter(
723                 function(type) { return type.id() == id })[0];
724             $scope.patron.ident_type = ident_type;
725         }
726         if (prs.org_settings['ui.patron.default_inet_access_level']) {
727             // $scope.patron needs this field to be an object
728             var id = prs.org_settings['ui.patron.default_inet_access_level'];
729             var level = $scope.net_access_levels.filter(
730                 function(lvl) { return lvl.id() == id })[0];
731             $scope.patron.net_access_level = level;
732         }
733         if (prs.org_settings['ui.patron.default_country']) {
734             $scope.patron.addresses[0].country = 
735                 prs.org_settings['ui.patron.default_country'];
736         }
737     }
738
739     function handle_home_org_changed() {
740         org_id = $scope.patron.home_ou.id();
741
742         patronRegSvc.has_group_link_perms(org_id)
743         .then(function(bool) {$scope.has_group_link_perm = bool});
744     }
745
746     $q.all([
747
748         $scope.initTab ? // initTab comes from patron app
749             $scope.initTab('edit', $routeParams.id) : $q.when(),
750
751         patronRegSvc.init()
752
753     ]).then(function() {
754         // called after initTab and patronRegSvc.init have completed
755
756         var prs = patronRegSvc; // brevity
757         // in standalone mode, we have no patronSvc
758         $scope.patron = prs.init_patron(patronSvc ? patronSvc.current : null);
759         $scope.field_doc = prs.field_doc;
760         $scope.edit_profiles = prs.edit_profiles;
761         $scope.ident_types = prs.ident_types;
762         $scope.net_access_levels = prs.net_access_levels;
763         $scope.user_setting_types = prs.user_setting_types;
764         $scope.opt_in_setting_types = prs.opt_in_setting_types;
765         $scope.org_settings = prs.org_settings;
766         $scope.sms_carriers = prs.sms_carriers;
767         $scope.stat_cats = prs.stat_cats;
768         $scope.surveys = prs.surveys;
769         $scope.survey_responses = prs.survey_responses;
770         $scope.stat_cat_entry_maps = prs.stat_cat_entry_maps;
771
772         $scope.user_settings = prs.user_settings;
773         // clone the user settings back into the patronRegSvc so
774         // we have a copy of the original state of the settings.
775         prs.user_settings = {};
776         angular.forEach($scope.user_settings, function(val, key) {
777             prs.user_settings[key] = val;
778         });
779
780         extract_hold_notify();
781         handle_home_org_changed();
782
783         if ($scope.org_settings['ui.patron.edit.default_suggested'])
784             $scope.edit_passthru.vis_level = 1;
785
786         if ($scope.patron.isnew) 
787             set_new_patron_defaults(prs);
788
789         $scope.page_data_loaded = true;
790     });
791
792     // update the currently displayed field documentation
793     $scope.set_selected_field_doc = function(cls, field) {
794         $scope.selected_field_doc = $scope.field_doc[cls][field];
795     }
796
797     // returns the tree depth of the selected profile group tree node.
798     $scope.pgt_depth = function(grp) {
799         var d = 0;
800         while (grp = egCore.env.pgt.map[grp.parent()]) d++;
801         return d;
802     }
803
804     // IDL fields used for labels in the UI.
805     $scope.idl_fields = {
806         au  : egCore.idl.classes.au.field_map,
807         ac  : egCore.idl.classes.ac.field_map,
808         aua : egCore.idl.classes.aua.field_map
809     };
810
811     // field visibility cache.  Some fields are universally required.
812     var field_visibility = {
813         'ac.barcode' : 2,
814         'au.usrname' : 2,
815         'au.passwd' :  2,
816         // TODO passwd2 2,
817         'au.first_given_name' : 2,
818         'au.family_name' : 2,
819         'au.ident_type' : 2,
820         'au.home_ou' : 2,
821         'au.profile' : 2,
822         'au.expire_date' : 2,
823         'au.net_access_level' : 2,
824         'aua.address_type' : 2,
825         'aua.post_code' : 2,
826         'aua.street1' : 2,
827         'aua.street2' : 2,
828         'aua.city' : 2,
829         'aua.county' : 2,
830         'aua.state' : 2,
831         'aua.country' : 2,
832         'aua.valid' : 2,
833         'aua.within_city_limits' : 2,
834         'stat_cats' : 1,
835         'surveys' : 1
836     }; 
837
838     // returns true if the selected field should be visible
839     // given the current required/suggested/all setting.
840     $scope.show_field = function(field_key) {
841
842         if (field_visibility[field_key] == undefined) {
843             // compile and cache the visibility for the selected field
844
845             // org settings have not been received yet.
846             if (!$scope.org_settings) return false;
847
848             var req_set = 'ui.patron.edit.' + field_key + '.require';
849             var sho_set = 'ui.patron.edit.' + field_key + '.show';
850             var sug_set = 'ui.patron.edit.' + field_key + '.suggest';
851
852             if ($scope.org_settings[req_set]) {
853                 field_visibility[field_key] = 2;
854             } else if ($scope.org_settings[sho_set]) {
855                 field_visibility[field_key] = 2;
856             } else if ($scope.org_settings[sug_set]) {
857                 field_visibility[field_key] = 1;
858             } else {
859                 field_visibility[field_key] = 0;
860             }
861         }
862
863         return field_visibility[field_key] >= $scope.edit_passthru.vis_level;
864     }
865
866     // generates a random 4-digit password
867     $scope.generate_password = function() {
868         $scope.patron.passwd = Math.floor(Math.random()*9000) + 1000;
869     }
870
871     $scope.set_expire_date = function() {
872         if (!$scope.patron.profile) return;
873         var seconds = egCore.date.intervalToSeconds(
874             $scope.patron.profile.perm_interval());
875         var now_epoch = new Date().getTime();
876         $scope.patron.expire_date = new Date(
877             now_epoch + (seconds * 1000 /* milliseconds */))
878     }
879
880     // grp is the pgt object
881     $scope.set_profile = function(grp) {
882         $scope.patron.profile = grp;
883         $scope.set_expire_date();
884     }
885
886     $scope.new_address = function() {
887         var addr = egCore.idl.toHash(new egCore.idl.aua());
888         patronRegSvc.ingest_address($scope.patron, addr);
889         addr.id = patronRegSvc.virt_id--;
890         addr.isnew = true;
891         addr.valid = true;
892         addr.within_city_limits = true;
893         $scope.patron.addresses.push(addr);
894     }
895
896     // keep deleted addresses out of the patron object so
897     // they won't appear in the UI.  They'll be re-inserted
898     // when the patron is updated.
899     deleted_addresses = [];
900     $scope.delete_address = function(id) {
901         var addresses = [];
902         angular.forEach($scope.patron.addresses, function(addr) {
903             if (addr.id == id) {
904                 if (id > 0) {
905                     addr.isdeleted = true;
906                     deleted_addresses.push(addr);
907                 }
908             } else {
909                 addresses.push(addr);
910             }
911         });
912         $scope.patron.addresses = addresses;
913     } 
914
915     $scope.post_code_changed = function(addr) { 
916         egCore.net.request(
917             'open-ils.search', 'open-ils.search.zip', addr.post_code)
918         .then(function(resp) {
919             if (!resp) return;
920             if (resp.city) addr.city = resp.city;
921             if (resp.state) addr.state = resp.state;
922             if (resp.county) addr.county = resp.county;
923             if (resp.alert) alert(resp.alert);
924         });
925     }
926
927     $scope.replace_card = function() {
928         $scope.patron.card.active = false;
929         $scope.patron.card.ischanged = true;
930         $scope.disable_bc = false;
931
932         var new_card = egCore.idl.toHash(new egCore.idl.ac());
933         new_card.id = patronRegSvc.virt_id--;
934         new_card.isnew = true;
935         new_card.active = true;
936         new_card._primary = 'on';
937         $scope.patron.card = new_card;
938         $scope.patron.cards.push(new_card);
939     }
940
941     $scope.day_phone_changed = function(phone) {
942         if (phone && $scope.org_settings['patron.password.use_phone']) {
943            $scope.patron.passwd = phone.substr(-4);
944         }
945     }
946
947     $scope.barcode_changed = function(bc) {
948         if (!bc) return;
949         egCore.net.request(
950             'open-ils.actor',
951             'open-ils.actor.barcode.exists',
952             egCore.auth.token(), bc
953         ).then(function(resp) {
954             if (resp == '1') {
955                 console.log('duplicate barcode detected: ' + bc);
956                 // DUPLICATE CARD
957             } else {
958                 if (!$scope.patron.usrname)
959                     $scope.patron.usrname = bc;
960                 // No dupe -- A-OK
961             }
962         });
963     }
964
965     $scope.cards_dialog = function() {
966         $modal.open({
967             templateUrl: './circ/patron/t_patron_cards_dialog',
968             controller: 
969                    ['$scope','$modalInstance','cards',
970             function($scope , $modalInstance , cards) {
971                 // scope here is the modal-level scope
972                 $scope.args = {cards : cards};
973                 $scope.ok = function() { $modalInstance.close($scope.args) }
974                 $scope.cancel = function () { $modalInstance.dismiss() }
975             }],
976             resolve : {
977                 cards : function() {
978                     // scope here is the controller-level scope
979                     return $scope.patron.cards;
980                 }
981             }
982         }).result.then(
983             function(args) {
984                 angular.forEach(args.cards, function(card) {
985                     card.ischanged = true; // assume cards need updating, OK?
986                     if (card._primary == 'on' && 
987                         card.id != $scope.patron.card.id) {
988                         $scope.patron.card = card;
989                     }
990                 });
991             }
992         );
993     }
994
995     $scope.set_addr_type = function(addr, type) {
996         var addrs = $scope.patron.addresses;
997         if (addr['_is_'+type]) {
998             angular.forEach(addrs, function(a) {
999                 if (a.id != addr.id) a['_is_'+type] = false;
1000             });
1001         } else {
1002             // unchecking mailing/billing means we have to randomly
1003             // select another address to fill that role.  Select the
1004             // first address in the list (that does not match the
1005             // modifed address)
1006             for (var i = 0; i < addrs.length; i++) {
1007                 if (addrs[i].id != addr.id) {
1008                     addrs[i]['_is_' + type] = true;
1009                     break;
1010                 }
1011             }
1012         }
1013     }
1014
1015
1016     // Translate hold notify preferences from the form/scope back into a 
1017     // single user setting value for opac.hold_notify.
1018     function compress_hold_notify() {
1019         var hold_notify = '';
1020         var splitter = '';
1021         if ($scope.hold_notify_phone) {
1022             hold_notify = 'phone';
1023             splitter = ':';
1024         }
1025         if ($scope.hold_notify_email) {
1026             hold_notify = splitter + 'email';
1027             splitter = ':';
1028         }
1029         if ($scope.hold_notify_sms) {
1030             hold_notify = splitter + 'sms';
1031             splitter = ':';
1032         }
1033         $scope.user_settings['opac.hold_notify'] = hold_notify;
1034     }
1035
1036     // dialog for selecting additional permission groups
1037     $scope.secondary_groups_dialog = function() {
1038         $modal.open({
1039             templateUrl: './circ/patron/t_patron_groups_dialog',
1040             controller: 
1041                    ['$scope','$modalInstance','linked_groups','pgt_depth',
1042             function($scope , $modalInstance , linked_groups , pgt_depth) {
1043
1044                 $scope.pgt_depth = pgt_depth;
1045                 $scope.args = {
1046                     linked_groups : linked_groups,
1047                     edit_profiles : patronRegSvc.edit_profiles,
1048                     new_profile   : patronRegSvc.edit_profiles[0]
1049                 };
1050
1051                 // add a new group to the linked groups list
1052                 $scope.link_group = function($event, grp) {
1053                     var found = false; // avoid duplicates
1054                     angular.forEach($scope.args.linked_groups, 
1055                         function(g) {if (g.id() == grp.id()) found = true});
1056                     if (!found) $scope.args.linked_groups.push(grp);
1057                     $event.preventDefault(); // avoid close
1058                 }
1059
1060                 // remove a group from the linked groups list
1061                 $scope.unlink_group = function($event, grp) {
1062                     $scope.args.linked_groups = 
1063                         $scope.args.linked_groups.filter(function(g) {
1064                         return g.id() != grp.id()
1065                     });
1066                     $event.preventDefault(); // avoid close
1067                 }
1068
1069                 $scope.ok = function() { $modalInstance.close($scope.args) }
1070                 $scope.cancel = function () { $modalInstance.dismiss() }
1071             }],
1072             resolve : {
1073                 linked_groups : function() { return $scope.patron.groups },
1074                 pgt_depth : function() { return $scope.pgt_depth }
1075             }
1076         }).result.then(
1077             function(args) {
1078                 var ids = args.linked_groups.map(function(g) {return g.id()});
1079                 console.log('linking permission groups ' + ids);
1080                 return egCore.net.request(
1081                     'open-ils.actor',
1082                     'open-ils.actor.user.set_groups',
1083                     egCore.auth.token(), $scope.patron.id, ids)
1084                 .then(function(resp) {
1085                     if (resp == 1) {
1086                         $scope.patron.groups = args.linked_groups;
1087                     } else {
1088                         // debugging -- should be no events
1089                         alert('linked groups failure ' + egCore.evt.parse(resp));
1090                     }
1091                 });
1092             }
1093         );
1094     }
1095
1096     function extract_hold_notify() {
1097         notify = $scope.user_settings['opac.hold_notify'];
1098         if (!notify) return;
1099         $scope.hold_notify_phone = Boolean(notify.match(/phone/));
1100         $scope.hold_notify_email = Boolean(notify.match(/email/));
1101         $scope.hold_notify_sms = Boolean(notify.match(/sms/));
1102     }
1103
1104     $scope.invalidate_field = function(field) {
1105         patronRegSvc.invalidate_field($scope.patron, field);
1106     }
1107
1108     $scope.dupe_value_changed = function(type, value) {
1109         $scope.dupe_counts[type] = 0;
1110         patronRegSvc.dupe_patron_search($scope.patron, type, value)
1111         .then(function(res) {
1112             $scope.dupe_counts[type] = res.count;
1113             $scope.dupe_search_encoded = 
1114                 encodeURIComponent(js2JSON(res.search));
1115         });
1116     }
1117
1118     $scope.edit_passthru.save = function() {
1119
1120         // toss the deleted addresses back into the patron's list of
1121         // addresses so it's included in the update
1122         $scope.patron.addresses = 
1123             $scope.patron.addresses.concat(deleted_addresses);
1124         
1125         compress_hold_notify();
1126
1127         patronRegSvc.save_user($scope.patron)
1128         .then(function(new_user) { 
1129             if (new_user && new_user.classname) {
1130                 return patronRegSvc.save_user_settings(
1131                     new_user, $scope.user_settings); 
1132             } else {
1133                 alert('Patron update failed. \n\n' + js2JSON(new_user));
1134                 return true; // ensure page reloads to reset
1135             }
1136         }).then(function(keep_going) {
1137             // reloading the page means potentially losing some information
1138             // (e.g. last patron search), but is the only way to ensure all
1139             // components are properly updated to reflect the modified patron.
1140             $window.location.href = location.href;
1141         });
1142     }
1143 }
1144
1145 // This controller may be loaded from different modules (patron edit vs.
1146 // register new patron), so we have to inject the controller params manually.
1147 PatronRegCtrl.$inject = ['$scope', '$routeParams', '$q', '$modal', 
1148     '$window', 'egCore', 'patronSvc', 'patronRegSvc', 'egUnloadPrompt'];
1149