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