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