LP#1452950 Patron reg pending users
[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_clone_user(),
45             service.get_stage_user(),
46             service.get_net_access_levels()
47         ]);
48     };
49
50     service.get_clone_user = function() {
51         if (!service.clone_id) return $q.when();
52         // we could load egUser and use its get() function, but loading
53         // user.js into the standalone register UI would mean creating a
54         // new module, since egUser is not loaded into egCoreMod.  This
55         // is a lot simpler.
56         return egCore.net.request(
57             'open-ils.actor',
58             'open-ils.actor.user.fleshed.retrieve',
59             egCore.auth.token(), service.clone_id, 
60             ['billing_address', 'mailing_address'])
61         .then(function(cuser) {
62             if (e = egCore.evt.parse(cuser)) {
63                 alert(e);
64             } else {
65                 service.clone_user = cuser;
66             }
67         });
68     }
69
70     service.get_stage_user = function() {
71         if (!service.stage_username) return $q.when();
72
73         // fetch the staged user object
74         return egCore.net.request(
75             'open-ils.actor',
76             'open-ils.actor.user.stage.retrieve.by_username',
77             egCore.auth.token(), 
78             service.stage_username
79         ).then(function(suser) {
80             if (e = egCore.evt.parse(suser)) {
81                 alert(e);
82             } else {
83                 service.stage_user = suser;
84             }
85         }).then(function() {
86
87             if (!service.stage_user) return;
88             var requestor = service.stage_user.user.requesting_usr();
89
90             if (!requestor) return;
91
92             // fetch the requesting user
93             return egCore.net.request(
94                 'open-ils.actor', 
95                 'open-ils.actor.user.retrieve.parts',
96                 egCore.auth.token(),
97                 requestor, 
98                 ['family_name', 'first_given_name', 'second_given_name'] 
99             ).then(function(parts) {
100                 service.stage_user_requestor = 
101                     service.format_name(parts[0], parts[1], parts[2]);
102             })
103         });
104     }
105
106     // See note above about not loading egUser.
107     // TODO: i18n
108     service.format_name = function(last, first, middle) {
109         return last + ', ' + first + (middle ? ' ' + middle : '');
110     }
111
112     //service.check_grp_app_perm = function(grp_id) {
113
114     // determine which user groups our user is not allowed to modify
115     service.set_edit_profiles = function() {
116         var all_app_perms = [];
117         var failed_perms = [];
118
119         // extract the application permissions
120         angular.forEach(service.profiles, function(grp) {
121             if (grp.application_perm())
122                 all_app_perms.push(grp.application_perm());
123         }); 
124
125         // fill in service.edit_profiles by inspecting failed_perms
126         function traverse_grp_tree(grp, failed) {
127             failed = failed || 
128                 failed_perms.indexOf(grp.application_perm()) > -1;
129
130             if (!failed) service.edit_profiles.push(grp);
131
132             angular.forEach(
133                 service.profiles.filter( // children of grp
134                     function(p) { return p.parent() == grp.id() }),
135                 function(child) {traverse_grp_tree(child, failed)}
136             );
137         }
138
139         return egCore.perm.hasPermAt(all_app_perms, true).then(
140             function(perm_orgs) {
141                 angular.forEach(all_app_perms, function(p) {
142                     if (perm_orgs[p].length == 0)
143                         failed_perms.push(p);
144                 });
145
146                 traverse_grp_tree(egCore.env.pgt.tree);
147             }
148         );
149     }
150
151     service.has_group_link_perms = function(org_id) {
152         return egCore.perm.hasPermAt('CREATE_USER_GROUP_LINK', true)
153         .then(function(p) { return p.indexOf(org_id) > -1; });
154     }
155
156     service.get_surveys = function() {
157         var org_ids = egCore.org.fullPath(egCore.auth.user().ws_ou(), true);
158
159         return egCore.pcrud.search('asv', {
160                 owner : org_ids,
161                 start_date : {'<=' : 'now'},
162                 end_date : {'>=' : 'now'}
163             }, {   
164                 flesh : 2, 
165                 flesh_fields : {
166                     asv : ['questions'], 
167                     asvq : ['answers']
168                 }
169             }, 
170             {atomic : true}
171         ).then(function(surveys) {
172             surveys = surveys.sort(function(a,b) {
173                 return a.name() < b.name() ? -1 : 1 });
174             service.surveys = surveys;
175             angular.forEach(surveys, function(survey) {
176                 angular.forEach(survey.questions(), function(question) {
177                     service.survey_questions[question.id()] = question;
178                     angular.forEach(question.answers(), function(answer) {
179                         service.survey_answers[answer.id()] = answer;
180                     });
181                 });
182             });
183         });
184     }
185
186     service.get_stat_cats = function() {
187         return egCore.net.request(
188             'open-ils.circ',
189             'open-ils.circ.stat_cat.actor.retrieve.all',
190             egCore.auth.token(), egCore.auth.user().ws_ou()
191         ).then(function(cats) {
192             cats = cats.sort(function(a, b) {
193                 return a.name() < b.name() ? -1 : 1});
194             angular.forEach(cats, function(cat) {
195                 cat.entries(
196                     cat.entries().sort(function(a,b) {
197                         return a.value() < b.value() ? -1 : 1
198                     })
199                 );
200             });
201             service.stat_cats = cats;
202         });
203     };
204
205     service.get_org_settings = function() {
206         return egCore.org.settings([
207             'global.password_regex',
208             'global.juvenile_age_threshold',
209             'patron.password.use_phone',
210             'ui.patron.default_inet_access_level',
211             'ui.patron.default_ident_type',
212             'ui.patron.default_country',
213             'ui.patron.registration.require_address',
214             'circ.holds.behind_desk_pickup_supported',
215             'circ.patron_edit.clone.copy_address',
216             'ui.patron.edit.au.prefix.require',
217             'ui.patron.edit.au.prefix.show',
218             'ui.patron.edit.au.prefix.suggest',
219             'ui.patron.edit.ac.barcode.regex',
220             'ui.patron.edit.au.second_given_name.show',
221             'ui.patron.edit.au.second_given_name.suggest',
222             'ui.patron.edit.au.suffix.show',
223             'ui.patron.edit.au.suffix.suggest',
224             'ui.patron.edit.au.alias.show',
225             'ui.patron.edit.au.alias.suggest',
226             'ui.patron.edit.au.dob.require',
227             'ui.patron.edit.au.dob.show',
228             'ui.patron.edit.au.dob.suggest',
229             'ui.patron.edit.au.dob.calendar',
230             'ui.patron.edit.au.juvenile.show',
231             'ui.patron.edit.au.juvenile.suggest',
232             'ui.patron.edit.au.ident_value.show',
233             'ui.patron.edit.au.ident_value.suggest',
234             'ui.patron.edit.au.ident_value2.show',
235             'ui.patron.edit.au.ident_value2.suggest',
236             'ui.patron.edit.au.email.require',
237             'ui.patron.edit.au.email.show',
238             'ui.patron.edit.au.email.suggest',
239             'ui.patron.edit.au.email.regex',
240             'ui.patron.edit.au.email.example',
241             'ui.patron.edit.au.day_phone.require',
242             'ui.patron.edit.au.day_phone.show',
243             'ui.patron.edit.au.day_phone.suggest',
244             'ui.patron.edit.au.day_phone.regex',
245             'ui.patron.edit.au.day_phone.example',
246             'ui.patron.edit.au.evening_phone.require',
247             'ui.patron.edit.au.evening_phone.show',
248             'ui.patron.edit.au.evening_phone.suggest',
249             'ui.patron.edit.au.evening_phone.regex',
250             'ui.patron.edit.au.evening_phone.example',
251             'ui.patron.edit.au.other_phone.require',
252             'ui.patron.edit.au.other_phone.show',
253             'ui.patron.edit.au.other_phone.suggest',
254             'ui.patron.edit.au.other_phone.regex',
255             'ui.patron.edit.au.other_phone.example',
256             'ui.patron.edit.phone.regex',
257             'ui.patron.edit.phone.example',
258             'ui.patron.edit.au.active.show',
259             'ui.patron.edit.au.active.suggest',
260             'ui.patron.edit.au.barred.show',
261             'ui.patron.edit.au.barred.suggest',
262             'ui.patron.edit.au.master_account.show',
263             'ui.patron.edit.au.master_account.suggest',
264             'ui.patron.edit.au.claims_returned_count.show',
265             'ui.patron.edit.au.claims_returned_count.suggest',
266             'ui.patron.edit.au.claims_never_checked_out_count.show',
267             'ui.patron.edit.au.claims_never_checked_out_count.suggest',
268             'ui.patron.edit.au.alert_message.show',
269             'ui.patron.edit.au.alert_message.suggest',
270             'ui.patron.edit.aua.post_code.regex',
271             'ui.patron.edit.aua.post_code.example',
272             'ui.patron.edit.aua.county.require',
273             'format.date',
274             'ui.patron.edit.default_suggested',
275             'opac.barcode_regex',
276             'opac.username_regex',
277             'sms.enable',
278             'ui.patron.edit.aua.state.require',
279             'ui.patron.edit.aua.state.suggest',
280             'ui.patron.edit.aua.state.show'
281         ]).then(function(settings) {
282             service.org_settings = settings;
283             return service.process_org_settings(settings);
284         });
285     };
286
287     // some org settings require the retrieval of additional data
288     service.process_org_settings = function(settings) {
289
290         var promises = [];
291
292         if (settings['sms.enable']) {
293             // fetch SMS carriers
294             promises.push(
295                 egCore.pcrud.search('csc', 
296                     {active: 'true'}, 
297                     {'order_by':[
298                         {'class':'csc', 'field':'name'},
299                         {'class':'csc', 'field':'region'}
300                     ]}, {atomic : true}
301                 ).then(function(carriers) {
302                     service.sms_carriers = carriers;
303                 })
304             );
305         } else {
306             // if other promises are added below, this is not necessary.
307             promises.push($q.when());  
308         }
309
310         // other post-org-settings processing goes here,
311         // adding to promises as needed.
312
313         return $q.all(promises);
314     };
315
316     service.get_ident_types = function() {
317         if (egCore.env.cit) {
318             service.ident_types = egCore.env.cit.list;
319             return $q.when();
320         } else {
321             return egCore.pcrud.retrieveAll('cit', {}, {atomic : true})
322             .then(function(types) { 
323                 egCore.env.absorbList(types, 'cit')
324                 service.ident_types = types 
325             });
326         }
327     };
328
329     service.get_net_access_levels = function() {
330         if (egCore.env.cnal) {
331             service.net_access_levels = egCore.env.cnal.list;
332             return $q.when();
333         } else {
334             return egCore.pcrud.retrieveAll('cnal', {}, {atomic : true})
335             .then(function(levels) { 
336                 egCore.env.absorbList(levels, 'cnal')
337                 service.net_access_levels = levels 
338             });
339         }
340     }
341
342     service.get_perm_groups = function() {
343         if (egCore.env.pgt) {
344             service.profiles = egCore.env.pgt.list;
345             return service.set_edit_profiles();
346         } else {
347             return egCore.pcrud.search('pgt', {parent : null}, 
348                 {flesh : -1, flesh_fields : {pgt : ['children']}}
349             ).then(
350                 function(tree) {
351                     egCore.env.absorbTree(tree, 'pgt')
352                     service.profiles = egCore.env.pgt.list;
353                     return service.set_edit_profiles();
354                 }
355             );
356         }
357     }
358
359     service.get_field_doc = function() {
360         return egCore.pcrud.search('fdoc', {
361             fm_class: ['au', 'ac', 'aua', 'actsc', 'asv', 'asvq', 'asva']})
362         .then(null, null, function(doc) {
363             if (!service.field_doc[doc.fm_class()]) {
364                 service.field_doc[doc.fm_class()] = {};
365             }
366             service.field_doc[doc.fm_class()][doc.field()] = doc;
367         });
368     };
369
370     service.get_user_settings = function() {
371         var org_ids = egCore.org.ancestors(egCore.auth.user().ws_ou(), true);
372
373         var static_types = [
374             'circ.holds_behind_desk', 
375             'circ.collections.exempt', 
376             'opac.hold_notify', 
377             'opac.default_phone', 
378             'opac.default_pickup_location', 
379             'opac.default_sms_carrier', 
380             'opac.default_sms_notify'];
381
382         return egCore.pcrud.search('cust', {
383             '-or' : [
384                 {name : static_types}, // common user settings
385                 {name : { // opt-in notification user settings
386                     'in': {
387                         select : {atevdef : ['opt_in_setting']}, 
388                         from : 'atevdef',
389                         // we only care about opt-in settings for 
390                         // event_defs our users encounter
391                         where : {'+atevdef' : {owner : org_ids}}
392                     }
393                 }}
394             ]
395         }, {}, {atomic : true}).then(function(setting_types) {
396
397             angular.forEach(setting_types, function(stype) {
398                 service.user_setting_types[stype.name()] = stype;
399                 if (static_types.indexOf(stype.name()) == -1) {
400                     service.opt_in_setting_types[stype.name()] = stype;
401                 }
402             });
403
404             if (service.patron_id) {
405                 // retrieve applied values for the current user 
406                 // for the setting types we care about.
407
408                 var setting_names = 
409                     setting_types.map(function(obj) { return obj.name() });
410
411                 return egCore.net.request(
412                     'open-ils.actor', 
413                     'open-ils.actor.patron.settings.retrieve.authoritative',
414                     egCore.auth.token(),
415                     service.patron_id,
416                     setting_names
417                 ).then(function(settings) {
418                     service.user_settings = settings;
419                 });
420             } else {
421
422                 // apply default user setting values
423                 angular.forEach(setting_types, function(stype, index) {
424                     if (stype.reg_default() != undefined) {
425                         service.user_settings[setting.name()] = 
426                             setting.reg_default();
427                     }
428                 });
429             }
430         });
431     }
432
433     service.invalidate_field = function(patron, field) {
434         console.log('Invalidating patron field ' + field);
435
436         return egCore.net.request(
437             'open-ils.actor',
438             'open-ils.actor.invalidate.' + field,
439             egCore.auth.token(), patron.id, null, patron.home_ou.id()
440
441         ).then(function(res) {
442             // clear the invalid value from the form
443             patron[field] = '';
444
445             // update last_xact_id so future save operations
446             // on this patron will be allowed
447             patron.last_xact_id = res.payload.last_xact_id[patron.id];
448         });
449     }
450
451     service.dupe_patron_search = function(patron, type, value) {
452         var search;
453
454         console.log('Dupe search called with "' + 
455             type +"' and value " + value);
456
457         switch (type) {
458
459             case 'name':
460                 var fname = patron.first_given_name;   
461                 var lname = patron.family_name;   
462                 if (!(fname && lname)) return;
463                 search = {
464                     first_given_name : {value : fname, group : 0},
465                     family_name : {value : lname, group : 0}
466                 };
467                 break;
468
469             case 'email':
470                 search = {email : {value : value, group : 0}};
471                 break;
472
473             case 'ident':
474                 search = {ident : {value : value, group : 2}};
475                 break;
476
477             case 'phone':
478                 search = {phone : {value : value, group : 2}};
479                 break;
480
481             case 'address':
482                 search = {};
483                 angular.forEach(['street1', 'street2', 'city', 'post_code'],
484                     function(field) {
485                         if(value[field])
486                             search[field] = {value : value[field], group: 1};
487                     }
488                 );
489                 break;
490         }
491
492         return egCore.net.request( 
493             'open-ils.actor', 
494             'open-ils.actor.patron.search.advanced',
495             egCore.auth.token(), search, null, null, 1
496         ).then(function(res) {
497             res = res.filter(function(id) {return id != patron.id});
498             return {
499                 count : res.length,
500                 search : search
501             };
502         });
503     }
504
505     service.init_patron = function(current) {
506
507         if (!current)
508             return service.init_new_patron();
509
510         service.patron = current;
511         return service.init_existing_patron(current)
512     }
513
514     service.ingest_address = function(patron, addr) {
515         addr.valid = addr.valid == 't';
516         addr.within_city_limits = addr.within_city_limits == 't';
517         addr._is_mailing = (patron.mailing_address && 
518             addr.id == patron.mailing_address.id);
519         addr._is_billing = (patron.billing_address && 
520             addr.id == patron.billing_address.id);
521     }
522
523     /*
524      * Existing patron objects reqire some data munging before insertion
525      * into the scope.
526      *
527      * 1. Turn everything into a hash
528      * 2. ... Except certain fields (selectors) whose widgets require objects
529      * 3. Bools must be Boolean, not t/f.
530      */
531     service.init_existing_patron = function(current) {
532
533         var patron = egCore.idl.toHash(current);
534
535         patron.home_ou = egCore.org.get(patron.home_ou.id);
536         patron.expire_date = new Date(Date.parse(patron.expire_date));
537         patron.dob = service.parse_dob(patron.dob);
538         patron.profile = current.profile(); // pre-hash version
539         patron.net_access_level = current.net_access_level();
540         patron.ident_type = current.ident_type();
541         patron.groups = current.groups(); // pre-hash
542
543         angular.forEach(
544             ['juvenile', 'barred', 'active', 'master_account'],
545             function(field) { patron[field] = patron[field] == 't'; }
546         );
547
548         angular.forEach(patron.cards, function(card) {
549             card.active = card.active == 't';
550             if (card.id == patron.card.id) {
551                 patron.card = card;
552                 card._primary = 'on';
553             }
554         });
555
556         angular.forEach(patron.addresses, 
557             function(addr) { service.ingest_address(patron, addr) });
558
559         // toss entries for existing stat cat maps into our living 
560         // stat cat entry map, which is modified within the template.
561         angular.forEach(patron.stat_cat_entries, function(map) {
562             service.stat_cat_entry_maps[map.stat_cat.id] = map.stat_cat_entry;
563         });
564
565         return patron;
566     }
567
568     service.init_new_patron = function() {
569         var addr = {
570             id : service.virt_id--,
571             isnew : true,
572             valid : true,
573             address_type : egCore.strings.REG_ADDR_TYPE,
574             _is_mailing : true,
575             _is_billing : true,
576             within_city_limits : false,
577             stat_cat_entries : []
578         };
579
580         var card = {
581             id : service.virt_id--,
582             isnew : true,
583             active : true,
584             _primary : 'on'
585         };
586
587         var user = {
588             isnew : true,
589             active : true,
590             card : card,
591             cards : [card],
592             home_ou : egCore.org.get(egCore.auth.user().ws_ou()),
593             addresses : [addr]
594         };
595
596         if (service.clone_user)
597             service.copy_clone_data(user);
598
599         if (service.stage_user)
600             service.copy_stage_data(user);
601
602         return user;
603     }
604
605     // dob is always YYYY-MM-DD
606     // Dates of birth do not contain timezone info, which can lead to
607     // inconcistent timezone handling, potentially representing
608     // different points in time, depending on the implementation.
609     // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse
610     // See "Differences in assumed time zone"
611     // TODO: move this into egDate ?
612     service.parse_dob = function(dob) {
613         if (!dob) return null;
614         var parts = dob.split('-');
615         var d = new Date(); // always local time zone, yay.
616         d.setFullYear(parts[0]);
617         d.setMonth(parts[1]);
618         d.setDate(parts[2]);
619         return d;
620     }
621
622     service.copy_stage_data = function(user) {
623         var cuser = service.stage_user;
624
625         // copy the data into our new user object
626
627         for (var key in egCore.idl.classes.stgu.field_map) {
628             if (egCore.idl.classes.au.field_map[key] &&
629                 !egCore.idl.classes.stgu.field_map[key].virtual) {
630                 if (cuser.user[key]() !== null)
631                     user[key] = cuser.user[key]();
632             }
633         }
634
635         if (user.home_ou) user.home_ou = egCore.org.get(user.home_ou);
636         if (user.profile) user.profile = egCore.env.pgt.map[user.profile];
637         if (user.ident_type) 
638             user.ident_type = egCore.env.cit.map[user.ident_type];
639         user.dob = service.parse_dob(user.dob);
640
641         // Clear the usrname if it looks like a UUID
642         if (user.usrname.replace(/-/g,'').match(/[0-9a-f]{32}/)) 
643             user.usrname = '';
644
645         // Don't use stub address if we have one from the staged user.
646         if (cuser.mailing_addresses.length || cuser.billing_addresses.length)
647             user.addresses = [];
648
649         // is_mailing=false implies is_billing
650         function addr_from_stage(stage_addr) {
651             if (!stage_addr) return;
652             var cls = stage_addr.classname;
653
654             var addr = {
655                 id : service.virt_id--,
656                 usr : user.id,
657                 isnew : true,
658                 valid : true,
659                 _is_mailing : cls == 'stgma',
660                 _is_billing : cls == 'stgba'
661             };
662
663             user.mailing_address = addr;
664             user.addresses.push(addr);
665
666             for (var key in egCore.idl.classes[cls].field_map) {
667                 if (egCore.idl.classes.aua.field_map[key] &&
668                     !egCore.idl.classes[cls].field_map[key].virtual) {
669                     if (stage_addr[key]() !== null)
670                         addr[key] = stage_addr[key]();
671                 }
672             }
673         }
674
675         addr_from_stage(cuser.mailing_addresses[0]);
676         addr_from_stage(cuser.billing_addresses[0]);
677
678         if (user.addresses.length == 1) {
679             // If there is only one address, 
680             // use it as both mailing and billing.
681             var addr = user.addresses[0];
682             addr._is_mailing = addr._is_billing = true;
683             user.mailing_address = user.billing_address = addr;
684         }
685
686         if (cuser.cards.length) {
687             user.card = {
688                 id : service.virt_id--,
689                 barcode : cuser.cards[0].barcode(),
690                 isnew : true,
691                 active : true,
692                 _primary : 'on'
693             };
694
695             user.cards.push(user.card);
696             if (user.usrname == '') 
697                 user.usrname = card.barcode;
698         }
699     }
700
701     // copy select values from the cloned user to the new user.
702     // user is a hash
703     service.copy_clone_data = function(user) {
704         var clone_user = service.clone_user;
705
706         // flesh the home org locally
707         user.home_ou = egCore.org.get(clone_user.home_ou());
708         if (user.profile) user.profile = egCore.env.pgt.map[user.profile];
709
710         if (!clone_user.billing_address() &&
711             !clone_user.mailing_address())
712             return; // no addresses to copy or link
713
714         // if the cloned user has any addresses, we don't need 
715         // the stub address created in init_new_patron.
716         user.addresses = [];
717
718         var copy_addresses = 
719             service.org_settings['circ.patron_edit.clone.copy_address'];
720
721         var clone_fields = [
722             'day_phone',
723             'evening_phone',
724             'other_phone',
725             'usrgroup'
726         ]; 
727
728         angular.forEach(clone_fields, function(field) {
729             user[field] = clone_user[field]();
730         });
731
732         if (copy_addresses) {
733             var bill_addr, mail_addr;
734
735             // copy the billing and mailing addresses into new addresses
736             function clone_addr(addr) {
737                 var new_addr = egCore.idl.toHash(addr);
738                 new_addr.id = service.virt_id--;
739                 new_addr.usr = user.id;
740                 new_addr.isnew = true;
741                 new_addr.valid = true;
742                 user.addresses.push(new_addr);
743                 return new_addr;
744             }
745
746             if (bill_addr = clone_user.billing_address()) {
747                 var addr = clone_addr(bill_addr);
748                 addr._is_billing = true;
749                 user.billing_address = addr;
750             }
751
752             if (mail_addr = clone_user.mailing_address()) {
753
754                 if (bill_addr && bill_addr.id() == mail_addr.id()) {
755                     user.mailing_address = user.billing_address;
756                     user.mailing_address._is_mailing = true;
757                 } else {
758                     var addr = clone_addr(mail_addr);
759                     addr._is_mailing = true;
760                     user.mailing_address = addr;
761                 }
762
763                 if (!bill_addr) {
764                     // if there is no billing addr, use the mailing addr
765                     user.billing_address = user.mailing_address;
766                     user.billing_address._is_billing = true;
767                 }
768             }
769
770
771         } else {
772
773             // link the billing and mailing addresses
774             var addr;
775             if (addr = clone_user.billing_address()) {
776                 user.billing_address = egCore.idl.toHash(addr);
777                 user.billing_address._is_billing = true;
778                 user.addresses.push(user.billing_address);
779                 user.billing_address._linked_owner_id = clone_user.id();
780                 user.billing_address._linked_owner = service.format_name(
781                     clone_user.family_name(),
782                     clone_user.first_given_name(),
783                     clone_user.second_given_name()
784                 );
785             }
786
787             if (addr = clone_user.mailing_address()) {
788                 if (user.billing_address && 
789                     addr.id() == user.billing_address.id) {
790                     // mailing matches billing
791                     user.mailing_address = user.billing_address;
792                     user.mailing_address._is_mailing = true;
793                 } else {
794                     user.mailing_address = egCore.idl.toHash(addr);
795                     user.mailing_address._is_mailing = true;
796                     user.addresses.push(user.mailing_address);
797                     user.mailing_address._linked_owner_id = clone_user.id();
798                     user.mailing_address._linked_owner = service.format_name(
799                         clone_user.family_name(),
800                         clone_user.first_given_name(),
801                         clone_user.second_given_name()
802                     );
803                 }
804             }
805         }
806     }
807
808     // translate the patron back into IDL form
809     service.save_user = function(phash) {
810
811         var patron = egCore.idl.fromHash('au', phash);
812
813         patron.home_ou(patron.home_ou().id());
814         patron.expire_date(patron.expire_date().toISOString());
815         patron.profile(patron.profile().id());
816         if (patron.dob()) 
817             patron.dob(patron.dob().toISOString().replace(/T.*/,''));
818         if (patron.ident_type()) 
819             patron.ident_type(patron.ident_type().id());
820         if (patron.net_access_level())
821             patron.net_access_level(patron.net_access_level().id());
822
823         angular.forEach(
824             ['juvenile', 'barred', 'active', 'master_account'],
825             function(field) { patron[field](phash[field] ? 't' : 'f'); }
826         );
827
828         var card_hashes = patron.cards();
829         patron.cards([]);
830         angular.forEach(card_hashes, function(chash) {
831             var card = egCore.idl.fromHash('ac', chash)
832             card.usr(patron.id());
833             card.active(chash.active ? 't' : 'f');
834             patron.cards().push(card);
835             if (chash._primary) {
836                 patron.card(card);
837             }
838         });
839
840         var addr_hashes = patron.addresses();
841         patron.addresses([]);
842         angular.forEach(addr_hashes, function(addr_hash) {
843             if (!addr_hash.isnew && !addr_hash.isdeleted) 
844                 addr_hash.ischanged = true;
845             var addr = egCore.idl.fromHash('aua', addr_hash);
846             patron.addresses().push(addr);
847             addr.valid(addr.valid() ? 't' : 'f');
848             addr.within_city_limits(addr.within_city_limits() ? 't' : 'f');
849             if (addr_hash._is_mailing) patron.mailing_address(addr);
850             if (addr_hash._is_billing) patron.billing_address(addr);
851         });
852
853         patron.survey_responses([]);
854         angular.forEach(service.survey_responses, function(answer) {
855             var question = service.survey_questions[answer.question()];
856             var resp = new egCore.idl.asvr();
857             resp.isnew(true);
858             resp.survey(question.survey());
859             resp.question(question.id());
860             resp.answer(answer.id());
861             resp.usr(patron.id());
862             resp.answer_date('now');
863             patron.survey_responses().push(resp);
864         });
865         
866         // re-object-ify the patron stat cat entry maps
867         var maps = [];
868         angular.forEach(patron.stat_cat_entries(), function(entry) {
869             var e = egCore.idl.fromHash('actscecm', entry);
870             e.stat_cat(e.stat_cat().id);
871             maps.push(e);
872         });
873         patron.stat_cat_entries(maps);
874
875         // service.stat_cat_entry_maps maps stats to values
876         // patron.stat_cat_entries is an array of stat_cat_entry_usr_map's
877         angular.forEach(
878             service.stat_cat_entry_maps, function(value, cat_id) {
879
880             // see if we already have a mapping for this entry
881             var existing = patron.stat_cat_entries().filter(
882                 function(e) { return e.stat_cat() == cat_id })[0];
883
884             if (existing) { // we have a mapping
885                 // if the existing mapping matches the new one,
886                 // there' nothing left to do
887                 if (existing.stat_cat_entry() == value) return;
888
889                 // mappings differ.  delete the old one and create
890                 // a new one below.
891                 existing.isdeleted(true);
892             }
893
894             var newmap = new egCore.idl.actscecm();
895             newmap.target_usr(patron.id());
896             newmap.isnew(true);
897             newmap.stat_cat(cat_id);
898             newmap.stat_cat_entry(value);
899             patron.stat_cat_entries().push(newmap);
900         });
901
902         if (!patron.isnew()) patron.ischanged(true);
903
904         return egCore.net.request(
905             'open-ils.actor', 
906             'open-ils.actor.patron.update',
907             egCore.auth.token(), patron);
908     }
909
910     service.remove_staged_user = function() {
911         if (!service.stage_user) return $q.when();
912         return egCore.net.request(
913             'open-ils.actor',
914             'open-ils.actor.user.stage.delete',
915             egCore.auth.token(),
916             service.stage_user.row_id()
917         );
918     }
919
920     service.save_user_settings = function(new_user, user_settings) {
921         // user_settings contains the values from the scope/form.
922         // service.user_settings contain the values from page load time.
923
924         var settings = {};
925         if (service.patron_id) {
926             // only update modified settings for existing patrons
927             angular.forEach(user_settings, function(val, key) {
928                 if (val !== service.user_settings[key])
929                     settings[key] = val;
930             });
931
932         } else {
933             // all non-null setting values are updated for new patrons
934             angular.forEach(user_settings, function(val, key) {
935                 if (val !== null) settings[key] = val;
936             });
937         }
938
939         if (Object.keys(settings).length == 0) return $q.when();
940
941         return egCore.net.request(
942             'open-ils.actor',
943             'open-ils.actor.patron.settings.update',
944             egCore.auth.token(), new_user.id(), settings
945         ).then(function(resp) {
946             console.log('settings returned ' + resp);
947             return resp;
948         });
949     }
950
951     return service;
952 }]);
953
954
955 function PatronRegCtrl($scope, $routeParams, 
956     $q, $modal, $window, egCore, patronSvc, patronRegSvc, egUnloadPrompt) {
957
958     $scope.page_data_loaded = false;
959     $scope.clone_id = patronRegSvc.clone_id = $routeParams.clone_id;
960     $scope.stage_username = 
961         patronRegSvc.stage_username = $routeParams.stage_username;
962     $scope.patron_id = 
963         patronRegSvc.patron_id = $routeParams.edit_id || $routeParams.id;
964
965     // for existing patrons, disable barcode input by default
966     $scope.disable_bc = $scope.focus_usrname = Boolean($scope.patron_id);
967     $scope.focus_bc = !Boolean($scope.patron_id);
968     $scope.dupe_counts = {};
969
970     if (!$scope.edit_passthru) {
971         // in edit more, scope.edit_passthru is delivered to us by
972         // the enclosing controller.  In register mode, there is 
973         // no enclosing controller, so we create our own.
974         $scope.edit_passthru = {};
975     }
976
977     // 0=all, 1=suggested, 2=all
978     $scope.edit_passthru.vis_level = 0; 
979     // TODO: add save/clone handlers here
980
981     $scope.field_modified = function() {
982         // Call attach with every field change, regardless of whether
983         // it's been called before.  This will allow for re-attach after
984         // the user clicks through the unload warning. egUnloadPrompt
985         // will ensure we only attach once.
986         egUnloadPrompt.attach($scope);
987     }
988
989     // Apply default values for new patrons during initial registration
990     // prs is shorthand for patronSvc
991     function set_new_patron_defaults(prs) {
992         if (!$scope.patron.passwd) {
993             // passsword may originate from staged user.
994             $scope.generate_password();
995         }
996         $scope.hold_notify_phone = true;
997         $scope.hold_notify_email = true;
998
999         // staged users may be loaded w/ a profile.
1000         $scope.set_expire_date();
1001
1002         if (prs.org_settings['ui.patron.default_ident_type']) {
1003             // $scope.patron needs this field to be an object
1004             var id = prs.org_settings['ui.patron.default_ident_type'];
1005             var ident_type = $scope.ident_types.filter(
1006                 function(type) { return type.id() == id })[0];
1007             $scope.patron.ident_type = ident_type;
1008         }
1009         if (prs.org_settings['ui.patron.default_inet_access_level']) {
1010             // $scope.patron needs this field to be an object
1011             var id = prs.org_settings['ui.patron.default_inet_access_level'];
1012             var level = $scope.net_access_levels.filter(
1013                 function(lvl) { return lvl.id() == id })[0];
1014             $scope.patron.net_access_level = level;
1015         }
1016         if (prs.org_settings['ui.patron.default_country']) {
1017             $scope.patron.addresses[0].country = 
1018                 prs.org_settings['ui.patron.default_country'];
1019         }
1020     }
1021
1022     function handle_home_org_changed() {
1023         org_id = $scope.patron.home_ou.id();
1024
1025         patronRegSvc.has_group_link_perms(org_id)
1026         .then(function(bool) {$scope.has_group_link_perm = bool});
1027     }
1028
1029     $q.all([
1030
1031         $scope.initTab ? // initTab comes from patron app
1032             $scope.initTab('edit', $routeParams.id) : $q.when(),
1033
1034         patronRegSvc.init()
1035
1036     ]).then(function() {
1037         // called after initTab and patronRegSvc.init have completed
1038
1039         var prs = patronRegSvc; // brevity
1040         // in standalone mode, we have no patronSvc
1041         $scope.patron = prs.init_patron(patronSvc ? patronSvc.current : null);
1042         $scope.field_doc = prs.field_doc;
1043         $scope.edit_profiles = prs.edit_profiles;
1044         $scope.ident_types = prs.ident_types;
1045         $scope.net_access_levels = prs.net_access_levels;
1046         $scope.user_setting_types = prs.user_setting_types;
1047         $scope.opt_in_setting_types = prs.opt_in_setting_types;
1048         $scope.org_settings = prs.org_settings;
1049         $scope.sms_carriers = prs.sms_carriers;
1050         $scope.stat_cats = prs.stat_cats;
1051         $scope.surveys = prs.surveys;
1052         $scope.survey_responses = prs.survey_responses;
1053         $scope.stat_cat_entry_maps = prs.stat_cat_entry_maps;
1054         $scope.stage_user = prs.stage_user;
1055         $scope.stage_user_requestor = prs.stage_user_requestor;
1056
1057         $scope.user_settings = prs.user_settings;
1058         // clone the user settings back into the patronRegSvc so
1059         // we have a copy of the original state of the settings.
1060         prs.user_settings = {};
1061         angular.forEach($scope.user_settings, function(val, key) {
1062             prs.user_settings[key] = val;
1063         });
1064
1065         extract_hold_notify();
1066         handle_home_org_changed();
1067
1068         if ($scope.org_settings['ui.patron.edit.default_suggested'])
1069             $scope.edit_passthru.vis_level = 1;
1070
1071         if ($scope.patron.isnew) 
1072             set_new_patron_defaults(prs);
1073
1074         $scope.page_data_loaded = true;
1075
1076     });
1077
1078     // update the currently displayed field documentation
1079     $scope.set_selected_field_doc = function(cls, field) {
1080         $scope.selected_field_doc = $scope.field_doc[cls][field];
1081     }
1082
1083     // returns the tree depth of the selected profile group tree node.
1084     $scope.pgt_depth = function(grp) {
1085         var d = 0;
1086         while (grp = egCore.env.pgt.map[grp.parent()]) d++;
1087         return d;
1088     }
1089
1090     // IDL fields used for labels in the UI.
1091     $scope.idl_fields = {
1092         au  : egCore.idl.classes.au.field_map,
1093         ac  : egCore.idl.classes.ac.field_map,
1094         aua : egCore.idl.classes.aua.field_map
1095     };
1096
1097     // field visibility cache.  Some fields are universally required.
1098     var field_visibility = {
1099         'ac.barcode' : 2,
1100         'au.usrname' : 2,
1101         'au.passwd' :  2,
1102         // TODO passwd2 2,
1103         'au.first_given_name' : 2,
1104         'au.family_name' : 2,
1105         'au.ident_type' : 2,
1106         'au.home_ou' : 2,
1107         'au.profile' : 2,
1108         'au.expire_date' : 2,
1109         'au.net_access_level' : 2,
1110         'aua.address_type' : 2,
1111         'aua.post_code' : 2,
1112         'aua.street1' : 2,
1113         'aua.street2' : 2,
1114         'aua.city' : 2,
1115         'aua.county' : 2,
1116         'aua.state' : 2,
1117         'aua.country' : 2,
1118         'aua.valid' : 2,
1119         'aua.within_city_limits' : 2,
1120         'stat_cats' : 1,
1121         'surveys' : 1
1122     }; 
1123
1124     // returns true if the selected field should be visible
1125     // given the current required/suggested/all setting.
1126     $scope.show_field = function(field_key) {
1127
1128         if (field_visibility[field_key] == undefined) {
1129             // compile and cache the visibility for the selected field
1130
1131             // org settings have not been received yet.
1132             if (!$scope.org_settings) return false;
1133
1134             var req_set = 'ui.patron.edit.' + field_key + '.require';
1135             var sho_set = 'ui.patron.edit.' + field_key + '.show';
1136             var sug_set = 'ui.patron.edit.' + field_key + '.suggest';
1137
1138             if ($scope.org_settings[req_set]) {
1139                 field_visibility[field_key] = 2;
1140             } else if ($scope.org_settings[sho_set]) {
1141                 field_visibility[field_key] = 2;
1142             } else if ($scope.org_settings[sug_set]) {
1143                 field_visibility[field_key] = 1;
1144             } else {
1145                 field_visibility[field_key] = 0;
1146             }
1147         }
1148
1149         return field_visibility[field_key] >= $scope.edit_passthru.vis_level;
1150     }
1151
1152     // generates a random 4-digit password
1153     $scope.generate_password = function() {
1154         $scope.patron.passwd = Math.floor(Math.random()*9000) + 1000;
1155     }
1156
1157     $scope.set_expire_date = function() {
1158         if (!$scope.patron.profile) return;
1159         var seconds = egCore.date.intervalToSeconds(
1160             $scope.patron.profile.perm_interval());
1161         var now_epoch = new Date().getTime();
1162         $scope.patron.expire_date = new Date(
1163             now_epoch + (seconds * 1000 /* milliseconds */))
1164     }
1165
1166     // grp is the pgt object
1167     $scope.set_profile = function(grp) {
1168         $scope.patron.profile = grp;
1169         $scope.set_expire_date();
1170         $scope.field_modified();
1171     }
1172
1173     $scope.new_address = function() {
1174         var addr = egCore.idl.toHash(new egCore.idl.aua());
1175         patronRegSvc.ingest_address($scope.patron, addr);
1176         addr.id = patronRegSvc.virt_id--;
1177         addr.isnew = true;
1178         addr.valid = true;
1179         addr.within_city_limits = true;
1180         $scope.patron.addresses.push(addr);
1181     }
1182
1183     // keep deleted addresses out of the patron object so
1184     // they won't appear in the UI.  They'll be re-inserted
1185     // when the patron is updated.
1186     deleted_addresses = [];
1187     $scope.delete_address = function(id) {
1188         var addresses = [];
1189         angular.forEach($scope.patron.addresses, function(addr) {
1190             if (addr.id == id) {
1191                 if (id > 0) {
1192                     addr.isdeleted = true;
1193                     deleted_addresses.push(addr);
1194                 }
1195             } else {
1196                 addresses.push(addr);
1197             }
1198         });
1199         $scope.patron.addresses = addresses;
1200     } 
1201
1202     $scope.post_code_changed = function(addr) { 
1203         egCore.net.request(
1204             'open-ils.search', 'open-ils.search.zip', addr.post_code)
1205         .then(function(resp) {
1206             if (!resp) return;
1207             if (resp.city) addr.city = resp.city;
1208             if (resp.state) addr.state = resp.state;
1209             if (resp.county) addr.county = resp.county;
1210             if (resp.alert) alert(resp.alert);
1211         });
1212     }
1213
1214     $scope.replace_card = function() {
1215         $scope.patron.card.active = false;
1216         $scope.patron.card.ischanged = true;
1217         $scope.disable_bc = false;
1218
1219         var new_card = egCore.idl.toHash(new egCore.idl.ac());
1220         new_card.id = patronRegSvc.virt_id--;
1221         new_card.isnew = true;
1222         new_card.active = true;
1223         new_card._primary = 'on';
1224         $scope.patron.card = new_card;
1225         $scope.patron.cards.push(new_card);
1226     }
1227
1228     $scope.day_phone_changed = function(phone) {
1229         if (phone && $scope.patron.isnew && 
1230             $scope.org_settings['patron.password.use_phone']) {
1231             $scope.patron.passwd = phone.substr(-4);
1232         }
1233     }
1234
1235     $scope.barcode_changed = function(bc) {
1236         if (!bc) return;
1237         egCore.net.request(
1238             'open-ils.actor',
1239             'open-ils.actor.barcode.exists',
1240             egCore.auth.token(), bc
1241         ).then(function(resp) {
1242             if (resp == '1') {
1243                 console.log('duplicate barcode detected: ' + bc);
1244                 // DUPLICATE CARD
1245             } else {
1246                 if (!$scope.patron.usrname)
1247                     $scope.patron.usrname = bc;
1248                 // No dupe -- A-OK
1249             }
1250         });
1251     }
1252
1253     $scope.cards_dialog = function() {
1254         $modal.open({
1255             templateUrl: './circ/patron/t_patron_cards_dialog',
1256             controller: 
1257                    ['$scope','$modalInstance','cards',
1258             function($scope , $modalInstance , cards) {
1259                 // scope here is the modal-level scope
1260                 $scope.args = {cards : cards};
1261                 $scope.ok = function() { $modalInstance.close($scope.args) }
1262                 $scope.cancel = function () { $modalInstance.dismiss() }
1263             }],
1264             resolve : {
1265                 cards : function() {
1266                     // scope here is the controller-level scope
1267                     return $scope.patron.cards;
1268                 }
1269             }
1270         }).result.then(
1271             function(args) {
1272                 angular.forEach(args.cards, function(card) {
1273                     card.ischanged = true; // assume cards need updating, OK?
1274                     if (card._primary == 'on' && 
1275                         card.id != $scope.patron.card.id) {
1276                         $scope.patron.card = card;
1277                     }
1278                 });
1279             }
1280         );
1281     }
1282
1283     $scope.set_addr_type = function(addr, type) {
1284         var addrs = $scope.patron.addresses;
1285         if (addr['_is_'+type]) {
1286             angular.forEach(addrs, function(a) {
1287                 if (a.id != addr.id) a['_is_'+type] = false;
1288             });
1289         } else {
1290             // unchecking mailing/billing means we have to randomly
1291             // select another address to fill that role.  Select the
1292             // first address in the list (that does not match the
1293             // modifed address)
1294             for (var i = 0; i < addrs.length; i++) {
1295                 if (addrs[i].id != addr.id) {
1296                     addrs[i]['_is_' + type] = true;
1297                     break;
1298                 }
1299             }
1300         }
1301     }
1302
1303
1304     // Translate hold notify preferences from the form/scope back into a 
1305     // single user setting value for opac.hold_notify.
1306     function compress_hold_notify() {
1307         var hold_notify = '';
1308         var splitter = '';
1309         if ($scope.hold_notify_phone) {
1310             hold_notify = 'phone';
1311             splitter = ':';
1312         }
1313         if ($scope.hold_notify_email) {
1314             hold_notify = splitter + 'email';
1315             splitter = ':';
1316         }
1317         if ($scope.hold_notify_sms) {
1318             hold_notify = splitter + 'sms';
1319             splitter = ':';
1320         }
1321         $scope.user_settings['opac.hold_notify'] = hold_notify;
1322     }
1323
1324     // dialog for selecting additional permission groups
1325     $scope.secondary_groups_dialog = function() {
1326         $modal.open({
1327             templateUrl: './circ/patron/t_patron_groups_dialog',
1328             controller: 
1329                    ['$scope','$modalInstance','linked_groups','pgt_depth',
1330             function($scope , $modalInstance , linked_groups , pgt_depth) {
1331
1332                 $scope.pgt_depth = pgt_depth;
1333                 $scope.args = {
1334                     linked_groups : linked_groups,
1335                     edit_profiles : patronRegSvc.edit_profiles,
1336                     new_profile   : patronRegSvc.edit_profiles[0]
1337                 };
1338
1339                 // add a new group to the linked groups list
1340                 $scope.link_group = function($event, grp) {
1341                     var found = false; // avoid duplicates
1342                     angular.forEach($scope.args.linked_groups, 
1343                         function(g) {if (g.id() == grp.id()) found = true});
1344                     if (!found) $scope.args.linked_groups.push(grp);
1345                     $event.preventDefault(); // avoid close
1346                 }
1347
1348                 // remove a group from the linked groups list
1349                 $scope.unlink_group = function($event, grp) {
1350                     $scope.args.linked_groups = 
1351                         $scope.args.linked_groups.filter(function(g) {
1352                         return g.id() != grp.id()
1353                     });
1354                     $event.preventDefault(); // avoid close
1355                 }
1356
1357                 $scope.ok = function() { $modalInstance.close($scope.args) }
1358                 $scope.cancel = function () { $modalInstance.dismiss() }
1359             }],
1360             resolve : {
1361                 linked_groups : function() { return $scope.patron.groups },
1362                 pgt_depth : function() { return $scope.pgt_depth }
1363             }
1364         }).result.then(
1365             function(args) {
1366                 var ids = args.linked_groups.map(function(g) {return g.id()});
1367                 console.log('linking permission groups ' + ids);
1368                 return egCore.net.request(
1369                     'open-ils.actor',
1370                     'open-ils.actor.user.set_groups',
1371                     egCore.auth.token(), $scope.patron.id, ids)
1372                 .then(function(resp) {
1373                     if (resp == 1) {
1374                         $scope.patron.groups = args.linked_groups;
1375                     } else {
1376                         // debugging -- should be no events
1377                         alert('linked groups failure ' + egCore.evt.parse(resp));
1378                     }
1379                 });
1380             }
1381         );
1382     }
1383
1384     function extract_hold_notify() {
1385         notify = $scope.user_settings['opac.hold_notify'];
1386         if (!notify) return;
1387         $scope.hold_notify_phone = Boolean(notify.match(/phone/));
1388         $scope.hold_notify_email = Boolean(notify.match(/email/));
1389         $scope.hold_notify_sms = Boolean(notify.match(/sms/));
1390     }
1391
1392     $scope.invalidate_field = function(field) {
1393         patronRegSvc.invalidate_field($scope.patron, field);
1394     }
1395
1396     $scope.dupe_value_changed = function(type, value) {
1397         $scope.dupe_counts[type] = 0;
1398         patronRegSvc.dupe_patron_search($scope.patron, type, value)
1399         .then(function(res) {
1400             $scope.dupe_counts[type] = res.count;
1401             $scope.dupe_search_encoded = 
1402                 encodeURIComponent(js2JSON(res.search));
1403         });
1404     }
1405
1406     $scope.edit_passthru.save = function(save_args) {
1407         if (!save_args) save_args = {};
1408
1409         // remove page unload warning prompt
1410         egUnloadPrompt.clear();
1411
1412         // toss the deleted addresses back into the patron's list of
1413         // addresses so it's included in the update
1414         $scope.patron.addresses = 
1415             $scope.patron.addresses.concat(deleted_addresses);
1416         
1417         compress_hold_notify();
1418
1419         var updated_user;
1420
1421         patronRegSvc.save_user($scope.patron)
1422         .then(function(new_user) { 
1423             if (new_user && new_user.classname) {
1424                 updated_user = new_user;
1425                 return patronRegSvc.save_user_settings(
1426                     new_user, $scope.user_settings); 
1427             } else {
1428                 alert('Patron update failed. \n\n' + js2JSON(new_user));
1429             }
1430
1431         }).then(function() {
1432
1433             // only remove the staged user if the update succeeded.
1434             if (updated_user) 
1435                 return patronRegSvc.remove_staged_user();
1436
1437         }).then(function() {
1438
1439             // reloading the page means potentially losing some information
1440             // (e.g. last patron search), but is the only way to ensure all
1441             // components are properly updated to reflect the modified patron.
1442             if (updated_user && save_args.clone) {
1443                 // open a separate tab for registering a new 
1444                 // patron from our cloned data.
1445                 var url = 'https://' 
1446                     + $window.location.hostname 
1447                     + egCore.env.basePath 
1448                     + '/circ/patron/register/clone/' 
1449                     + updated_user.id();
1450                 $window.open(url, '_blank').focus();
1451
1452             } else {
1453                 // reload the current page
1454                 $window.location.href = location.href;
1455             }
1456         });
1457     }
1458 }
1459
1460 // This controller may be loaded from different modules (patron edit vs.
1461 // register new patron), so we have to inject the controller params manually.
1462 PatronRegCtrl.$inject = ['$scope', '$routeParams', '$q', '$modal', 
1463     '$window', 'egCore', 'patronSvc', 'patronRegSvc', 'egUnloadPrompt'];
1464