]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/actor/user/register.js
Merge branch 'master' of git.evergreen-ils.org:Evergreen into dbs/tpac-non-fixed...
[working/Evergreen.git] / Open-ILS / web / js / ui / default / actor / user / register.js
1 dojo.require('dojo.data.ItemFileReadStore');
2 dojo.require('dijit.form.Form');
3 dojo.require('dijit.form.Textarea');
4 dojo.require('dijit.form.FilteringSelect');
5 dojo.require('dijit.form.ComboBox');
6 dojo.require('dijit.form.NumberSpinner');
7 dojo.require('fieldmapper.IDL');
8 dojo.require('openils.PermaCrud');
9 dojo.require('openils.widget.AutoGrid');
10 dojo.require('openils.widget.AutoFieldWidget');
11 dojo.require('dijit.form.CheckBox');
12 dojo.require('dijit.form.Button');
13 dojo.require('dojo.date');
14 dojo.require('openils.CGI');
15 dojo.require('openils.XUL');
16 dojo.require('openils.Util');
17 dojo.require('openils.Event');
18
19 dojo.requireLocalization('openils.actor', 'register');
20 var localeStrings = dojo.i18n.getLocalization('openils.actor', 'register');
21
22
23 var pcrud;
24 var fmClasses = ['au', 'ac', 'aua', 'actsc', 'asv', 'asvq', 'asva'];
25 var fieldDoc = {};
26 var statCats;
27 var statCatTemplate;
28 var surveys;
29 var staff;
30 var patron;
31 var uEditUsePhonePw = false;
32 var widgetPile = [];
33 var uEditCardVirtId = -1;
34 var uEditAddrVirtId = -1;
35 var orgSettings = {};
36 var userSettings = {};
37 var userSettingsToUpdate = {};
38 var userSettingTypes;
39 var tbody;
40 var addrTemplateRows;
41 var cgi;
42 var cloneUser;
43 var cloneUserObj;
44 var stageUser;
45 var optInSettings;
46 var allCardsTemplate;
47 var uEditCloneCopyAddr; // if true, copy addrs on clone instead of link
48 var homeOuTypes = {};
49 var cardPerms = {};
50 var editCard;
51
52 var dupeUsrname = false;
53 var dupeBarcode = false;
54
55 if(!window.xulG) var xulG = null;
56 var lock_ready = false;
57 var already_locked = false;
58
59 function load() {
60     staff = new openils.User().user;
61     pcrud = new openils.PermaCrud();
62     cgi = new openils.CGI();
63     cloneUser = cgi.param('clone');
64     var userId = cgi.param('usr');
65     var stageUname = cgi.param('stage');
66
67     saveButton.attr("label", localeStrings.SAVE);
68     saveCloneButton.attr("label", localeStrings.SAVE_CLONE);
69     replaceBarcode.attr("label", localeStrings.REPLACE_BARCODE);
70     dojo.byId('uedit-show-required').innerHTML = localeStrings.SHOW_REQUIRED;
71     dojo.byId('uedit-show-suggested').innerHTML = localeStrings.SHOW_SUGGESTED;
72     dojo.byId('uedit-show-all').innerHTML = localeStrings.SHOW_ALL;
73     dojo.byId('uedit-dupe-barcode-warning').innerHTML = localeStrings.BARCODE_IN_USE;
74     allCards.attr("label", localeStrings.SEE_ALL);
75     dojo.byId('uedit-dupe-username-warning').innerHTML = localeStrings.DUPE_USERNAME;
76     generatePassword.attr("label", localeStrings.RESET_PASSWORD);
77     dojo.byId('verifyPassword').innerHTML = localeStrings.VERIFY_PASSWORD;
78     dojo.byId('parentGuardian').innerHTML = localeStrings.PARENT_OR_GUARDIAN;
79     dojo.byId('userSettings').innerHTML = localeStrings.USER_SETTINGS;
80     dojo.byId('statCats').innerHTML = localeStrings.STAT_CATS;
81     dojo.byId('uedit-all-cards-barcode').innerHTML = localeStrings.ALL_CARDS_BARCODE;
82     dojo.byId('uedit-all-cards-active').innerHTML = localeStrings.ALL_CARDS_ACTIVE;
83     dojo.byId('uedit-all-cards-primary').innerHTML = localeStrings.ALL_CARDS_PRIMARY;
84     allCardsClose.attr("label", localeStrings.ALL_CARDS_CLOSE);
85     allCardsApply.attr("label", localeStrings.ALL_CARDS_APPLY);
86
87     dojo.query("td[name='addressHeader']").forEach( function(item) { item.innerHTML = localeStrings.ADDRESS_HEADER; });
88     dojo.query("span[name='mailingAddress']").forEach( function(item) { item.innerHTML = localeStrings.ADDRESS_MAILING; });
89     dojo.query("span[name='billingAddress']").forEach( function(item) { item.innerHTML = localeStrings.ADDRESS_BILLING; });
90     dojo.query("span[name='addressPending']").forEach( function(item) { item.innerHTML = localeStrings.ADDRESS_PENDING; });
91     dojo.query("button[name='approve-button']").forEach( function(item) { item.innerHTML = localeStrings.ADDRESS_APPROVE; });
92     dojo.query("span[name='address-already-owned']").forEach( function(item) { item.innerHTML = localeStrings.ADDRESS_OWNED; });
93     dojo.query("button[name='addressNew']").forEach( function(item) { item.innerHTML = localeStrings.ADDRESS_NEW; });
94
95     if(xulG) {
96             if(xulG.ses) openils.User.authtoken = xulG.ses;
97             if(typeof xulG.clone != 'undefined') cloneUser = xulG.clone;
98         if(typeof xulG.usr != 'undefined') userId = xulG.usr
99         if(typeof xulG.params != 'undefined') {
100             var parms = xulG.params;
101                 if(typeof parms.ses != 'undefined') 
102                 openils.User.authtoken = parms.ses;
103                 if(typeof parms.clone != 'undefined') 
104                 cloneUser = parms.clone;
105             if(typeof parms.usr != 'undefined')
106                 userId = parms.usr;
107             if(typeof parms.stage != 'undefined')
108                 stageUname = parms.stage
109         }
110     }
111
112     orgSettings = fieldmapper.aou.fetchOrgSettingBatch(staff.ws_ou(), [
113         'global.password_regex',
114         'global.juvenile_age_threshold',
115         'patron.password.use_phone',
116         'ui.patron.default_inet_access_level',
117         'ui.patron.default_ident_type',
118         'ui.patron.default_country',
119         'ui.patron.registration.require_address',
120         'circ.holds.behind_desk_pickup_supported',
121         'circ.patron_edit.clone.copy_address',
122         'ui.patron.edit.au.second_given_name.show',
123         'ui.patron.edit.au.second_given_name.suggest',
124         'ui.patron.edit.au.suffix.show',
125         'ui.patron.edit.au.suffix.suggest',
126         'ui.patron.edit.au.alias.show',
127         'ui.patron.edit.au.alias.suggest',
128         'ui.patron.edit.au.dob.require',
129         'ui.patron.edit.au.dob.show',
130         'ui.patron.edit.au.dob.suggest',
131         'ui.patron.edit.au.dob.calendar',
132         'ui.patron.edit.au.juvenile.show',
133         'ui.patron.edit.au.juvenile.suggest',
134         'ui.patron.edit.au.ident_value.show',
135         'ui.patron.edit.au.ident_value.suggest',
136         'ui.patron.edit.au.ident_value2.show',
137         'ui.patron.edit.au.ident_value2.suggest',
138         'ui.patron.edit.au.email.require',
139         'ui.patron.edit.au.email.show',
140         'ui.patron.edit.au.email.suggest',
141         'ui.patron.edit.au.email.regex',
142         'ui.patron.edit.au.email.example',
143         'ui.patron.edit.au.day_phone.require',
144         'ui.patron.edit.au.day_phone.show',
145         'ui.patron.edit.au.day_phone.suggest',
146         'ui.patron.edit.au.day_phone.regex',
147         'ui.patron.edit.au.day_phone.example',
148         'ui.patron.edit.au.evening_phone.require',
149         'ui.patron.edit.au.evening_phone.show',
150         'ui.patron.edit.au.evening_phone.suggest',
151         'ui.patron.edit.au.evening_phone.regex',
152         'ui.patron.edit.au.evening_phone.example',
153         'ui.patron.edit.au.other_phone.require',
154         'ui.patron.edit.au.other_phone.show',
155         'ui.patron.edit.au.other_phone.suggest',
156         'ui.patron.edit.au.other_phone.regex',
157         'ui.patron.edit.au.other_phone.example',
158         'ui.patron.edit.phone.regex',
159         'ui.patron.edit.phone.example',
160         'ui.patron.edit.au.active.show',
161         'ui.patron.edit.au.active.suggest',
162         'ui.patron.edit.au.barred.show',
163         'ui.patron.edit.au.barred.suggest',
164         'ui.patron.edit.au.master_account.show',
165         'ui.patron.edit.au.master_account.suggest',
166         'ui.patron.edit.au.claims_returned_count.show',
167         'ui.patron.edit.au.claims_returned_count.suggest',
168         'ui.patron.edit.au.claims_never_checked_out_count.show',
169         'ui.patron.edit.au.claims_never_checked_out_count.suggest',
170         'ui.patron.edit.au.alert_message.show',
171         'ui.patron.edit.au.alert_message.suggest',
172         'ui.patron.edit.aua.post_code.regex',
173         'ui.patron.edit.aua.post_code.example',
174         'ui.patron.edit.aua.county.require',
175         'format.date',
176         'ui.patron.edit.default_suggested',
177         'opac.barcode_regex',
178         'opac.username_regex'
179     ]);
180
181     for(k in orgSettings)
182         if(orgSettings[k])
183             orgSettings[k] = orgSettings[k].value;
184
185     uEditCloneCopyAddr = orgSettings['circ.patron_edit.clone.copy_address'];
186     uEditUsePhonePw = orgSettings['patron.password.use_phone'];
187     uEditFetchUserSettings(userId);
188
189     if(userId) {
190         patron = uEditLoadUser(userId);
191     } else {
192         if(stageUname) {
193             patron = uEditLoadStageUser(stageUname);
194         } else {
195             patron = uEditNewPatron();
196             if(cloneUser) 
197                 uEditCopyCloneData(patron);
198         }
199     }
200
201
202     var list = pcrud.search('fdoc', {fm_class:fmClasses});
203     for(var i in list) {
204         var doc = list[i];
205         if(!fieldDoc[doc.fm_class()])
206             fieldDoc[doc.fm_class()] = {};
207         fieldDoc[doc.fm_class()][doc.field()] = doc;
208     }
209
210     list = pcrud.search('aout', {can_have_users: 'true'});
211     for(var i in list) {
212         var type = list[i];
213         homeOuTypes[type.id()] = true;
214     }
215
216     tbody = dojo.byId('uedit-tbody');
217
218     if(orgSettings['ui.patron.edit.default_suggested'])
219         uEditToggleRequired(2);
220
221     addrTemplateRows = dojo.query('tr[type=addr-template]', tbody);
222     dojo.forEach(addrTemplateRows, function(row) { row.parentNode.removeChild(row); } );
223     statCatTemplate = tbody.removeChild(dojo.byId('stat-cat-row-template'));
224     surveyTemplate = tbody.removeChild(dojo.byId('survey-row-template'));
225     surveyQuestionTemplate = tbody.removeChild(dojo.byId('survey-question-row-template'));
226
227     checkGrpAppPerm(); // to do the initial load
228     loadStaticFields();
229
230
231     if(patron.isnew() && patron.addresses().length == 0) 
232         uEditNewAddr(null, uEditAddrVirtId, true);
233     else loadAllAddrs();
234     loadStatCats();
235     loadSurveys();
236     checkClaimsReturnCountPerm();
237     checkClaimsNoCheckoutCountPerm();
238
239     dojo.connect(replaceBarcode, 'onClick', replaceCardHandler);
240     dojo.connect(allCards, 'onClick', drawAllCards);
241     if(patron.isnew()) {
242         dojo.addClass(dojo.byId('uedit-all-barcodes'), 'hidden');
243     } else if(checkGrpAppPerm(patron.profile())) {
244         new openils.User().getPermOrgList(
245             'UPDATE_PATRON_ACTIVE_CARD',
246             function(orgList) { 
247                 if(orgList.indexOf(patron.home_ou()) != -1) 
248                     cardPerms['UPDATE_PATRON_ACTIVE_CARD'] = true;
249             },
250             true, 
251             true
252         );
253         new openils.User().getPermOrgList(
254             'UPDATE_PATRON_PRIMARY_CARD',
255             function(orgList) { 
256                 if(orgList.indexOf(patron.home_ou()) != -1) 
257                     cardPerms['UPDATE_PATRON_PRIMARY_CARD'] = true;
258             },
259             true, 
260             true
261         );
262     }
263
264     var input = findWidget('ac', 'barcode');
265     if (patron.isnew()) {
266         replaceBarcode.attr('disabled', true);
267     } else {
268         input.widget.attr('disabled', true).attr('readOnly', true);
269     }
270
271         dojo.connect(generatePassword, 'onClick', generatePasswordHandler);
272
273     if(!patron.isnew() && !checkGrpAppPerm(patron.profile()) && patron.id() != openils.User.user.id()) {
274         // we are not allowed to edit this user, so disable the save option
275         saveButton.attr('disabled', true);
276         saveCloneButton.attr('disabled', true);
277     }
278         
279     lock_ready = true;
280 }
281
282 var permGroups;
283 var noPermGroups = [];
284 // Returns true if the user is allowed to edit the selected group
285 function checkGrpAppPerm(grpId) {
286
287     if(!permGroups) {
288
289         // get the groups
290         permGroups = new openils.PermaCrud().retrieveAll('pgt');
291         var permGroupPerms = []
292
293         // collect the group permissions
294         dojo.forEach(permGroups, 
295             function(grp) {
296                 if(grp.application_perm())
297                     permGroupPerms.push(grp.application_perm());
298             }
299         );
300
301         // see which of the group application perms I do not have
302         var myPerms = fieldmapper.standardRequest(
303             ['open-ils.actor', 'open-ils.actor.user.has_work_perm_at.batch'],
304             [openils.User.authtoken, permGroupPerms]
305         );
306
307         var failedPerms = [];
308         for(var p in myPerms) { 
309             if(myPerms[p].length == 0) 
310                 failedPerms.push(p); 
311         }
312
313         // identify which groups I cannot edit because I do not have permisssion
314
315         function checkTree(grp, failed) {
316             failed = failed || failedPerms.indexOf(grp.application_perm()) > -1;
317             if(failed) noPermGroups.push(grp.id()+'');
318             dojo.forEach(
319                 permGroups.filter(function(g) { return g.parent() == grp.id() } ),
320                 function(child) {
321                     checkTree(child, failed);
322                 }
323             );
324         }
325
326         checkTree(permGroups.filter(function(g) { return g.parent() == null })[0]);
327     }
328
329     return noPermGroups.indexOf(grpId+'') == -1;
330 }
331
332
333 function drawAllCards() {
334
335     var tbody = dojo.byId('uedit-all-cards-tbody');
336     if(!allCardsTemplate) {
337         allCardsTemplate = tbody.removeChild(dojo.byId('uedit-all-cards-tr-template'));
338     } else {
339         while(tbody.childNodes[0])
340             tbody.removeChild(tbody.childNodes[0]);
341     }
342
343     if(cardPerms['UPDATE_PATRON_ACTIVE_CARD'] || cardPerms['UPDATE_PATRON_PRIMARY_CARD']) {
344         dojo.removeClass(dojo.byId('uedit-apply-card-changes'), 'hidden');
345     } else {
346         dojo.addClass(dojo.byId('uedit-apply-card-changes'), 'hidden');
347     }
348
349     var first = true;
350     dojo.forEach(
351         patron.cards().filter(function(c) { return c.id() == patron.card().id(); }).concat(patron.cards()), // grab the main card first
352         function(card) {
353             if(!first) {
354                 if(card.id() == patron.card().id())
355                     return;
356             }
357             var row = allCardsTemplate.cloneNode(true);
358             row.setAttribute("cardid", card.id());
359             row.card = card;
360             getByName(row, 'barcode').innerHTML = card.barcode();
361             if(cardPerms['UPDATE_PATRON_ACTIVE_CARD']) {
362                 row.active_checkbox = new dijit.form.CheckBox({
363                     scrollOnFocus:false,
364                     checked: openils.Util.isTrue(card.active())
365                 }, getByName(row, 'active'));
366             } else {
367                 getByName(row, 'active').appendChild(
368                     openils.Util.isTrue(card.active()) ? 
369                         dojo.byId('true').cloneNode(true) :
370                         dojo.byId('false').cloneNode(true)
371                 );
372             }
373             if(cardPerms['UPDATE_PATRON_PRIMARY_CARD']) {
374                 row.primary_radiobutton = new dijit.form.RadioButton({
375                     scrollOnFocus:false,
376                     checked: card.id() == patron.card().id(),
377                     value: card.id(),
378                     name: 'card_primary'
379                 }, getByName(row, 'primary'));
380             } else {
381                 getByName(row, 'primary').appendChild(
382                     openils.Util.isTrue(card.id() == patron.card().id()) ? 
383                         dojo.byId('true').cloneNode(true) :
384                         dojo.byId('false').cloneNode(true)
385                 );
386             }
387             tbody.appendChild(row);
388             first = false;
389         }
390     );
391
392     allCardsDialog.show();
393 }
394
395 function applyCardChanges() {
396     var cardrows = dojo.query('[cardid]', allCardsDialog.domNode);
397     var changed = false;
398     dojo.forEach(cardrows,
399         function(row) {
400             if(cardPerms['UPDATE_PATRON_ACTIVE_CARD']) {
401                 var active = row.active_checkbox.checked ? 't' : 'f'
402                 if(row.card.active() != active) {
403                     row.card.active(active);
404                     row.card.ischanged(1);
405                     changed = true;
406                 }
407             }
408             if(cardPerms['UPDATE_PATRON_PRIMARY_CARD']) {
409                 if(row.primary_radiobutton.checked && row.card.id() != patron.card().id()) {
410                     patron.card(row.card);
411                     changed = true;
412                 }
413             }
414         }
415     );
416     if(changed && lock_ready && xulG && typeof xulG.lock_tab == 'function' && !already_locked) {
417         xulG.lock_tab();
418         already_locked = true;
419     }
420     allCardsDialog.hide();
421 }
422
423 /**
424  * Mark the current card inactive, create a new primary card
425  */
426 function replaceCardHandler() {
427     var input = findWidget('ac', 'barcode');
428     input.widget.attr('disabled', false).attr('readOnly', false).attr('value', null).focus();
429     replaceBarcode.attr('disabled', true);
430     
431     // pull old card off the cards list so we don't have a dupe sitting in there
432     if (patron.cards().length > 0) {
433         var old = patron.cards().filter(function(c){return (c.id() == patron.card().id())})[0];
434         old.active('f');
435         old.ischanged(1);
436     }
437
438     var newc = new fieldmapper.ac();
439     newc.id(uEditCardVirtId--);
440     newc.isnew(1);
441     newc.active('t');
442     patron.card(newc);
443     editCard = newc;
444     var t = patron.cards();
445         if (!t) { t = []; }
446         t.push(newc);
447         patron.cards(t);
448 }
449
450 /**
451  * Generate a random password for the patron.
452  */
453 function generatePasswordHandler() {
454         uEditMakeRandomPw(patron);
455         var f = findWidget('au', 'passwd');
456         f.widget.attr('value', patron.passwd());
457         f = findWidget('au', 'passwd2');
458         f.widget.attr('value', patron.passwd());
459 }
460
461 /**
462  * Loads a staged user and turns them into something the editor can understand
463  */
464 function uEditLoadStageUser(stageUname) {
465
466     var data = fieldmapper.standardRequest(
467         ['open-ils.actor', 'open-ils.actor.user.stage.retrieve.by_username'],
468         { params : [openils.User.authtoken, stageUname] }
469     );
470
471     stageUser = data.user;
472     patron = uEditNewPatron();
473
474     if(!stageUser) 
475         return patron;
476
477     // copy the data into our new user object
478     for(var key in fieldmapper.IDL.fmclasses.stgu.field_map) {
479         if(fieldmapper.IDL.fmclasses.au.field_map[key] && !fieldmapper.IDL.fmclasses.stgu.field_map[key].virtual) {
480             if(data.user[key]() !== null)
481                 patron[key]( data.user[key]() );
482         }
483     }
484
485     // copy the data into our new address objects
486     // TODO: uses the first mailing address only
487     if(data.mailing_addresses.length) {
488
489         var mail_addr = new fieldmapper.aua();
490         mail_addr.id(-1); // virtual ID
491         mail_addr.usr(-1);
492         mail_addr.isnew(1);
493         patron.mailing_address(mail_addr);
494         var t = patron.addresses();
495             if (!t) { t = []; }
496             t.push(mail_addr);
497             patron.addresses(t);
498
499         for(var key in fieldmapper.IDL.fmclasses.stgma.field_map) {
500             if(fieldmapper.IDL.fmclasses.aua.field_map[key] && !fieldmapper.IDL.fmclasses.stgma.field_map[key].virtual) {
501                 if(data.mailing_addresses[0][key]() !== null)
502                     mail_addr[key]( data.mailing_addresses[0][key]() );
503             }
504         }
505     }
506     
507     // copy the data into our new address objects
508     // TODO uses the first billing address only
509     if(data.billing_addresses.length) {
510
511         var bill_addr = new fieldmapper.aua();
512         bill_addr.id(-2); // virtual ID
513         bill_addr.usr(-1);
514         bill_addr.isnew(1);
515         patron.billing_address(bill_addr);
516         var t = patron.addresses();
517             if (!t) { t = []; }
518             t.push(bill_addr);
519             patron.addresses(t);
520
521         for(var key in fieldmapper.IDL.fmclasses.stgba.field_map) {
522             if(fieldmapper.IDL.fmclasses.aua.field_map[key] && !fieldmapper.IDL.fmclasses.stgba.field_map[key].virtual) {
523                 if(data.billing_addresses[0][key]() !== null)
524                     bill_addr[key]( data.billing_addresses[0][key]() );
525             }
526         }
527     }
528
529     // TODO: uses the first card only
530     if(data.cards.length) {
531         var card = new fieldmapper.ac();
532         card.id(-1); // virtual ID
533         patron.card().barcode(data.cards[0].barcode());
534     }
535
536     return patron;
537 }
538
539 /*
540  * clone the home org, phone numbers, and billing/mailing address
541  */
542 function uEditCopyCloneData(patron) {
543     cloneUserObj = uEditLoadUser(cloneUser);
544
545     var cloneFields = [
546         'home_ou', 
547         'day_phone', 
548         'evening_phone', 
549         'other_phone',
550         'usrgroup'
551     ];
552
553     if(!uEditCloneCopyAddr) 
554         cloneFields = cloneFields.concat(['mailing_address', 'billing_address']);
555
556     dojo.forEach(
557         cloneFields, 
558         function(field) {
559             patron[field](cloneUserObj[field]());
560         }
561     );
562
563     if(uEditCloneCopyAddr) {
564         var billAddr, mailAddr;
565
566         // copy the billing and mailing addresses into new addresses
567         function cloneAddr(addr) {
568             var newAddr = addr.clone();
569             newAddr.isnew(true);
570             newAddr.id(uEditAddrVirtId--);
571             newAddr.usr(patron.id());
572             patron.addresses().push(newAddr);
573             return newAddr;
574         }
575
576         if(billAddr = cloneUserObj.billing_address()) 
577             patron.billing_address(cloneAddr(billAddr));
578
579         if(mailAddr = cloneUserObj.mailing_address()) {
580             if (billAddr && billAddr.id() == mailAddr.id()) {
581                 patron.mailing_address(patron.billing_address());
582             } else {
583                 patron.mailing_address(cloneAddr(mailAddr));
584             }
585         }
586
587         if(!billAddr) // if there was no billing addr, use the mailing addr
588             patron.billing_address(patron.mailing_address());
589
590     } else {
591
592         // link the billing and mailing addresses
593         if(patron.billing_address()) {
594             var t = patron.addresses();
595                 if (!t) { t = []; }
596                 t.push(patron.billing_address());
597                 patron.addresses(t);
598         }
599
600         if(patron.mailing_address() && (
601                 patron.addresses().length == 0 || 
602                 patron.mailing_address().id() != patron.billing_address().id()) ) {
603             var t = patron.addresses();
604                 if (!t) { t = []; }
605                 t.push(patron.mailing_address());
606                 patron.addresses(t);
607         }
608     }
609 }
610
611
612 function uEditFetchUserSettings(userId) {
613     
614     var baseNode = fieldmapper.aou.findOrgUnit(staff.ws_ou());
615     var orgs = fieldmapper.aou.orgNodeTrail(baseNode);
616     orgs = orgs.map(function(node) { return node.id(); });
617
618     /* fetch any user setting types we need + any that offer opt-in */
619     userSettingTypes = pcrud.search('cust', {
620         '-or' : [
621             {name:['circ.holds_behind_desk', 'circ.collections.exempt']}, 
622             {name : {
623                 'in': {
624                     select : {atevdef : ['opt_in_setting']}, 
625                     from : 'atevdef',
626                     // we only care about opt-in settings for event_defs our users encounter
627                     where : {'+atevdef' : {owner : orgs}}
628                 }
629             }}
630         ]
631     });
632
633     var names = userSettingTypes.map(function(obj) { return obj.name() });
634
635     /* fetch any values set for this user */
636     if(userId) {
637         userSettings = fieldmapper.standardRequest(
638             ['open-ils.actor', 'open-ils.actor.patron.settings.retrieve.authoritative'],
639             {params : [openils.User.authtoken, userId, names]});
640     }
641 }
642
643
644 function uEditLoadUser(userId) {
645     var patron = fieldmapper.standardRequest(
646         ['open-ils.actor', 'open-ils.actor.user.fleshed.retrieve.authoritative'],
647         {params : [openils.User.authtoken, userId]}
648     );
649     openils.Event.parse_and_raise(patron);
650     return patron;
651 }
652
653 function loadStaticFields() {
654     for(var idx = 0; tbody.childNodes[idx]; idx++) {
655         var row = tbody.childNodes[idx];
656         if(row.nodeType != row.ELEMENT_NODE) continue;
657         var fmcls = row.getAttribute('fmclass');
658         if(fmcls) {
659             fleshFMRow(row, fmcls);
660         } else {
661
662             if(row.id == 'uedit-settings-divider') {
663
664                 var template = tbody.removeChild(dojo.byId('uedit-user-setting-template'));
665                 dojo.forEach(userSettingTypes, function(type) { uEditDrawSettingRow(tbody, row, template, type); } );
666
667                 if(userSettingTypes.length > 1 || orgSettings['circ.holds.behind_desk_pickup_supported']) {
668                     openils.Util.show('uedit-settings-divider', 'table-row');
669                 }
670             }
671         }
672     }
673 }
674
675 function uEditDrawSettingRow(tbody, dividerRow, template, stype) {
676     var row = template.cloneNode(true);
677     row.setAttribute('user_setting', stype.name());
678     getByName(row, 'label').innerHTML = stype.label();
679     var cb = new dijit.form.CheckBox({scrollOnFocus:false}, getByName(row, 'widget'));
680     cb.attr('value', userSettings[stype.name()]);
681     dojo.connect(cb, 'onChange', function(newVal) { userSettingsToUpdate[stype.name()] = newVal; });
682     tbody.insertBefore(row, dividerRow.nextSibling);
683     openils.Util.show(row, 'table-row');
684
685     if(stype.name() == 'circ.collections.exempt') {
686         checkCollectionsExemptPerm(cb);
687     }
688 }
689
690 function uEditUpdateUserSettings(userId) {
691     return fieldmapper.standardRequest(
692         ['open-ils.actor', 'open-ils.actor.patron.settings.update'],
693         {params : [openils.User.authtoken, userId, userSettingsToUpdate]});
694 }
695
696 function loadAllAddrs() {
697     dojo.forEach(patron.addresses(),
698         function(addr) {
699             uEditNewAddr(null, addr.id());
700         }
701     );
702 }
703
704 function loadStatCats() {
705
706     statCats = fieldmapper.standardRequest(
707         ['open-ils.circ', 'open-ils.circ.stat_cat.actor.retrieve.all'],
708         {params : [openils.User.authtoken, staff.ws_ou()]}
709     );
710
711     // draw stat cats
712     for(var idx in statCats) {
713         var stat = statCats[idx];
714         var row = statCatTemplate.cloneNode(true);
715         row.id = 'stat-cat-row-' + idx;
716         row.setAttribute('stat_cat_owner',stat.owner());
717         row.setAttribute('stat_cat_name',stat.name());
718         row.setAttribute('stat_cat_id',stat.id());
719         tbody.appendChild(row);
720         getByName(row, 'name').innerHTML = stat.name();
721         var valtd = getByName(row, 'widget');
722         var span = valtd.appendChild(document.createElement('span'));
723         var store = new dojo.data.ItemFileReadStore(
724                 {data:fieldmapper.actsc.toStoreData(stat.entries())});
725         var comboBox = new dijit.form.ComboBox({store:store,scrollOnFocus:false,fetchProperties:{sort:[{attribute: 'value'}]}}, span);
726         comboBox.labelAttr = 'value';
727         comboBox.searchAttr = 'value';
728
729         comboBox._wtype = 'statcat';
730         comboBox._statcat = stat.id();
731         widgetPile.push(comboBox); 
732
733         // populate existing cats
734         var map = patron.stat_cat_entries().filter(
735             function(mp) { return (mp.stat_cat() == stat.id()) })[0];
736         if(map) comboBox.attr('value', map.stat_cat_entry()); 
737
738     }
739 }
740
741 function loadSurveys() {
742
743     surveys = fieldmapper.standardRequest(
744         ['open-ils.circ', 'open-ils.circ.survey.retrieve.all'],
745         {params : [openils.User.authtoken]}
746     );
747
748     // draw surveys
749     for(var idx in surveys) {
750         var survey = surveys[idx];
751         var required = openils.Util.isTrue(survey.required());
752         var srow = surveyTemplate.cloneNode(true);
753         if(required) srow.setAttribute('required','required');
754         tbody.appendChild(srow);
755         getByName(srow, 'name').innerHTML = survey.name();
756
757         for(var q in survey.questions()) {
758             var quest = survey.questions()[q];
759             var qrow = surveyQuestionTemplate.cloneNode(true);
760             if(required) qrow.setAttribute('required','required');
761             tbody.appendChild(qrow);
762             getByName(qrow, 'question').innerHTML = quest.question();
763
764             var span = getByName(qrow, 'answers').appendChild(document.createElement('span'));
765             var store = new dojo.data.ItemFileReadStore(
766                 {data:fieldmapper.asva.toStoreData(quest.answers())});
767             var select = new dijit.form.FilteringSelect({store:store,scrollOnFocus:false}, span);
768             if (! required ) {
769                 select.isValid = function() { return true; };
770             }
771             select.labelAttr = 'answer';
772             select.searchAttr = 'answer';
773
774             select._wtype = 'survey';
775             select._survey = survey.id();
776             select._question = quest.id();
777             widgetPile.push(select); 
778         }
779     }
780 }
781
782
783 function fleshFMRow(row, fmcls, args) {
784     var fmfield = row.getAttribute('fmfield');
785     var wclass = row.getAttribute('wclass');
786     var wstyle = row.getAttribute('wstyle');
787     var wconstraints = row.getAttribute('wconstraints');
788     /* use CSS to set the zindex for widgets you want to disable. */
789     var disabled = dojo.style(row, 'zIndex') == -1 ? true : false;
790     var isphone = (fmcls == 'au') && (fmfield.search('_phone') != -1);
791
792     var isPasswd2 = (fmfield == 'passwd2');
793     if(isPasswd2) fmfield = 'passwd';
794     var fieldIdl = fieldmapper.IDL.fmclasses[fmcls].field_map[fmfield];
795     if(!args) args = {};
796
797     var existing = dojo.query('td', row);
798     var htd = existing[0] || row.appendChild(document.createElement('td'));
799     var ltd = existing[1] || row.appendChild(document.createElement('td'));
800     var wtd = existing[2] || row.appendChild(document.createElement('td'));
801     var ftd = existing[3] || row.appendChild(document.createElement('td'));
802
803     openils.Util.addCSSClass(htd, 'uedit-help');
804     if(fieldDoc[fmcls] && fieldDoc[fmcls][fmfield]) {
805         var link = dojo.byId('uedit-help-template').cloneNode(true);
806         link.id = '';
807         link.onclick = function() { ueLoadContextHelp(fmcls, fmfield) };
808         openils.Util.removeCSSClass(link, 'hidden');
809         htd.appendChild(link);
810     }
811
812     if(!ltd.textContent) {
813         ltd.appendChild(document.createTextNode(fieldIdl.label));
814     }
815
816     if(!ftd.textContent) {
817         if(orgSettings['ui.patron.edit.' + fmcls + '.' + fmfield + '.example']) {
818             ftd.appendChild(document.createTextNode(localeStrings.EXAMPLE + orgSettings['ui.patron.edit.' + fmcls + '.' + fmfield + '.example']));
819         }
820         else if(isphone && orgSettings['ui.patron.edit.phone.example']) {
821             ftd.appendChild(document.createTextNode(localeStrings.EXAMPLE + orgSettings['ui.patron.edit.phone.example']));
822         }
823         else if(fieldIdl.datatype == 'timestamp') {
824             ftd.appendChild(document.createTextNode(localeStrings.EXAMPLE + dojo.date.locale.format(new Date(1970,0,31),{selector: "date", fullYear: true, datePattern: (orgSettings['format.date'] ? orgSettings['format.date'] : null)})));
825         }
826     }
827
828     var span = document.createElement('span');
829     wtd.appendChild(span);
830
831     var fmObject = null;
832     switch(fmcls) {
833         case 'au' : fmObject = patron; break;
834         case 'ac' : if(!editCard) editCard = patron.card(); fmObject = editCard; break;
835         case 'aua' : 
836             fmObject = patron.addresses().filter(
837                 function(i) { return (i.id() == args.addr) })[0];
838             if(fmObject && fmObject.usr() != patron.id())
839                 disabled = true;
840             break;
841     }
842
843     // Adjust required value by org settings
844     var curRequired = row.getAttribute('required');
845     var required = curRequired == 'required';
846     if(orgSettings['ui.patron.edit.' + fmcls + '.' + fmfield + '.require']) {
847         row.setAttribute('required', 'required');
848         required = true;
849     }
850     else if (curRequired != 'required' && orgSettings['ui.patron.edit.' + fmcls + '.' + fmfield + '.show']) {
851         row.setAttribute('required', 'show');
852     }
853     else if (curRequired != 'required' && curRequired != 'show' && orgSettings['ui.patron.edit.' + fmcls + '.' + fmfield + '.suggest']) {
854         row.setAttribute('required', 'suggested');
855     }
856
857     // password data is not fetched/required/displayed for existing users
858     if(!patron.isnew() && 'passwd' == fmfield)
859         required = false;
860
861     var dijitArgs = {
862         style: wstyle, 
863         required : required,
864         constraints : (wconstraints) ? eval('('+wconstraints+')') : {}, // the ()'s prevent Invalid Label errors with eval
865         disabled : disabled
866     };
867
868     // Org settings provided regex?
869     if(orgSettings['ui.patron.edit.' + fmcls + '.' + fmfield + '.regex']) {
870         dijitArgs.regExp = orgSettings['ui.patron.edit.' + fmcls + '.' + fmfield + '.regex'];
871     }
872     else if(isphone && orgSettings['ui.patron.edit.phone.regex']) {
873         dijitArgs.regExp = orgSettings['ui.patron.edit.phone.regex'];
874     }
875
876     if(fmcls == 'au' && fmfield == 'passwd') {
877         if (orgSettings['global.password_regex']) {
878             dijitArgs.regExp = orgSettings['global.password_regex'];
879         }
880     }
881
882     if(fmcls == 'au' && fmfield == 'dob' && !orgSettings['ui.patron.edit.au.dob.calendar'])
883         dijitArgs.popupClass = "";
884
885     var value = row.getAttribute('wvalue');
886     if(value !== null)
887         dijitArgs.value = value;
888
889     var wargs = {
890         idlField : fieldIdl,
891         fmObject : fmObject,
892         fmClass : fmcls,
893         parentNode : span,
894         widgetClass : wclass,
895         dijitArgs : dijitArgs,
896         orgDefaultsToWs : true,
897         orgLimitPerms : ['UPDATE_USER'],
898     };
899
900     if(fmfield == 'profile') {
901         // fetch profile groups non-async so existing expire_date is
902         // not overwritten when the profile groups arrive and update
903         wargs.forceSync = true;
904         wargs.disableQuery = {usergroup : 'f'};
905         if(!patron.isnew() && !checkGrpAppPerm(patron.profile()))
906             wargs.readOnly = true;
907     } else {
908         wargs.forceSync = false;
909     }
910
911     if(fmcls == 'au' && fmfield == 'home_ou'){
912         wargs.labelAttr = 'name';
913         wargs.searchAttr = 'name';
914     }
915
916     var widget = new openils.widget.AutoFieldWidget(wargs);
917     widget.build(
918         function(w, ww) {
919             if(fmfield == 'profile') { trimGrpTree(ww); }
920         }
921     );
922
923     // now put it back before we register the widget
924     if(isPasswd2) fmfield = 'passwd2';
925
926     widget._wtype = fmcls;
927     widget._fmfield = fmfield;
928     widget._addr = args.addr;
929     widgetPile.push(widget);
930     attachWidgetEvents(fmcls, fmfield, widget);
931     return widget;
932 }
933
934 function trimGrpTree(autoWidget) {
935     var store = autoWidget.widget.store;
936     if(!store) return;
937     // remove all groups that this user are not allowed to edit, 
938     // except the profile group of an existing user
939     store.fetch({onItem : 
940         function(item) {
941             if(!checkGrpAppPerm(item.id[0]) && patron.profile() != item.id[0])
942                 store.deleteItem(item);
943         }
944     });
945 }
946
947 function findWidget(wtype, fmfield, callback) {
948     return widgetPile.filter(
949         function(i){
950             if(i._wtype == wtype && i._fmfield == fmfield) {
951                 if(callback) return callback(i);
952                 return true;
953             }
954         }
955     ).pop();
956 }
957
958 /**
959  * if the user does not have the UPDATE_PATRON_CLAIM_RETURN_COUNT, 
960  * they are not allowed to directly alter the claim return count. 
961  * This function checks the perm and disable/enables the widget.
962  */
963 function checkClaimsReturnCountPerm() {
964     new openils.User().getPermOrgList(
965         'UPDATE_PATRON_CLAIM_RETURN_COUNT',
966         function(orgList) { 
967             var cr = findWidget('au', 'claims_returned_count');
968             if(orgList.indexOf(patron.home_ou()) == -1) 
969                 cr.widget.attr('disabled', true);
970             else
971                 cr.widget.attr('disabled', false);
972         },
973         true, 
974         true
975     );
976 }
977
978
979 function checkClaimsNoCheckoutCountPerm() {
980     new openils.User().getPermOrgList(
981         'UPDATE_PATRON_CLAIM_NEVER_CHECKED_OUT_COUNT',
982         function(orgList) { 
983             var cr = findWidget('au', 'claims_never_checked_out_count');
984             if(orgList.indexOf(patron.home_ou()) == -1) 
985                 cr.widget.attr('disabled', true);
986             else
987                 cr.widget.attr('disabled', false);
988         },
989         true, 
990         true
991     );
992 }
993
994 var collectExemptCBox;
995 function checkCollectionsExemptPerm(cbox) {
996     if(cbox) collectExemptCBox = cbox;
997     new openils.User().getPermOrgList(
998         'UPDATE_PATRON_COLLECTIONS_EXEMPT',
999         function(orgList) { 
1000             if(orgList.indexOf(patron.home_ou()) == -1) 
1001                 collectExemptCBox.attr('disabled', true);
1002             else
1003                 collectExemptCBox.attr('disabled', false);
1004         },
1005         true, 
1006         true
1007     );
1008 }
1009
1010 function usePhonePw(newVal) {
1011     var newPw = false;
1012     if(this.regExp) {
1013         matches = RegExp(this.regExp).exec(newVal);
1014         if(matches.length > 1) newPw = matches[1];
1015     }
1016     if(!newPw && newVal && newVal.length >= 4) {
1017         newPw = newVal.substring(newVal.length - 4);
1018     }
1019     if(newPw) {
1020         var p1 = findWidget('au', 'passwd');
1021         var p2 = findWidget('au', 'passwd2');
1022         if (p1 && p2) {
1023             p1.widget.attr('value', newPw);
1024             p2.widget.attr('value', newPw);
1025         }
1026         return newPw;
1027     } else {
1028         return null;
1029     }
1030 }
1031
1032 function attachWidgetEvents(fmcls, fmfield, widget) {
1033
1034     dojo.connect(
1035         widget.widget,
1036         'onKeyPress',
1037         function(ev){
1038             netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
1039             if (!(ev.altKey || ev.ctrlKey || ev.metaKey)) {
1040                 if (lock_ready && xulG && typeof xulG.lock_tab == 'function') {
1041                     if (! already_locked) {
1042                         xulG.lock_tab();
1043                         already_locked = true;
1044                     }
1045                 }
1046             }
1047         }
1048     );
1049     dojo.connect(
1050         widget.widget,
1051         'onChange',
1052         function(){
1053             if (lock_ready && xulG && typeof xulG.lock_tab == 'function') {
1054                 if (! already_locked) {
1055                     xulG.lock_tab();
1056                     already_locked = true;
1057                 }
1058             }
1059         }
1060     );
1061
1062
1063     if(fmcls == 'ac') {
1064         if(fmfield == 'barcode') {
1065             dojo.connect(widget.widget, 'onChange',
1066                 function() {
1067                     var barcode = this.attr('value');
1068                     dupeBarcode = false;
1069                     dojo.addClass(dojo.byId('uedit-dupe-barcode-warning'), 'hidden');
1070                     fieldmapper.standardRequest(
1071                         ['open-ils.actor', 'open-ils.actor.barcode.exists'],
1072                         {
1073                             params: [openils.User.authtoken, barcode],
1074                             oncomplete : function(r) {
1075                                 var res = openils.Util.readResponse(r);
1076                                 if(res == '1') {
1077                                     dupeBarcode = true;
1078                                     dojo.removeClass(dojo.byId('uedit-dupe-barcode-warning'), 'hidden');
1079                                 } else {
1080                                     dupeBarcode = false;
1081                                     dojo.addClass(dojo.byId('uedit-dupe-barcode-warning'), 'hidden');
1082                                     editCard.barcode(barcode); // Keep the "All" interface up to date
1083                                     var un = findWidget('au', 'usrname');
1084                                     if(!un.widget.attr('value'))
1085                                         un.widget.attr('value', barcode);
1086                                 }
1087                             }
1088                         }
1089                     );
1090                 }
1091             );
1092             return;
1093         }
1094     }
1095
1096     if(fmcls == 'au') {
1097         switch(fmfield) {
1098
1099             case 'usrname':
1100                 widget.widget.isValid = function() {
1101                     // No spaces
1102                     if(this.attr("value").match(/\s/)) {
1103                         return false;
1104                     }
1105                     // Can look like a barcode (for initial value)
1106                     if(orgSettings['opac.barcode_regex']) {
1107                         var test_regexp = new RegExp(orgSettings['opac.barcode_regex']);
1108                         if(test_regexp.test(this.attr("value"))) {
1109                             return true;
1110                         }
1111                     }
1112                     // Can look like a username
1113                     if(orgSettings['opac.username_regex']) {
1114                         var test_regexp = new RegExp(orgSettings['opac.username_regex']);
1115                         if(test_regexp.test(this.attr("value"))) {
1116                             return true;
1117                         }
1118                     }
1119                     // If we know what a barcode and username look like and we got here, reject
1120                     if(orgSettings['opac.barcode_regex'] && orgSettings['opac.username_regex'])
1121                         return false;
1122                     // Otherwise we don't have enough info to say either way, let it through.
1123                     return true;
1124                 }
1125                 dojo.connect(widget.widget, 'onChange', 
1126                     function() {
1127                         var input = findWidget('au', 'usrname');
1128                         var usrname = input.widget.attr('value');
1129
1130                         if(!usrname) {
1131                             dupeUsrname = false;
1132                             dojo.addClass(dojo.byId('uedit-dupe-username-warning'), 'hidden');
1133                             return;
1134                         }
1135
1136                         fieldmapper.standardRequest(
1137                             ['open-ils.actor', 'open-ils.actor.username.exists'],
1138                             {
1139                                 params: [openils.User.authtoken, usrname],
1140                                 oncomplete : function(r) {
1141                                     var res = openils.Util.readResponse(r);
1142                                     if(res) {
1143                                         dupeUsrname = true;
1144                                         dojo.removeClass(dojo.byId('uedit-dupe-username-warning'), 'hidden');
1145                                     } else {
1146                                         dupeUsrname = false;
1147                                         dojo.addClass(dojo.byId('uedit-dupe-username-warning'), 'hidden');
1148                                     }
1149                                 }
1150                             }
1151                         );
1152                     }   
1153                 );
1154
1155                 return;
1156
1157             case 'profile': // when the profile changes, update the expire date
1158                 dojo.connect(widget.widget, 'onChange', 
1159                     function() {
1160                         var self = this;
1161                         var expireWidget = findWidget('au', 'expire_date');
1162                         function found(items) {
1163                             if(items.length == 0) return;
1164                             var item = items[0];
1165                             var interval = self.store.getValue(item, 'perm_interval');
1166                             expireWidget.widget.attr('value', dojo.date.add(new Date(), 
1167                                 'second', openils.Util.intervalToSeconds(interval)));
1168                         }
1169                         this.store.fetch({onComplete:found, query:{id:this.attr('value')}});
1170                     }
1171                 );
1172                 return;
1173
1174             case 'dob':
1175                 widget.widget.isValid = function() {
1176                     return this.attr("value") < new Date();
1177                 };
1178                 dojo.connect(widget.widget, 'onChange',
1179                     function(newDob) {
1180                         if(!newDob) return;
1181                         var oldDob = patron.dob();
1182                         if(dojo.date.stamp.fromISOString(oldDob) == newDob) return;
1183
1184                         var juvInterval = orgSettings['global.juvenile_age_threshold'] || '18 years';
1185                         var juvWidget = findWidget('au', 'juvenile');
1186                         var base = new Date();
1187                         base.setTime(base.getTime() - Number(openils.Util.intervalToSeconds(juvInterval) + '000'));
1188
1189                         if(newDob <= base) // older than global.juvenile_age_threshold
1190                             juvWidget.widget.attr('value', false);
1191                         else
1192                             juvWidget.widget.attr('value', true);
1193                     }
1194                 );
1195                 return;
1196
1197             case 'first_given_name':
1198             case 'family_name':
1199                 dojo.connect(widget.widget, 'onChange',
1200                     function(newVal) { uEditDupeSearch('name', newVal); });
1201                 return;
1202
1203             case 'email':
1204                 dojo.connect(widget.widget, 'onChange',
1205                     function(newVal) { uEditDupeSearch('email', newVal); });
1206                 return;
1207
1208             case 'ident_value':
1209             case 'ident_value2':
1210                 dojo.connect(widget.widget, 'onChange',
1211                     function(newVal) { uEditDupeSearch('ident', newVal); });
1212                 return;
1213
1214             case 'day_phone':
1215                 // if configured, use the last four digits of the day phone number as the password
1216                 // Alt, use the first capture group of the validator regex
1217                 if(uEditUsePhonePw && patron.isnew()) {
1218                     dojo.connect(widget.widget, 'onChange', widget.widget, usePhonePw);
1219                     if (patron.day_phone()) {
1220                         usePhonePw(patron.day_phone());
1221                     }
1222                 }
1223             case 'evening_phone':
1224             case 'other_phone':
1225                 dojo.connect(widget.widget, 'onChange',
1226                     function(newVal) { uEditDupeSearch('phone', newVal); });
1227                 return;
1228
1229             case 'home_ou':
1230                 widget.widget.isValid = function() {
1231                     if(this.item) {
1232                         if(homeOuTypes[this.store.getValue(this.item, 'ou_type')]) {
1233                             return true;
1234                         }
1235                         return false;
1236                     }
1237                     return true;
1238                 };
1239                 dojo.connect(widget.widget, 'onChange',
1240                     function(newVal) { 
1241                         checkClaimsReturnCountPerm(); 
1242                         checkClaimsNoCheckoutCountPerm();
1243                         checkCollectionsExemptPerm();
1244                     }
1245                 );
1246                 return;
1247
1248             case 'passwd':
1249                 dojo.connect(widget.widget, 'onChange',
1250                     function(newVal) {
1251                         var pw1 = findWidget('au', 'passwd').widget;
1252                         var pw2 = findWidget('au', 'passwd2').widget;
1253                         var preserved_value = pw2.attr('value');
1254                         // Ensure that the pw2 field match the pw1 field to validate
1255                         pw2.regExp = newVal.replace(/([.\\^$*+?\(\)\[\]\{\}])/g, '\\$1');
1256                         pw2.reset();
1257                         pw2.attr('value',preserved_value);
1258                     });
1259                 return;
1260         }
1261     }
1262
1263     if(fmclass = 'aua') {
1264         switch(fmfield) {
1265             case 'post_code':
1266                 dojo.connect(widget.widget, 'onChange',
1267                     function(e) { 
1268                         fieldmapper.standardRequest(
1269                             ['open-ils.search', 'open-ils.search.zip'],
1270                             {   async: true,
1271                                 params: [e],
1272                                 oncomplete : function(r) {
1273                                     var res = openils.Util.readResponse(r);
1274                                     if(!res) return;
1275                                     var callback = function(w) { return w._addr == widget._addr; };
1276                                     if(res.city) findWidget('aua', 'city', callback).widget.attr('value', res.city);
1277                                     if(res.state) findWidget('aua', 'state', callback).widget.attr('value', res.state);
1278                                     if(res.county) findWidget('aua', 'county', callback).widget.attr('value', res.county);
1279                                     if(res.alert) alert(res.alert);
1280                                 }
1281                             }
1282                         );
1283                     }
1284                 );
1285                 return;
1286
1287             case 'street1':
1288             case 'street2':
1289             case 'city':
1290                 dojo.connect(widget.widget, 'onChange',
1291                     function(e) {
1292                         var callback = function(w) { return w._addr == widget._addr; };
1293                         var args = {
1294                             street1 : findWidget('aua', 'street1', callback).widget.attr('value'),
1295                             street2 : findWidget('aua', 'street2', callback).widget.attr('value'),
1296                             city : findWidget('aua', 'city', callback).widget.attr('value'),
1297                             post_code : findWidget('aua', 'post_code', callback).widget.attr('value')
1298                         };
1299                         if(args.street1 && args.city && args.post_code)
1300                             uEditDupeSearch('address', args); 
1301                     }
1302                 );
1303                 return;
1304         }
1305     }
1306 }
1307
1308 function uEditDupeSearch(type, value) {
1309     if(!value) return;
1310     var search;
1311     switch(type) {
1312
1313         case 'name':
1314             openils.Util.hide('uedit-dupe-names-link');
1315             var fname = findWidget('au', 'first_given_name').widget.attr('value');
1316             var lname = findWidget('au', 'family_name').widget.attr('value');
1317             if( !(fname && lname) ) return;
1318             search = {
1319                 first_given_name : {value : fname, group : 0},
1320                 family_name : {value : lname, group : 0},
1321             };
1322             break;
1323
1324         case 'email':
1325             openils.Util.hide('uedit-dupe-email-link');
1326             search = {email : {value : value, group : 0}};
1327             break;
1328
1329         case 'ident':
1330             openils.Util.hide('uedit-dupe-ident-link');
1331             search = {ident : {value : value, group : 2}};
1332             break;
1333
1334         case 'phone':
1335             openils.Util.hide('uedit-dupe-phone-link');
1336             search = {phone : {value : value, group : 2}};
1337             break;
1338
1339         case 'address':
1340             openils.Util.hide('uedit-dupe-address-link');
1341             search = {};
1342             dojo.forEach(['street1', 'street2', 'city', 'post_code'],
1343                 function(field) {
1344                     if(value[field])
1345                         search[field] = {value : value[field], group: 1};
1346                 }
1347             );
1348             break;
1349     }
1350
1351     // find possible duplicate patrons
1352     fieldmapper.standardRequest(
1353         ['open-ils.actor', 'open-ils.actor.patron.search.advanced'],
1354         {   async: true,
1355             params: [openils.User.authtoken, search],
1356             oncomplete : function(r) {
1357                 var resp = openils.Util.readResponse(r);
1358                 resp = resp.filter(function(id) { return (id != patron.id()); });
1359
1360                 if(resp && resp.length > 0) {
1361
1362                     openils.Util.hide('uedit-help-div');
1363                     openils.Util.show('uedit-dupe-div');
1364                     var link;
1365
1366                     switch(type) {
1367                         case 'name':
1368                             link = dojo.byId('uedit-dupe-names-link');
1369                             link.innerHTML = dojo.string.substitute(localeStrings.DUPE_PATRON_NAME, [resp.length]);
1370                             break;
1371                         case 'email':
1372                             link = dojo.byId('uedit-dupe-email-link');
1373                             link.innerHTML = dojo.string.substitute(localeStrings.DUPE_PATRON_EMAIL, [resp.length]);
1374                             break;
1375                         case 'ident':
1376                             link = dojo.byId('uedit-dupe-ident-link');
1377                             link.innerHTML = dojo.string.substitute(localeStrings.DUPE_PATRON_IDENT, [resp.length]);
1378                             break;
1379                         case 'phone':
1380                             link = dojo.byId('uedit-dupe-phone-link');
1381                             link.innerHTML = dojo.string.substitute(localeStrings.DUPE_PATRON_PHONE, [resp.length]);
1382                             break;
1383                         case 'address':
1384                             link = dojo.byId('uedit-dupe-address-link');
1385                             link.innerHTML = dojo.string.substitute(localeStrings.DUPE_PATRON_ADDR, [resp.length]);
1386                             break;
1387                     }
1388
1389                     openils.Util.show(link);
1390                     link.onclick = function() {
1391                         search.search_sort = js2JSON(["penalties", "family_name", "first_given_name"]);
1392                         if(window.xulG)
1393                             window.xulG.spawn_search(search);
1394                         else
1395                             console.log("running XUL patron search " + js2JSON(search));
1396                     }
1397                 }
1398             }
1399         }
1400     );
1401 }
1402
1403 function getByName(node, name) {
1404     return dojo.query('[name='+name+']', node)[0];
1405 }
1406
1407
1408 function ueLoadContextHelp(fmcls, fmfield) {
1409     openils.Util.hide('uedit-dupe-div');
1410     openils.Util.show('uedit-help-div');
1411     dojo.byId('uedit-help-field').innerHTML = fieldmapper.IDL.fmclasses[fmcls].field_map[fmfield].label;
1412     dojo.byId('uedit-help-text').innerHTML = fieldDoc[fmcls][fmfield].string();
1413 }
1414
1415
1416 /* creates a new patron object with card attached */
1417 function uEditNewPatron() {
1418     patron = new au();
1419     patron.isnew(1);
1420     patron.id(-1);
1421     card = new ac();
1422     card.id(uEditCardVirtId--);
1423     card.isnew(1);
1424     patron.active(1);
1425     patron.card(card);
1426     patron.cards([card]);
1427     patron.net_access_level(orgSettings['ui.patron.default_inet_access_level'] || 1);
1428     patron.ident_type(orgSettings['ui.patron.default_ident_type']);
1429     patron.stat_cat_entries([]);
1430     patron.survey_responses([]);
1431     patron.addresses([]);
1432     uEditMakeRandomPw(patron);
1433     return patron;
1434 }
1435
1436 function uEditMakeRandomPw(patron) {
1437     var rand  = Math.random();
1438     rand = parseInt(rand * 10000) + '';
1439     while(rand.length < 4) rand += '0';
1440 /*
1441     appendClear($('ue_password_plain'),text(rand));
1442     unHideMe($('ue_password_gen'));
1443 */
1444     patron.passwd(rand);
1445     return rand;
1446 }
1447
1448 function uEditWidgetVal(w) {
1449     var val = (w.getFormattedValue) ? w.getFormattedValue() : w.attr('value');
1450     if(val === '') val = null;
1451     return val;
1452 }
1453
1454 function uEditSave() { _uEditSave(); }
1455 function uEditSaveClone() { _uEditSave(true); }
1456
1457 function _uEditSave(doClone) {
1458
1459     if ( (! myForm.isValid()) || dupeUsrname || dupeBarcode ) {
1460         alert(localeStrings.INVALID_FORM);
1461         return;
1462     }
1463
1464     for(var idx in widgetPile) {
1465         var w = widgetPile[idx];
1466         var val = uEditWidgetVal(w);
1467
1468         switch(w._wtype) {
1469             case 'au':
1470                 if(w._fmfield != 'passwd2')
1471                     patron[w._fmfield](val);
1472                 break;
1473
1474             case 'ac':
1475                 if(!editCard) editCard = patron.card();
1476                 editCard[w._fmfield](val);
1477                 break;
1478
1479             case 'aua':
1480                 var addr = patron.addresses().filter(function(i){return (i.id() == w._addr)})[0];
1481                 if(!addr) {
1482                     addr = new fieldmapper.aua();
1483                     addr.id(w._addr);
1484                     addr.isnew(1);
1485                     addr.usr(patron.id());
1486                     addr.country(orgSettings['ui.patron.default_country']);
1487                     var t = patron.addresses();
1488                         if (!t) { t = []; }
1489                         t.push(addr);
1490                         patron.addresses(t);
1491                 } else {
1492                     if(addr[w._fmfield]() != val)
1493                         addr.ischanged(1);
1494                 }
1495                 addr[w._fmfield](val);
1496
1497                 if(dojo.byId('uedit-billing-address-' + addr.id()).checked) 
1498                     patron.billing_address(addr.id());
1499
1500                 if(dojo.byId('uedit-mailing-address-' + addr.id()).checked)
1501                     patron.mailing_address(addr.id());
1502
1503                 break;
1504
1505             case 'survey':
1506                 if(val == null) break;
1507                 var resp = new fieldmapper.asvr();
1508                 resp.isnew(1);
1509                 resp.survey(w._survey)
1510                 resp.usr(patron.id());
1511                 resp.question(w._question)
1512                 resp.answer(val);
1513                 var t = patron.survey_responses();
1514                     if (!t) { t = []; }
1515                     t.push(resp);
1516                     patron.survey_responses(t);
1517                 break;
1518
1519             case 'statcat':
1520                 var map = patron.stat_cat_entries().filter(
1521                     function(m){
1522                         return (m.stat_cat() == w._statcat) })[0];
1523
1524                 if(map) {
1525                     if(map.stat_cat_entry() == val) 
1526                         break;
1527                     if(val == null) {
1528                         val = '';
1529                         map.isdeleted(1);
1530                     } else {
1531                         map.ischanged(1);
1532                     }
1533                 } else {
1534                     if(val == null)
1535                         break;
1536                     map = new fieldmapper.actscecm();
1537                     map.isnew(1);
1538                 }
1539
1540                 map.stat_cat(w._statcat);
1541                 map.stat_cat_entry(val);
1542                 map.target_usr(patron.id());
1543                 var t = patron.stat_cat_entries();
1544                     if (!t) { t = []; }
1545                     t.push(map);
1546                     patron.stat_cat_entries(t);
1547                 break;
1548         }
1549     }
1550
1551     patron.ischanged(1);
1552     fieldmapper.standardRequest(
1553         ['open-ils.actor', 'open-ils.actor.patron.update'],
1554         {   async: true,
1555             params: [openils.User.authtoken, patron],
1556             oncomplete: function(r) {
1557                 lock_ready = false;
1558                 if (xulG && typeof xulG.unlock_tab == 'function') {
1559                     xulG.unlock_tab();
1560                     already_locked = false;
1561                 }
1562                 newPatron = openils.Util.readResponse(r);
1563                 if(newPatron) {
1564                     uEditUpdateUserSettings(newPatron.id());
1565                     if(stageUser) uEditRemoveStage();
1566                     uEditFinishSave(newPatron, doClone);
1567                 }
1568             }
1569         }
1570     );
1571 }
1572
1573 function uEditRemoveStage() {
1574     var resp = fieldmapper.standardRequest(
1575         ['open-ils.actor', 'open-ils.actor.user.stage.delete'],
1576         { params : [openils.User.authtoken, stageUser.row_id()] }
1577     )
1578 }
1579
1580 function uEditFinishSave(newPatron, doClone) {
1581
1582     if(doClone && cloneUser == null)
1583         cloneUser = newPatron.id();
1584
1585         if( doClone ) {
1586
1587                 if(xulG && typeof xulG.spawn_editor == 'function' && !patron.isnew() ) {
1588             window.xulG.spawn_editor({ses:openils.User.authtoken,clone:cloneUser});
1589             uEditRefresh();
1590
1591                 } else {
1592                         location.href = location.href.replace(/\?.*/, '') + '?clone=' + cloneUser;
1593                 }
1594
1595         } else {
1596
1597                 uEditRefresh();
1598         }
1599
1600         uEditRefreshXUL(newPatron);
1601 }
1602
1603 function uEditRefresh() {
1604     var usr = cgi.param('usr');
1605     var href = location.href.replace(/\?.*/, '');
1606     href += ((usr) ? '?usr=' + usr : '');
1607     location.href = href;
1608 }
1609
1610 function uEditRefreshXUL(newuser) {
1611         if (window.xulG && typeof window.xulG.on_save == 'function') 
1612                 window.xulG.on_save(newuser);
1613 }
1614
1615
1616 /**
1617  * Create a new address and insert it into the DOM
1618  * @param evt ignored
1619  * @param id The address id
1620  * @param mkLinks If true, set the new address as the 
1621  *  mailing/billing address for the user
1622  */
1623 function uEditNewAddr(evt, id, mkLinks) {
1624
1625     if(id == null) 
1626         id = --uEditAddrVirtId; // new address
1627
1628     var addr =  patron.addresses().filter(
1629         function(i) { return (i.id() == id) })[0];
1630
1631     dojo.forEach(addrTemplateRows, 
1632         function(row) {
1633
1634             row = tbody.insertBefore(row.cloneNode(true), dojo.byId('new-addr-row'));
1635             row.setAttribute('type', '');
1636             row.setAttribute('addr', id+'');
1637
1638             if(row.getAttribute('fmclass')) {
1639                 var widget = fleshFMRow(row, 'aua', {addr:id});
1640
1641                 // make new addresses a default address type
1642                 if(id < 0 && row.getAttribute('fmfield') == 'address_type') 
1643                     widget.widget.attr('value', localeStrings.DEFAULT_ADDRESS_TYPE); 
1644
1645                 // make new addresses valid by default
1646                 if(id < 0 && row.getAttribute('fmfield') == 'valid') 
1647                     widget.widget.attr('value', true); 
1648
1649                 // make new addresses use the org setting for default country 
1650                 if(id < 0 && row.getAttribute('fmfield') == 'country') 
1651                     widget.widget.attr('value',orgSettings['ui.patron.default_country']);
1652
1653             } else if(row.getAttribute('name') == 'uedit-addr-pending-row') {
1654
1655                 // if it's a pending address, show the 'approve' button
1656                 if(addr && openils.Util.isTrue(addr.pending())) {
1657                     openils.Util.show(row, 'table-row');
1658                     dojo.query('[name=approve-button]', row)[0].onclick = 
1659                         function() { uEditApproveAddress(addr); };
1660
1661                     if(addr.replaces()) {
1662                         var div = dojo.query('[name=replaced-addr]', row)[0]
1663                         var replaced =  patron.addresses().filter(
1664                             function(i) { return (i.id() == addr.replaces()) })[0];
1665
1666                         div.innerHTML = dojo.string.substitute(localeStrings.REPLACED_ADDRESS, [
1667                             replaced.address_type() || '',
1668                             replaced.street1() || '',
1669                             replaced.street2() || '',
1670                             replaced.city() || '',
1671                             replaced.state() || '',
1672                             replaced.post_code() || ''
1673                         ]);
1674
1675                     } else {
1676                         openils.Util.hide(dojo.query('[name=replaced-addr-div]', row)[0]);
1677                     }
1678                 }
1679
1680             } else if(row.getAttribute('name') == 'uedit-addr-owner-row') {
1681                 // address is owned by someone else.  provide option to load the
1682                 // user in a different tab
1683                 
1684                 if(addr && addr.usr() != patron.id()) {
1685                     openils.Util.show(row, 'table-row');
1686                     var link = getByName(row, 'addr-owner');
1687
1688                     // fetch the linked user so we can present their name in the UI
1689                     var addrUser;
1690                     if(cloneUserObj && cloneUserObj.id() == addr.usr()) {
1691                         addrUser = [
1692                             cloneUserObj.first_given_name(), 
1693                             cloneUserObj.second_given_name(), 
1694                             cloneUserObj.family_name()
1695                         ];
1696                     } else {
1697                         addrUser = fieldmapper.standardRequest(
1698                             ['open-ils.actor', 'open-ils.actor.user.retrieve.parts'],
1699                             {params: [
1700                                 openils.User.authtoken, 
1701                                 addr.usr(), 
1702                                 ['first_given_name', 'second_given_name', 'family_name']
1703                             ]}
1704                         );
1705                     }
1706
1707                     link.innerHTML = (addrUser.map(function(name) { return (name) ? name+' ' : '' })+'').replace(/,/g,''); // TODO i18n
1708                     link.onclick = function() {
1709                         if(openils.XUL.isXUL()) { 
1710                             window.xulG.spawn_editor({ses:openils.User.authtoken, usr:addr.usr()})
1711                         } else {
1712                             parent.location.href = location.href.replace(/clone=\d+/, 'usr=' + addr.usr());
1713                         }
1714                     }
1715                 }
1716
1717             } else if(row.getAttribute('name') == 'uedit-addr-divider') {
1718                 // link up the billing/mailing address and give the inputs IDs so we can access the later
1719                 
1720                 // billing address
1721                 var ba = getByName(row, 'billing_address');
1722                 ba.id = 'uedit-billing-address-' + id;
1723                 if(mkLinks || (patron.billing_address() && patron.billing_address().id() == id))
1724                     ba.checked = true;
1725
1726                 // mailing address
1727                 var ma = getByName(row, 'mailing_address');
1728                 ma.id = 'uedit-mailing-address-' + id;
1729                 if(mkLinks || (patron.mailing_address() && patron.mailing_address().id() == id))
1730                     ma.checked = true;
1731
1732                 var btn = dojo.query('[name=delete-button]', row)[0];
1733                 if(btn) btn.onclick = function(){ uEditDeleteAddr(id) };
1734             }
1735         }
1736     );
1737 }
1738
1739 function uEditApproveAddress(addr) {
1740     fieldmapper.standardRequest(
1741         ['open-ils.actor', 'open-ils.actor.user.pending_address.approve'],
1742         {   async: true,
1743             params:  [openils.User.authtoken, addr],
1744
1745             oncomplete : function(r) {
1746                 var oldId = openils.Util.readResponse(r);
1747                     
1748                 // remove addrs from UI
1749                 dojo.forEach(
1750                     patron.addresses(), 
1751                     function(addr) { uEditDeleteAddr(addr.id(), true); }
1752                 );
1753
1754                 if(oldId != null) {
1755                     
1756                     // remove the replaced address 
1757                     if(oldId != addr.id()) {
1758                                 patron.addresses(
1759                             patron.addresses().filter(
1760                                                 function(i) { return (i.id() != oldId); }
1761                                         )
1762                                 );
1763                     }
1764                     
1765                     // fix the the new address
1766                     addr.id(oldId);
1767                     addr.replaces(null);
1768                     addr.pending('f');
1769
1770                 }
1771
1772                 // redraw addrs
1773                 loadAllAddrs();
1774             }
1775         }
1776     );
1777 }
1778
1779
1780 function uEditDeleteAddr(id, noAlert) {
1781     if (patron.isnew() && orgSettings['ui.patron.registration.require_address']) {
1782         if (dojo.query('tr[name=uedit-addr-divider]').length < 2) {
1783             alert(localeStrings.NEED_ADDRESS);
1784             return;
1785         }
1786     }
1787     if(!noAlert) {
1788         if(!confirm(dojo.string.substitute(localeStrings.DELETE_ADDRESS, [id]))) return;
1789     }
1790     var addr = patron.addresses().filter(function(i){return (i.id() == id)})[0];
1791     if (addr) { addr.isdeleted(1); }
1792     var m_a = patron.mailing_address();
1793         if (typeof m_a == 'object' && m_a != null) { m_a = m_a.id(); }
1794         if (m_a == id) { patron.mailing_address(null); }
1795     var b_a = patron.billing_address();
1796         if (typeof b_a == 'object' && b_a != null) { b_a = b_a.id(); }
1797         if (b_a == id) { patron.billing_address(null); }
1798
1799     var rows = dojo.query('tr[addr='+id+']', tbody);
1800     for(var i = 0; i < rows.length; i++)
1801         rows[i].parentNode.removeChild(rows[i]);
1802     widgetPile = widgetPile.filter(function(w){return (w._addr != id)});
1803 }
1804
1805 function uEditToggleRequired(level) {
1806     openils.Util.removeCSSClass(tbody, 'hide-non-required');
1807     openils.Util.removeCSSClass(tbody, 'hide-non-suggested');
1808     openils.Util.show('uedit-show-required');
1809     openils.Util.show('uedit-show-required-br');
1810     openils.Util.show('uedit-show-suggested');
1811     openils.Util.show('uedit-show-suggested-br');
1812     openils.Util.show('uedit-show-all');
1813     switch(level) {
1814         case 1:
1815             openils.Util.hide('uedit-show-required');
1816             openils.Util.hide('uedit-show-required-br');
1817             openils.Util.addCSSClass(tbody, 'hide-non-required');
1818             break;
1819         case 2:
1820             openils.Util.hide('uedit-show-suggested');
1821             openils.Util.hide('uedit-show-suggested-br');
1822             openils.Util.addCSSClass(tbody, 'hide-non-suggested');
1823             break;
1824         default:
1825             openils.Util.hide('uedit-show-all');
1826             break;
1827     } 
1828 }
1829
1830 function printable_output() {
1831     var temp; var s = '=-=-=-=\r\n';
1832     for (var idx in widgetPile) {
1833         var w = widgetPile[idx];
1834         var val = uEditWidgetVal(w);
1835         var label;
1836         if (typeof w.idlField == 'undefined') {
1837             label = w._wtype;
1838             if (w._wtype == 'statcat') {
1839                 var stat = statCats.filter(
1840                     function(m){
1841                         return (m.id() == w._statcat) })[0];
1842                 label = stat.name();
1843             } else if (w._wtype == 'survey') {
1844                 var survey = surveys.filter(
1845                     function(m){
1846                         return (m.id() == w._survey) })[0];
1847                 var question = survey.questions().filter(
1848                     function(m){
1849                         return (m.id() == w._question) })[0];
1850                 label = survey.name() + ' : ' + question.question();
1851             } else {
1852                 label = 'FIXME';
1853             }
1854         } else {
1855             label = w.idlField.label;
1856         }
1857         if (temp != w._wtype) {
1858             temp = w._wtype;
1859             s += '-------\r\n';
1860         }
1861         s += label + ':\t' + (typeof val == 'object' ? '' : val) + '\r\n';
1862     }
1863     s += '=-=-=-=\r\n';
1864     return s;
1865 }
1866
1867 openils.Util.addOnLoad(load);