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