]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/actor/user/register.js
avoid race condition with post-save patron editor refresh and replicated databases
[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
49 var dupeUsrname = false;
50 var dupeBarcode = false;
51
52 if(!window.xulG) var xulG = null;
53
54
55 function load() {
56     staff = new openils.User().user;
57     pcrud = new openils.PermaCrud();
58     cgi = new openils.CGI();
59     cloneUser = cgi.param('clone');
60     var userId = cgi.param('usr');
61     var stageUname = cgi.param('stage');
62
63     if(xulG) {
64             if(xulG.ses) openils.User.authtoken = xulG.ses;
65             if(typeof xulG.clone != 'undefined') cloneUser = xulG.clone;
66         if(typeof xulG.usr != 'undefined') userId = xulG.usr
67         if(typeof xulG.params != 'undefined') {
68             var parms = xulG.params;
69                 if(typeof parms.ses != 'undefined') 
70                 openils.User.authtoken = parms.ses;
71                 if(typeof parms.clone != 'undefined') 
72                 cloneUser = parms.clone;
73             if(typeof parms.usr != 'undefined')
74                 userId = parms.usr;
75             if(typeof parms.stage != 'undefined')
76                 stageUname = parms.stage
77         }
78     }
79
80     orgSettings = fieldmapper.aou.fetchOrgSettingBatch(staff.ws_ou(), [
81         'global.password_regex',
82         'global.juvenile_age_threshold',
83         'patron.password.use_phone',
84         'ui.patron.default_inet_access_level',
85         'ui.patron.default_ident_type',
86         'ui.patron.default_country',
87         'ui.patron.registration.require_address',
88         'circ.holds.behind_desk_pickup_supported',
89         'circ.patron_edit.clone.copy_address'
90     ]);
91
92     for(k in orgSettings)
93         if(orgSettings[k])
94             orgSettings[k] = orgSettings[k].value;
95
96     uEditCloneCopyAddr = orgSettings['circ.patron_edit.clone.copy_address'];
97     uEditUsePhonePw = orgSettings['patron.password.use_phone'];
98     uEditFetchUserSettings(userId);
99
100     if(userId) {
101         patron = uEditLoadUser(userId);
102     } else {
103         if(stageUname) {
104             patron = uEditLoadStageUser(stageUname);
105         } else {
106             patron = uEditNewPatron();
107             if(cloneUser) 
108                 uEditCopyCloneData(patron);
109         }
110     }
111
112
113     var list = pcrud.search('fdoc', {fm_class:fmClasses});
114     for(var i in list) {
115         var doc = list[i];
116         if(!fieldDoc[doc.fm_class()])
117             fieldDoc[doc.fm_class()] = {};
118         fieldDoc[doc.fm_class()][doc.field()] = doc;
119     }
120
121     tbody = dojo.byId('uedit-tbody');
122
123     addrTemplateRows = dojo.query('tr[type=addr-template]', tbody);
124     dojo.forEach(addrTemplateRows, function(row) { row.parentNode.removeChild(row); } );
125     statCatTemplate = tbody.removeChild(dojo.byId('stat-cat-row-template'));
126     surveyTemplate = tbody.removeChild(dojo.byId('survey-row-template'));
127     surveyQuestionTemplate = tbody.removeChild(dojo.byId('survey-question-row-template'));
128
129     loadStaticFields();
130     if(patron.isnew() && patron.addresses().length == 0) 
131         uEditNewAddr(null, uEditAddrVirtId, true);
132     else loadAllAddrs();
133     loadStatCats();
134     loadSurveys();
135     checkClaimsReturnCountPerm();
136     checkClaimsNoCheckoutCountPerm();
137
138     dojo.connect(replaceBarcode, 'onClick', replaceCardHandler);
139     dojo.connect(allCards, 'onClick', drawAllCards);
140     if(patron.cards().length > 1)
141         dojo.removeClass(dojo.byId('uedit-all-barcodes'), 'hidden');
142
143     var input = findWidget('ac', 'barcode');
144     if (patron.isnew()) {
145         replaceBarcode.attr('disabled', true);
146     } else {
147         input.widget.attr('disabled', true).attr('readOnly', true);
148     }
149 }
150
151
152 function drawAllCards() {
153
154     var tbody = dojo.byId('uedit-all-cards-tbody');
155     if(!allCardsTemplate) {
156         allCardsTemplate = tbody.removeChild(dojo.byId('uedit-all-cards-tr-template'));
157     } else {
158         while(tbody.childNodes[0])
159             tbody.removeChild(tbody.childNodes[0]);
160     }
161
162     var first = true;
163     dojo.forEach(
164         [patron.card()].concat(patron.cards()), // grab the main card first
165         function(card) {
166             if(!first) {
167                 if(card.id() == patron.card().id())
168                     return;
169             }
170             var row = allCardsTemplate.cloneNode(true);
171             getByName(row, 'barcode').innerHTML = card.barcode();
172             getByName(row, 'active').appendChild(
173                 openils.Util.isTrue(card.active()) ? 
174                     dojo.byId('true').cloneNode(true) :
175                     dojo.byId('false').cloneNode(true)
176             ); 
177
178             tbody.appendChild(row);
179             first = false;
180         }
181     );
182
183     allCardsDialog.show();
184 }
185
186 /**
187  * Mark the current card inactive, create a new primary card
188  */
189 function replaceCardHandler() {
190     var input = findWidget('ac', 'barcode');
191     input.widget.attr('disabled', false).attr('readOnly', false).attr('value', null).focus();
192     replaceBarcode.attr('disabled', true);
193     
194     // pull old card off the cards list so we don't have a dupe sitting in there
195     var old = patron.cards().filter(function(c){return (c.id() == patron.card().id())})[0];
196     old.active('f');
197     old.ischanged(1);
198
199     var newc = new fieldmapper.ac();
200     newc.id(uEditCardVirtId--);
201     newc.isnew(1);
202     newc.active('t');
203     patron.card(newc);
204     var t = patron.cards();
205         if (!t) { t = []; }
206         t.push(newc);
207         patron.cards(t);
208 }
209
210
211 /**
212  * Loads a staged user and turns them into something the editor can understand
213  */
214 function uEditLoadStageUser(stageUname) {
215
216     var data = fieldmapper.standardRequest(
217         ['open-ils.actor', 'open-ils.actor.user.stage.retrieve.by_username'],
218         { params : [openils.User.authtoken, stageUname] }
219     );
220
221     stageUser = data.user;
222     patron = uEditNewPatron();
223
224     if(!stageUser) 
225         return patron;
226
227     // copy the data into our new user object
228     for(var key in fieldmapper.IDL.fmclasses.stgu.field_map) {
229         if(fieldmapper.IDL.fmclasses.au.field_map[key] && !fieldmapper.IDL.fmclasses.stgu.field_map[key].virtual) {
230             if(data.user[key]() !== null)
231                 patron[key]( data.user[key]() );
232         }
233     }
234
235     // copy the data into our new address objects
236     // TODO: uses the first mailing address only
237     if(data.mailing_addresses.length) {
238
239         var mail_addr = new fieldmapper.aua();
240         mail_addr.id(-1); // virtual ID
241         mail_addr.usr(-1);
242         mail_addr.isnew(1);
243         patron.mailing_address(mail_addr);
244         var t = patron.addresses();
245             if (!t) { t = []; }
246             t.push(mail_addr);
247             patron.addresses(t);
248
249         for(var key in fieldmapper.IDL.fmclasses.stgma.field_map) {
250             if(fieldmapper.IDL.fmclasses.aua.field_map[key] && !fieldmapper.IDL.fmclasses.stgma.field_map[key].virtual) {
251                 if(data.mailing_addresses[0][key]() !== null)
252                     mail_addr[key]( data.mailing_addresses[0][key]() );
253             }
254         }
255     }
256     
257     // copy the data into our new address objects
258     // TODO uses the first billing address only
259     if(data.billing_addresses.length) {
260
261         var bill_addr = new fieldmapper.aua();
262         bill_addr.id(-2); // virtual ID
263         bill_addr.usr(-1);
264         bill_addr.isnew(1);
265         patron.billing_address(bill_addr);
266         var t = patron.addresses();
267             if (!t) { t = []; }
268             t.push(bill_addr);
269             patron.addresses(t);
270
271         for(var key in fieldmapper.IDL.fmclasses.stgba.field_map) {
272             if(fieldmapper.IDL.fmclasses.aua.field_map[key] && !fieldmapper.IDL.fmclasses.stgba.field_map[key].virtual) {
273                 if(data.billing_addresses[0][key]() !== null)
274                     bill_addr[key]( data.billing_addresses[0][key]() );
275             }
276         }
277     }
278
279     // TODO: uses the first card only
280     if(data.cards.length) {
281         var card = new fieldmapper.ac();
282         card.id(-1); // virtual ID
283         patron.card().barcode(data.cards[0].barcode());
284     }
285
286     return patron;
287 }
288
289 /*
290  * clone the home org, phone numbers, and billing/mailing address
291  */
292 function uEditCopyCloneData(patron) {
293     cloneUserObj = uEditLoadUser(cloneUser);
294
295     var cloneFields = [
296         'home_ou', 
297         'day_phone', 
298         'evening_phone', 
299         'other_phone',
300         'usrgroup'
301     ];
302
303     if(!uEditCloneCopyAddr) 
304         cloneFields = cloneFields.concat(['mailing_address', 'billing_address']);
305
306     dojo.forEach(
307         cloneFields, 
308         function(field) {
309             patron[field](cloneUserObj[field]());
310         }
311     );
312
313     if(uEditCloneCopyAddr) {
314         var billAddr, mailAddr;
315
316         // copy the billing and mailing addresses into new addresses
317         function cloneAddr(addr) {
318             var newAddr = addr.clone();
319             newAddr.isnew(true);
320             newAddr.id(uEditAddrVirtId--);
321             newAddr.usr(patron.id());
322             patron.addresses().push(newAddr);
323             return newAddr;
324         }
325
326         if(billAddr = cloneUserObj.billing_address()) 
327             patron.billing_address(cloneAddr(billAddr));
328
329         if(mailAddr = cloneUserObj.mailing_address()) {
330             if (billAddr && billAddr.id() == mailAddr.id()) {
331                 patron.mailing_address(patron.billing_address());
332             } else {
333                 patron.mailing_address(cloneAddr(mailAddr));
334             }
335         }
336
337         if(!billAddr) // if there was no billing addr, use the mailing addr
338             patron.billing_address(patron.mailing_address());
339
340     } else {
341
342         // link the billing and mailing addresses
343         if(patron.billing_address()) {
344             var t = patron.addresses();
345                 if (!t) { t = []; }
346                 t.push(patron.billing_address());
347                 patron.addresses(t);
348         }
349
350         if(patron.mailing_address() && (
351                 patron.addresses().length == 0 || 
352                 patron.mailing_address().id() != patron.billing_address().id()) ) {
353             var t = patron.addresses();
354                 if (!t) { t = []; }
355                 t.push(patron.mailing_address());
356                 patron.addresses(t);
357         }
358     }
359 }
360
361
362 function uEditFetchUserSettings(userId) {
363     
364     var baseNode = fieldmapper.aou.findOrgUnit(staff.ws_ou());
365     var orgs = fieldmapper.aou.orgNodeTrail(baseNode);
366     orgs = orgs.map(function(node) { return node.id(); });
367
368     /* fetch any user setting types we need + any that offer opt-in */
369     userSettingTypes = pcrud.search('cust', {
370         '-or' : [
371             {name:['circ.holds_behind_desk']}, 
372             {name : {
373                 'in': {
374                     select : {atevdef : ['opt_in_setting']}, 
375                     from : 'atevdef',
376                     // we only care about opt-in settings for event_defs our users encounter
377                     where : {'+atevdef' : {owner : orgs}}
378                 }
379             }}
380         ]
381     });
382
383     var names = userSettingTypes.map(function(obj) { return obj.name() });
384
385     /* fetch any values set for this user */
386     userSettings = fieldmapper.standardRequest(
387         ['open-ils.actor', 'open-ils.actor.patron.settings.retrieve.authoritative'],
388         {params : [openils.User.authtoken, userId, names]});
389 }
390
391
392 function uEditLoadUser(userId) {
393     var patron = fieldmapper.standardRequest(
394         ['open-ils.actor', 'open-ils.actor.user.fleshed.retrieve.authoritative'],
395         {params : [openils.User.authtoken, userId]}
396     );
397     openils.Event.parse_and_raise(patron);
398     return patron;
399 }
400
401 function loadStaticFields() {
402     for(var idx = 0; tbody.childNodes[idx]; idx++) {
403         var row = tbody.childNodes[idx];
404         if(row.nodeType != row.ELEMENT_NODE) continue;
405         var fmcls = row.getAttribute('fmclass');
406         if(fmcls) {
407             fleshFMRow(row, fmcls);
408         } else {
409
410             if(row.id == 'uedit-settings-divider') {
411
412                 var template = tbody.removeChild(dojo.byId('uedit-user-setting-template'));
413                 dojo.forEach(userSettingTypes, function(type) { uEditDrawSettingRow(tbody, row, template, type); } );
414
415                 if(userSettingTypes.length > 1 || orgSettings['circ.holds.behind_desk_pickup_supported']) {
416                     openils.Util.show('uedit-settings-divider', 'table-row');
417                 }
418             }
419         }
420     }
421 }
422
423 function uEditDrawSettingRow(tbody, dividerRow, template, stype) {
424     var row = template.cloneNode(true);
425     row.setAttribute('user_setting', stype.name());
426     getByName(row, 'label').innerHTML = stype.label();
427     var cb = new dijit.form.CheckBox({scrollOnFocus:false}, getByName(row, 'widget'));
428     cb.attr('value', userSettings[stype.name()]);
429     dojo.connect(cb, 'onChange', function(newVal) { userSettingsToUpdate[stype.name()] = newVal; });
430     tbody.insertBefore(row, dividerRow.nextSibling);
431     openils.Util.show(row, 'table-row');
432 }
433
434 function uEditUpdateUserSettings(userId) {
435     return fieldmapper.standardRequest(
436         ['open-ils.actor', 'open-ils.actor.patron.settings.update'],
437         {params : [openils.User.authtoken, userId, userSettingsToUpdate]});
438 }
439
440 function loadAllAddrs() {
441     dojo.forEach(patron.addresses(),
442         function(addr) {
443             uEditNewAddr(null, addr.id());
444         }
445     );
446 }
447
448 function loadStatCats() {
449
450     statCats = fieldmapper.standardRequest(
451         ['open-ils.circ', 'open-ils.circ.stat_cat.actor.retrieve.all'],
452         {params : [openils.User.authtoken, staff.ws_ou()]}
453     );
454
455     // draw stat cats
456     for(var idx in statCats) {
457         var stat = statCats[idx];
458         var row = statCatTemplate.cloneNode(true);
459         row.id = 'stat-cat-row-' + idx;
460         row.setAttribute('stat_cat_owner',stat.owner());
461         row.setAttribute('stat_cat_name',stat.name());
462         row.setAttribute('stat_cat_id',stat.id());
463         tbody.appendChild(row);
464         getByName(row, 'name').innerHTML = stat.name();
465         var valtd = getByName(row, 'widget');
466         var span = valtd.appendChild(document.createElement('span'));
467         var store = new dojo.data.ItemFileReadStore(
468                 {data:fieldmapper.actsc.toStoreData(stat.entries())});
469         var comboBox = new dijit.form.ComboBox({store:store,scrollOnFocus:false}, span);
470         comboBox.labelAttr = 'value';
471         comboBox.searchAttr = 'value';
472
473         comboBox._wtype = 'statcat';
474         comboBox._statcat = stat.id();
475         widgetPile.push(comboBox); 
476
477         // populate existing cats
478         var map = patron.stat_cat_entries().filter(
479             function(mp) { return (mp.stat_cat() == stat.id()) })[0];
480         if(map) comboBox.attr('value', map.stat_cat_entry()); 
481
482     }
483 }
484
485 function loadSurveys() {
486
487     surveys = fieldmapper.standardRequest(
488         ['open-ils.circ', 'open-ils.circ.survey.retrieve.all'],
489         {params : [openils.User.authtoken]}
490     );
491
492     // draw surveys
493     for(var idx in surveys) {
494         var survey = surveys[idx];
495         var srow = surveyTemplate.cloneNode(true);
496         tbody.appendChild(srow);
497         getByName(srow, 'name').innerHTML = survey.name();
498
499         for(var q in survey.questions()) {
500             var quest = survey.questions()[q];
501             var qrow = surveyQuestionTemplate.cloneNode(true);
502             tbody.appendChild(qrow);
503             getByName(qrow, 'question').innerHTML = quest.question();
504
505             var span = getByName(qrow, 'answers').appendChild(document.createElement('span'));
506             var store = new dojo.data.ItemFileReadStore(
507                 {data:fieldmapper.asva.toStoreData(quest.answers())});
508             var select = new dijit.form.FilteringSelect({store:store,scrollOnFocus:false}, span);
509             if (! openils.Util.isTrue(survey.required())) {
510                 select.isValid = function() { return true; };
511             }
512             select.labelAttr = 'answer';
513             select.searchAttr = 'answer';
514
515             select._wtype = 'survey';
516             select._survey = survey.id();
517             select._question = quest.id();
518             widgetPile.push(select); 
519         }
520     }
521 }
522
523
524 function fleshFMRow(row, fmcls, args) {
525     var fmfield = row.getAttribute('fmfield');
526     var wclass = row.getAttribute('wclass');
527     var wstyle = row.getAttribute('wstyle');
528     var wconstraints = row.getAttribute('wconstraints');
529     /* use CSS to set the zindex for widgets you want to disable. */
530     var disabled = dojo.style(row, 'zIndex') == -1 ? true : false;
531
532     var isPasswd2 = (fmfield == 'passwd2');
533     if(isPasswd2) fmfield = 'passwd';
534     var fieldIdl = fieldmapper.IDL.fmclasses[fmcls].field_map[fmfield];
535     if(!args) args = {};
536
537     var existing = dojo.query('td', row);
538     var htd = existing[0] || row.appendChild(document.createElement('td'));
539     var ltd = existing[1] || row.appendChild(document.createElement('td'));
540     var wtd = existing[2] || row.appendChild(document.createElement('td'));
541
542     openils.Util.addCSSClass(htd, 'uedit-help');
543     if(fieldDoc[fmcls] && fieldDoc[fmcls][fmfield]) {
544         var link = dojo.byId('uedit-help-template').cloneNode(true);
545         link.id = '';
546         link.onclick = function() { ueLoadContextHelp(fmcls, fmfield) };
547         openils.Util.removeCSSClass(link, 'hidden');
548         htd.appendChild(link);
549     }
550
551     if(!ltd.textContent) {
552         var span = document.createElement('span');
553         ltd.appendChild(document.createTextNode(fieldIdl.label));
554     }
555
556     span = document.createElement('span');
557     wtd.appendChild(span);
558
559     var fmObject = null;
560     switch(fmcls) {
561         case 'au' : fmObject = patron; break;
562         case 'ac' : fmObject = patron.card(); break;
563         case 'aua' : 
564             fmObject = patron.addresses().filter(
565                 function(i) { return (i.id() == args.addr) })[0];
566             if(fmObject && fmObject.usr() != patron.id())
567                 disabled = true;
568             break;
569     }
570
571     var required = row.getAttribute('required') == 'required';
572
573     // password data is not fetched/required/displayed for existing users
574     if(!patron.isnew() && 'passwd' == fmfield)
575         required = false;
576
577     var dijitArgs = {
578         style: wstyle, 
579         required : required,
580         constraints : (wconstraints) ? eval('('+wconstraints+')') : {}, // the ()'s prevent Invalid Label errors with eval
581         disabled : disabled
582     };
583
584     if(fmcls == 'au' && fmfield == 'passwd') {
585         if (orgSettings['global.password_regex']) {
586             dijitArgs.regExp = orgSettings['global.password_regex'];
587         }
588     }
589
590     // TODO RSN: Add Setting!
591     if(fmcls == 'au' && fmfield == 'dob')
592         dijitArgs.popupClass = "";
593
594     var value = row.getAttribute('wvalue');
595     if(value !== null)
596         dijitArgs.value = value;
597
598     var wargs = {
599         idlField : fieldIdl,
600         fmObject : fmObject,
601         fmClass : fmcls,
602         parentNode : span,
603         widgetClass : wclass,
604         dijitArgs : dijitArgs,
605         orgDefaultsToWs : true,
606         orgLimitPerms : ['UPDATE_USER'],
607     };
608
609     if(fmfield == 'profile') {
610         // fetch profile groups non-async so existing expire_date is
611         // not overwritten when the profile groups arrive and update
612         wargs.forceSync = true;
613         wargs.disableQuery = {usergroup : 'f'};
614     } else {
615         wargs.forceSync = false;
616     }
617
618     var widget = new openils.widget.AutoFieldWidget(wargs);
619     widget.build();
620
621     // now put it back before we register the widget
622     if(isPasswd2) fmfield = 'passwd2';
623
624     widget._wtype = fmcls;
625     widget._fmfield = fmfield;
626     widget._addr = args.addr;
627     widgetPile.push(widget);
628     attachWidgetEvents(fmcls, fmfield, widget);
629     return widget;
630 }
631
632 function findWidget(wtype, fmfield, callback) {
633     return widgetPile.filter(
634         function(i){
635             if(i._wtype == wtype && i._fmfield == fmfield) {
636                 if(callback) return callback(i);
637                 return true;
638             }
639         }
640     ).pop();
641 }
642
643 /**
644  * if the user does not have the UPDATE_PATRON_CLAIM_RETURN_COUNT, 
645  * they are not allowed to directly alter the claim return count. 
646  * This function checks the perm and disable/enables the widget.
647  */
648 function checkClaimsReturnCountPerm() {
649     new openils.User().getPermOrgList(
650         'UPDATE_PATRON_CLAIM_RETURN_COUNT',
651         function(orgList) { 
652             var cr = findWidget('au', 'claims_returned_count');
653             if(orgList.indexOf(patron.home_ou()) == -1) 
654                 cr.widget.attr('disabled', true);
655             else
656                 cr.widget.attr('disabled', false);
657         },
658         true, 
659         true
660     );
661 }
662
663
664 function checkClaimsNoCheckoutCountPerm() {
665     new openils.User().getPermOrgList(
666         'UPDATE_PATRON_CLAIM_NEVER_CHECKED_OUT_COUNT',
667         function(orgList) { 
668             var cr = findWidget('au', 'claims_never_checked_out_count');
669             if(orgList.indexOf(patron.home_ou()) == -1) 
670                 cr.widget.attr('disabled', true);
671             else
672                 cr.widget.attr('disabled', false);
673         },
674         true, 
675         true
676     );
677 }
678
679
680 function attachWidgetEvents(fmcls, fmfield, widget) {
681
682     if(fmcls == 'ac') {
683         if(fmfield == 'barcode') {
684             dojo.connect(widget.widget, 'onChange',
685                 function() {
686                     var barcode = this.attr('value');
687                     dupeBarcode = false;
688                     dojo.addClass(dojo.byId('uedit-dupe-barcode-warning'), 'hidden');
689                     fieldmapper.standardRequest(
690                         ['open-ils.actor', 'open-ils.actor.barcode.exists'],
691                         {
692                             params: [openils.User.authtoken, barcode],
693                             oncomplete : function(r) {
694                                 var res = openils.Util.readResponse(r);
695                                 if(res == '1') {
696                                     dupeBarcode = true;
697                                     dojo.removeClass(dojo.byId('uedit-dupe-barcode-warning'), 'hidden');
698                                 } else {
699                                     dupeBarcode = false;
700                                     dojo.addClass(dojo.byId('uedit-dupe-barcode-warning'), 'hidden');
701                                     var un = findWidget('au', 'usrname');
702                                     if(!un.widget.attr('value'))
703                                         un.widget.attr('value', barcode);
704                                 }
705                             }
706                         }
707                     );
708                 }
709             );
710             return;
711         }
712     }
713
714     if(fmcls == 'au') {
715         switch(fmfield) {
716
717             case 'usrname':
718                 dojo.connect(widget.widget, 'onChange', 
719                     function() {
720                         var input = findWidget('au', 'usrname');
721                         var usrname = input.widget.attr('value');
722
723                         if(!usrname) {
724                             dupeUsrname = false;
725                             dojo.addClass(dojo.byId('uedit-dupe-username-warning'), 'hidden');
726                             return;
727                         }
728
729                         fieldmapper.standardRequest(
730                             ['open-ils.actor', 'open-ils.actor.username.exists'],
731                             {
732                                 params: [openils.User.authtoken, usrname],
733                                 oncomplete : function(r) {
734                                     var res = openils.Util.readResponse(r);
735                                     if(res) {
736                                         dupeUsrname = true;
737                                         dojo.removeClass(dojo.byId('uedit-dupe-username-warning'), 'hidden');
738                                     } else {
739                                         dupeUsrname = false;
740                                         dojo.addClass(dojo.byId('uedit-dupe-username-warning'), 'hidden');
741                                     }
742                                 }
743                             }
744                         );
745                     }   
746                 );
747
748                 return;
749
750             case 'profile': // when the profile changes, update the expire date
751                 dojo.connect(widget.widget, 'onChange', 
752                     function() {
753                         var self = this;
754                         var expireWidget = findWidget('au', 'expire_date');
755                         function found(items) {
756                             if(items.length == 0) return;
757                             var item = items[0];
758                             var interval = self.store.getValue(item, 'perm_interval');
759                             expireWidget.widget.attr('value', dojo.date.add(new Date(), 
760                                 'second', openils.Util.intervalToSeconds(interval)));
761                         }
762                         this.store.fetch({onComplete:found, query:{id:this.attr('value')}});
763                     }
764                 );
765                 return;
766
767             case 'dob':
768                 dojo.connect(widget.widget, 'onChange',
769                     function(newDob) {
770                         if(!newDob) return;
771                         var oldDob = patron.dob();
772                         if(dojo.date.stamp.fromISOString(oldDob) == newDob) return;
773
774                         var juvInterval = orgSettings['global.juvenile_age_threshold'] || '18 years';
775                         var juvWidget = findWidget('au', 'juvenile');
776                         var base = new Date();
777                         base.setTime(base.getTime() - Number(openils.Util.intervalToSeconds(juvInterval) + '000'));
778
779                         if(newDob <= base) // older than global.juvenile_age_threshold
780                             juvWidget.widget.attr('value', false);
781                         else
782                             juvWidget.widget.attr('value', true);
783                     }
784                 );
785                 return;
786
787             case 'first_given_name':
788             case 'family_name':
789                 dojo.connect(widget.widget, 'onChange',
790                     function(newVal) { uEditDupeSearch('name', newVal); });
791                 return;
792
793             case 'email':
794                 dojo.connect(widget.widget, 'onChange',
795                     function(newVal) { uEditDupeSearch('email', newVal); });
796                 return;
797
798             case 'ident_value':
799             case 'ident_value2':
800                 dojo.connect(widget.widget, 'onChange',
801                     function(newVal) { uEditDupeSearch('ident', newVal); });
802                 return;
803
804             case 'day_phone':
805                 // if configured, use the last for digits of the day phone number as the password
806                 if(uEditUsePhonePw && patron.isnew()) {
807                     dojo.connect(widget.widget, 'onChange',
808                         function(newVal) {
809                             if(newVal && newVal.length >= 4) {
810                                 var pw1 = findWidget('au', 'passwd').widget;
811                                 var pw2 = findWidget('au', 'passwd2').widget;
812                                 pw1.attr('value', newVal.substring(newVal.length - 4));
813                                 pw2.attr('value', newVal.substring(newVal.length - 4));
814                             }
815                         }
816                     );
817                 }
818             case 'evening_phone':
819             case 'other_phone':
820                 dojo.connect(widget.widget, 'onChange',
821                     function(newVal) { uEditDupeSearch('phone', newVal); });
822                 return;
823
824             case 'home_ou':
825                 dojo.connect(widget.widget, 'onChange',
826                     function(newVal) { 
827                         checkClaimsReturnCountPerm(); 
828                         checkClaimsNoCheckoutCountPerm();
829                     }
830                 );
831                 return;
832
833             case 'passwd':
834                 dojo.connect(widget.widget, 'onChange',
835                     function(newVal) {
836                         var pw1 = findWidget('au', 'passwd').widget;
837                         var pw2 = findWidget('au', 'passwd2').widget;
838                         var preserved_value = pw2.attr('value');
839                         // Ensure that the pw2 field match the pw1 field to validate
840                         pw2.regExp = newVal.replace(/([.\\^$*+?\(\)\[\]\{\}])/g, '\\$1');
841                         pw2.reset();
842                         pw2.attr('value',preserved_value);
843                     });
844                 return;
845         }
846     }
847
848     if(fmclass = 'aua') {
849         switch(fmfield) {
850             case 'post_code':
851                 dojo.connect(widget.widget, 'onChange',
852                     function(e) { 
853                         fieldmapper.standardRequest(
854                             ['open-ils.search', 'open-ils.search.zip'],
855                             {   async: true,
856                                 params: [e],
857                                 oncomplete : function(r) {
858                                     var res = openils.Util.readResponse(r);
859                                     if(!res) return;
860                                     var callback = function(w) { return w._addr == widget._addr; };
861                                     if(res.city) findWidget('aua', 'city', callback).widget.attr('value', res.city);
862                                     if(res.state) findWidget('aua', 'state', callback).widget.attr('value', res.state);
863                                     if(res.county) findWidget('aua', 'county', callback).widget.attr('value', res.county);
864                                     if(res.alert) alert(res.alert);
865                                 }
866                             }
867                         );
868                     }
869                 );
870                 return;
871
872             case 'street1':
873             case 'street2':
874             case 'city':
875                 dojo.connect(widget.widget, 'onChange',
876                     function(e) {
877                         var callback = function(w) { return w._addr == widget._addr; };
878                         var args = {
879                             street1 : findWidget('aua', 'street1', callback).widget.attr('value'),
880                             street2 : findWidget('aua', 'street2', callback).widget.attr('value'),
881                             city : findWidget('aua', 'city', callback).widget.attr('value'),
882                             post_code : findWidget('aua', 'post_code', callback).widget.attr('value')
883                         };
884                         if(args.street1 && args.city && args.post_code)
885                             uEditDupeSearch('address', args); 
886                     }
887                 );
888                 return;
889         }
890     }
891 }
892
893 function uEditDupeSearch(type, value) {
894     if(!value) return;
895     var search;
896     switch(type) {
897
898         case 'name':
899             openils.Util.hide('uedit-dupe-names-link');
900             var fname = findWidget('au', 'first_given_name').widget.attr('value');
901             var lname = findWidget('au', 'family_name').widget.attr('value');
902             if( !(fname && lname) ) return;
903             search = {
904                 first_given_name : {value : fname, group : 0},
905                 family_name : {value : lname, group : 0},
906             };
907             break;
908
909         case 'email':
910             openils.Util.hide('uedit-dupe-email-link');
911             search = {email : {value : value, group : 0}};
912             break;
913
914         case 'ident':
915             openils.Util.hide('uedit-dupe-ident-link');
916             search = {ident : {value : value, group : 2}};
917             break;
918
919         case 'phone':
920             openils.Util.hide('uedit-dupe-phone-link');
921             search = {phone : {value : value, group : 2}};
922             break;
923
924         case 'address':
925             openils.Util.hide('uedit-dupe-address-link');
926             search = {};
927             dojo.forEach(['street1', 'street2', 'city', 'post_code'],
928                 function(field) {
929                     if(value[field])
930                         search[field] = {value : value[field], group: 1};
931                 }
932             );
933             break;
934     }
935
936     // find possible duplicate patrons
937     fieldmapper.standardRequest(
938         ['open-ils.actor', 'open-ils.actor.patron.search.advanced'],
939         {   async: true,
940             params: [openils.User.authtoken, search],
941             oncomplete : function(r) {
942                 var resp = openils.Util.readResponse(r);
943                 resp = resp.filter(function(id) { return (id != patron.id()); });
944
945                 if(resp && resp.length > 0) {
946
947                     openils.Util.hide('uedit-help-div');
948                     openils.Util.show('uedit-dupe-div');
949                     var link;
950
951                     switch(type) {
952                         case 'name':
953                             link = dojo.byId('uedit-dupe-names-link');
954                             link.innerHTML = dojo.string.substitute(localeStrings.DUPE_PATRON_NAME, [resp.length]);
955                             break;
956                         case 'email':
957                             link = dojo.byId('uedit-dupe-email-link');
958                             link.innerHTML = dojo.string.substitute(localeStrings.DUPE_PATRON_EMAIL, [resp.length]);
959                             break;
960                         case 'ident':
961                             link = dojo.byId('uedit-dupe-ident-link');
962                             link.innerHTML = dojo.string.substitute(localeStrings.DUPE_PATRON_IDENT, [resp.length]);
963                             break;
964                         case 'phone':
965                             link = dojo.byId('uedit-dupe-phone-link');
966                             link.innerHTML = dojo.string.substitute(localeStrings.DUPE_PATRON_PHONE, [resp.length]);
967                             break;
968                         case 'address':
969                             link = dojo.byId('uedit-dupe-address-link');
970                             link.innerHTML = dojo.string.substitute(localeStrings.DUPE_PATRON_ADDR, [resp.length]);
971                             break;
972                     }
973
974                     openils.Util.show(link);
975                     link.onclick = function() {
976                         search.search_sort = js2JSON(["penalties", "family_name", "first_given_name"]);
977                         if(window.xulG)
978                             window.xulG.spawn_search(search);
979                         else
980                             console.log("running XUL patron search " + js2JSON(search));
981                     }
982                 }
983             }
984         }
985     );
986 }
987
988 function getByName(node, name) {
989     return dojo.query('[name='+name+']', node)[0];
990 }
991
992
993 function ueLoadContextHelp(fmcls, fmfield) {
994     openils.Util.hide('uedit-dupe-div');
995     openils.Util.show('uedit-help-div');
996     dojo.byId('uedit-help-field').innerHTML = fieldmapper.IDL.fmclasses[fmcls].field_map[fmfield].label;
997     dojo.byId('uedit-help-text').innerHTML = fieldDoc[fmcls][fmfield].string();
998 }
999
1000
1001 /* creates a new patron object with card attached */
1002 function uEditNewPatron() {
1003     patron = new au();
1004     patron.isnew(1);
1005     patron.id(-1);
1006     card = new ac();
1007     card.id(uEditCardVirtId--);
1008     card.isnew(1);
1009     patron.active(1);
1010     patron.card(card);
1011     patron.cards([card]);
1012     patron.net_access_level(orgSettings['ui.patron.default_inet_access_level'] || 1);
1013     patron.ident_type(orgSettings['ui.patron.default_ident_type']);
1014     patron.stat_cat_entries([]);
1015     patron.survey_responses([]);
1016     patron.addresses([]);
1017     uEditMakeRandomPw(patron);
1018     return patron;
1019 }
1020
1021 function uEditMakeRandomPw(patron) {
1022     if(uEditUsePhonePw) return;
1023     var rand  = Math.random();
1024     rand = parseInt(rand * 10000) + '';
1025     while(rand.length < 4) rand += '0';
1026 /*
1027     appendClear($('ue_password_plain'),text(rand));
1028     unHideMe($('ue_password_gen'));
1029 */
1030     patron.passwd(rand);
1031     return rand;
1032 }
1033
1034 function uEditWidgetVal(w) {
1035     var val = (w.getFormattedValue) ? w.getFormattedValue() : w.attr('value');
1036     if(val === '') val = null;
1037     return val;
1038 }
1039
1040 function uEditSave() { _uEditSave(); }
1041 function uEditSaveClone() { _uEditSave(true); }
1042
1043 function _uEditSave(doClone) {
1044
1045     if ( (! myForm.isValid()) || dupeUsrname || dupeBarcode ) {
1046         alert(localeStrings.INVALID_FORM);
1047         return;
1048     }
1049
1050     for(var idx in widgetPile) {
1051         var w = widgetPile[idx];
1052         var val = uEditWidgetVal(w);
1053
1054         switch(w._wtype) {
1055             case 'au':
1056                 if(w._fmfield != 'passwd2')
1057                     patron[w._fmfield](val);
1058                 break;
1059
1060             case 'ac':
1061                 patron.card()[w._fmfield](val);
1062                 break;
1063
1064             case 'aua':
1065                 var addr = patron.addresses().filter(function(i){return (i.id() == w._addr)})[0];
1066                 if(!addr) {
1067                     addr = new fieldmapper.aua();
1068                     addr.id(w._addr);
1069                     addr.isnew(1);
1070                     addr.usr(patron.id());
1071                     addr.country(orgSettings['ui.patron.default_country']);
1072                     var t = patron.addresses();
1073                         if (!t) { t = []; }
1074                         t.push(addr);
1075                         patron.addresses(t);
1076                 } else {
1077                     if(addr[w._fmfield]() != val)
1078                         addr.ischanged(1);
1079                 }
1080                 addr[w._fmfield](val);
1081
1082                 if(dojo.byId('uedit-billing-address-' + addr.id()).checked) 
1083                     patron.billing_address(addr.id());
1084
1085                 if(dojo.byId('uedit-mailing-address-' + addr.id()).checked)
1086                     patron.mailing_address(addr.id());
1087
1088                 break;
1089
1090             case 'survey':
1091                 if(val == null) break;
1092                 var resp = new fieldmapper.asvr();
1093                 resp.isnew(1);
1094                 resp.survey(w._survey)
1095                 resp.usr(patron.id());
1096                 resp.question(w._question)
1097                 resp.answer(val);
1098                 var t = patron.survey_responses();
1099                     if (!t) { t = []; }
1100                     t.push(resp);
1101                     patron.survey_responses(t);
1102                 break;
1103
1104             case 'statcat':
1105                 if(val == null) break;
1106
1107                 var map = patron.stat_cat_entries().filter(
1108                     function(m){
1109                         return (m.stat_cat() == w._statcat) })[0];
1110
1111                 if(map) {
1112                     if(map.stat_cat_entry() == val) 
1113                         break;
1114                     map.ischanged(1);
1115                 } else {
1116                     map = new fieldmapper.actscecm();
1117                     map.isnew(1);
1118                 }
1119
1120                 map.stat_cat(w._statcat);
1121                 map.stat_cat_entry(val);
1122                 map.target_usr(patron.id());
1123                 var t = patron.stat_cat_entries();
1124                     if (!t) { t = []; }
1125                     t.push(map);
1126                     patron.stat_cat_entries(t);
1127                 break;
1128         }
1129     }
1130
1131     patron.ischanged(1);
1132     fieldmapper.standardRequest(
1133         ['open-ils.actor', 'open-ils.actor.patron.update'],
1134         {   async: true,
1135             params: [openils.User.authtoken, patron],
1136             oncomplete: function(r) {
1137                 newPatron = openils.Util.readResponse(r);
1138                 if(newPatron) {
1139                     uEditUpdateUserSettings(newPatron.id());
1140                     if(stageUser) uEditRemoveStage();
1141                     uEditFinishSave(newPatron, doClone);
1142                 }
1143             }
1144         }
1145     );
1146 }
1147
1148 function uEditRemoveStage() {
1149     var resp = fieldmapper.standardRequest(
1150         ['open-ils.actor', 'open-ils.actor.user.stage.delete'],
1151         { params : [openils.User.authtoken, stageUser.row_id()] }
1152     )
1153 }
1154
1155 function uEditFinishSave(newPatron, doClone) {
1156
1157     if(doClone && cloneUser == null)
1158         cloneUser = newPatron.id();
1159
1160         if( doClone ) {
1161
1162                 if(xulG && typeof xulG.spawn_editor == 'function' && !patron.isnew() ) {
1163             window.xulG.spawn_editor({ses:openils.User.authtoken,clone:cloneUser});
1164             uEditRefresh();
1165
1166                 } else {
1167                         location.href = location.href.replace(/\?.*/, '') + '?clone=' + cloneUser;
1168                 }
1169
1170         } else {
1171
1172                 uEditRefresh();
1173         }
1174
1175         uEditRefreshXUL(newPatron);
1176 }
1177
1178 function uEditRefresh() {
1179     var usr = cgi.param('usr');
1180     var href = location.href.replace(/\?.*/, '');
1181     href += ((usr) ? '?usr=' + usr : '');
1182     location.href = href;
1183 }
1184
1185 function uEditRefreshXUL(newuser) {
1186         if (window.xulG && typeof window.xulG.on_save == 'function') 
1187                 window.xulG.on_save(newuser);
1188 }
1189
1190
1191 /**
1192  * Create a new address and insert it into the DOM
1193  * @param evt ignored
1194  * @param id The address id
1195  * @param mkLinks If true, set the new address as the 
1196  *  mailing/billing address for the user
1197  */
1198 function uEditNewAddr(evt, id, mkLinks) {
1199
1200     if(id == null) 
1201         id = --uEditAddrVirtId; // new address
1202
1203     var addr =  patron.addresses().filter(
1204         function(i) { return (i.id() == id) })[0];
1205
1206     dojo.forEach(addrTemplateRows, 
1207         function(row) {
1208
1209             row = tbody.insertBefore(row.cloneNode(true), dojo.byId('new-addr-row'));
1210             row.setAttribute('type', '');
1211             row.setAttribute('addr', id+'');
1212
1213             if(row.getAttribute('fmclass')) {
1214                 var widget = fleshFMRow(row, 'aua', {addr:id});
1215
1216                 // make new addresses a default address type
1217                 if(id < 0 && row.getAttribute('fmfield') == 'address_type') 
1218                     widget.widget.attr('value', localeStrings.DEFAULT_ADDRESS_TYPE); 
1219
1220                 // make new addresses valid by default
1221                 if(id < 0 && row.getAttribute('fmfield') == 'valid') 
1222                     widget.widget.attr('value', true); 
1223
1224                 // make new addresses use the org setting for default country 
1225                 if(id < 0 && row.getAttribute('fmfield') == 'country') 
1226                     widget.widget.attr('value',orgSettings['ui.patron.default_country']);
1227
1228             } else if(row.getAttribute('name') == 'uedit-addr-pending-row') {
1229
1230                 // if it's a pending address, show the 'approve' button
1231                 if(addr && openils.Util.isTrue(addr.pending())) {
1232                     openils.Util.show(row, 'table-row');
1233                     dojo.query('[name=approve-button]', row)[0].onclick = 
1234                         function() { uEditApproveAddress(addr); };
1235
1236                     if(addr.replaces()) {
1237                         var div = dojo.query('[name=replaced-addr]', row)[0]
1238                         var replaced =  patron.addresses().filter(
1239                             function(i) { return (i.id() == addr.replaces()) })[0];
1240
1241                         div.innerHTML = dojo.string.substitute(localeStrings.REPLACED_ADDRESS, [
1242                             replaced.address_type() || '',
1243                             replaced.street1() || '',
1244                             replaced.street2() || '',
1245                             replaced.city() || '',
1246                             replaced.state() || '',
1247                             replaced.post_code() || ''
1248                         ]);
1249
1250                     } else {
1251                         openils.Util.hide(dojo.query('[name=replaced-addr-div]', row)[0]);
1252                     }
1253                 }
1254
1255             } else if(row.getAttribute('name') == 'uedit-addr-owner-row') {
1256                 // address is owned by someone else.  provide option to load the
1257                 // user in a different tab
1258                 
1259                 if(addr && addr.usr() != patron.id()) {
1260                     openils.Util.show(row, 'table-row');
1261                     var link = getByName(row, 'addr-owner');
1262
1263                     // fetch the linked user so we can present their name in the UI
1264                     var addrUser;
1265                     if(cloneUserObj && cloneUserObj.id() == addr.usr()) {
1266                         addrUser = [
1267                             cloneUserObj.first_given_name(), 
1268                             cloneUserObj.second_given_name(), 
1269                             cloneUserObj.family_name()
1270                         ];
1271                     } else {
1272                         addrUser = fieldmapper.standardRequest(
1273                             ['open-ils.actor', 'open-ils.actor.user.retrieve.parts'],
1274                             {params: [
1275                                 openils.User.authtoken, 
1276                                 addr.usr(), 
1277                                 ['first_given_name', 'second_given_name', 'family_name']
1278                             ]}
1279                         );
1280                     }
1281
1282                     link.innerHTML = (addrUser.map(function(name) { return (name) ? name+' ' : '' })+'').replace(/,/g,''); // TODO i18n
1283                     link.onclick = function() {
1284                         if(openils.XUL.isXUL()) { 
1285                             window.xulG.spawn_editor({ses:openils.User.authtoken, usr:addr.usr()})
1286                         } else {
1287                             parent.location.href = location.href.replace(/clone=\d+/, 'usr=' + addr.usr());
1288                         }
1289                     }
1290                 }
1291
1292             } else if(row.getAttribute('name') == 'uedit-addr-divider') {
1293                 // link up the billing/mailing address and give the inputs IDs so we can acces the later
1294                 
1295                 // billing address
1296                 var ba = getByName(row, 'billing_address');
1297                 ba.id = 'uedit-billing-address-' + id;
1298                 if(mkLinks || (patron.billing_address() && patron.billing_address().id() == id))
1299                     ba.checked = true;
1300
1301                 // mailing address
1302                 var ma = getByName(row, 'mailing_address');
1303                 ma.id = 'uedit-mailing-address-' + id;
1304                 if(mkLinks || (patron.mailing_address() && patron.mailing_address().id() == id))
1305                     ma.checked = true;
1306
1307                 var btn = dojo.query('[name=delete-button]', row)[0];
1308                 if(btn) btn.onclick = function(){ uEditDeleteAddr(id) };
1309             }
1310         }
1311     );
1312 }
1313
1314 function uEditApproveAddress(addr) {
1315     fieldmapper.standardRequest(
1316         ['open-ils.actor', 'open-ils.actor.user.pending_address.approve'],
1317         {   async: true,
1318             params:  [openils.User.authtoken, addr],
1319
1320             oncomplete : function(r) {
1321                 var oldId = openils.Util.readResponse(r);
1322                     
1323                 // remove addrs from UI
1324                 dojo.forEach(
1325                     patron.addresses(), 
1326                     function(addr) { uEditDeleteAddr(addr.id(), true); }
1327                 );
1328
1329                 if(oldId != null) {
1330                     
1331                     // remove the replaced address 
1332                     if(oldId != addr.id()) {
1333                                 patron.addresses(
1334                             patron.addresses().filter(
1335                                                 function(i) { return (i.id() != oldId); }
1336                                         )
1337                                 );
1338                     }
1339                     
1340                     // fix the the new address
1341                     addr.id(oldId);
1342                     addr.replaces(null);
1343                     addr.pending('f');
1344
1345                 }
1346
1347                 // redraw addrs
1348                 loadAllAddrs();
1349             }
1350         }
1351     );
1352 }
1353
1354
1355 function uEditDeleteAddr(id, noAlert) {
1356     if (patron.isnew() && orgSettings['ui.patron.registration.require_address']) {
1357         if (dojo.query('tr[name=uedit-addr-divider]').length < 2) {
1358             alert(localeStrings.NEED_ADDRESS);
1359             return;
1360         }
1361     }
1362     if(!noAlert) {
1363         if(!confirm(dojo.string.substitute(localeStrings.DELETE_ADDRESS, [id]))) return;
1364     }
1365     var addr = patron.addresses().filter(function(i){return (i.id() == id)})[0];
1366     if (addr) { addr.isdeleted(1); }
1367     var m_a = patron.mailing_address();
1368         if (typeof m_a == 'object' && m_a != null) { m_a = m_a.id(); }
1369         if (m_a == id) { patron.mailing_address(null); }
1370     var b_a = patron.billing_address();
1371         if (typeof b_a == 'object' && b_a != null) { b_a = b_a.id(); }
1372         if (b_a == id) { patron.billing_address(null); }
1373
1374     var rows = dojo.query('tr[addr='+id+']', tbody);
1375     for(var i = 0; i < rows.length; i++)
1376         rows[i].parentNode.removeChild(rows[i]);
1377     widgetPile = widgetPile.filter(function(w){return (w._addr != id)});
1378 }
1379
1380 function uEditToggleRequired() {
1381     if((tbody.className +'').match(/hide-non-required/)) 
1382         openils.Util.removeCSSClass(tbody, 'hide-non-required');
1383     else
1384         openils.Util.addCSSClass(tbody, 'hide-non-required');
1385     openils.Util.toggle('uedit-show-required');
1386     openils.Util.toggle('uedit-show-all');
1387 }
1388
1389 function printable_output() {
1390     var temp; var s = '=-=-=-=\r\n';
1391     for (var idx in widgetPile) {
1392         var w = widgetPile[idx];
1393         var val = uEditWidgetVal(w);
1394         var label;
1395         if (typeof w.idlField == 'undefined') {
1396             label = w._wtype;
1397             if (w._wtype == 'statcat') {
1398                 var stat = statCats.filter(
1399                     function(m){
1400                         return (m.id() == w._statcat) })[0];
1401                 label = stat.name();
1402             } else if (w._wtype == 'survey') {
1403                 var survey = surveys.filter(
1404                     function(m){
1405                         return (m.id() == w._survey) })[0];
1406                 var question = survey.questions().filter(
1407                     function(m){
1408                         return (m.id() == w._question) })[0];
1409                 label = survey.name() + ' : ' + question.question();
1410             } else {
1411                 label = 'FIXME';
1412             }
1413         } else {
1414             label = w.idlField.label;
1415         }
1416         if (temp != w._wtype) {
1417             temp = w._wtype;
1418             s += '-------\r\n';
1419         }
1420         s += label + ':\t' + (typeof val == 'object' ? '' : val) + '\r\n';
1421     }
1422     s += '=-=-=-=\r\n';
1423     return s;
1424 }
1425
1426 openils.Util.addOnLoad(load);