Let parents register once and book for their children
A parent registers once and manages lessons for one or more children, who need no login of their own. A child is a real wp_users row with the student role but no usable login — so student_id keeps meaning "a WordPress user" on every table, and booking, credits, policies and enrolments work unchanged. A us_guardians link table maps guardian to child. The signup form gains a parent/guardian tick that reveals a block per child, with the account-signup questions asked per child rather than per guardian — they describe the student, not the account holder. Signup policies are recorded once per child with the guardian as the acceptor, which is the record that actually means something. A family that half-creates is rolled back entirely rather than leaving a guardian who cannot re-register. The booking and enrolment forms gain a "Who is this for?" picker listing children first, so the default selection is never the parent — booking for the wrong child is correctable, quietly billing a parent for their kid's lesson is not. POST /bookings and POST /enrollments take an optional student_id honoured only for that child's guardian; anything else is a 403. That check is the authorisation boundary of the feature. Payments and credits gain a payer: the charge names the child it was for and the guardian who owes it, so per-child reporting is unchanged while notices, receipts and the payment step reach the parent. Credit is held by the payer, so one child's cancellation can settle a sibling's charge, and the daily billing scan sends a guardian one notice covering every child. Closes #132 Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+112
-10
@@ -1,23 +1,31 @@
|
||||
/**
|
||||
* Progressive enhancement for the two-step student registration form.
|
||||
* Progressive enhancement for the student registration form.
|
||||
*
|
||||
* When account-signup questions are configured the form renders two panels
|
||||
* (`[data-step="1"]` account details, `[data-step="2"]` the questions) inside a
|
||||
* single form marked `data-steps="1"`. This script hides step two behind a
|
||||
* "Next" button that only advances once step one passes native validation.
|
||||
* Without JS both panels stay visible and the single submit still works.
|
||||
* Two independent behaviours, both optional — without JS every panel stays
|
||||
* visible and the single submit still works:
|
||||
*
|
||||
* 1. **Two steps.** When account-signup questions are configured the form
|
||||
* renders two panels (`[data-step="1"]` account details, `[data-step="2"]`
|
||||
* the questions) inside a form marked `data-steps="1"`. Step two is hidden
|
||||
* behind a "Next" button that only advances once step one passes native
|
||||
* validation.
|
||||
* 2. **Parent/guardian.** The children section is hidden until the
|
||||
* parent/guardian box is ticked, and "Add another child" clones the child
|
||||
* block. Ticking the box also takes the guardian's *own* question panel out
|
||||
* of play — in guardian mode the questions are asked per child, so the
|
||||
* server ignores those answers and the browser must not demand them.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function enhance(form) {
|
||||
function enhanceSteps(form) {
|
||||
var step1 = form.querySelector('[data-step="1"]');
|
||||
var step2 = form.querySelector('[data-step="2"]');
|
||||
var next = form.querySelector('.us-reg-next');
|
||||
var back = form.querySelector('.us-reg-back');
|
||||
|
||||
if (!step1 || !step2 || !next) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
function show(step) {
|
||||
@@ -45,13 +53,107 @@
|
||||
show(1);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
step2: step2,
|
||||
next: next,
|
||||
earlySubmit: form.querySelector('.us-reg-submit-early'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Rewrite a cloned child block's `children[0][…]` names and ids to the new
|
||||
* index, and clear the values carried over from the block it was cloned from.
|
||||
*/
|
||||
function reindex(block, index) {
|
||||
block.setAttribute('data-child-index', String(index));
|
||||
|
||||
var fields = block.querySelectorAll('input, select, textarea');
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
var field = fields[i];
|
||||
|
||||
if (field.name) {
|
||||
field.name = field.name.replace(/^children\[\d+\]/, 'children[' + index + ']');
|
||||
}
|
||||
|
||||
var oldId = field.id;
|
||||
if (oldId) {
|
||||
field.id = oldId.replace(/^us-child-\d+-/, 'us-child-' + index + '-');
|
||||
|
||||
var label = block.querySelector('label[for="' + oldId + '"]');
|
||||
if (label) {
|
||||
label.setAttribute('for', field.id);
|
||||
}
|
||||
}
|
||||
|
||||
if (field.type === 'checkbox' || field.type === 'radio') {
|
||||
field.checked = false;
|
||||
} else {
|
||||
field.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function enhanceGuardian(form, steps) {
|
||||
var toggle = form.querySelector('#us-is-guardian');
|
||||
var children = form.querySelector('#us-children');
|
||||
|
||||
if (!toggle || !children) {
|
||||
return;
|
||||
}
|
||||
|
||||
var addButton = children.querySelector('.us-add-child');
|
||||
var nextIndex = 1;
|
||||
|
||||
// The guardian's own question panel is only meaningful when they are
|
||||
// registering for themselves. Disabling it (rather than hiding it) is what
|
||||
// stops a `required` question the server will ignore from blocking submit.
|
||||
function sync() {
|
||||
children.hidden = !toggle.checked;
|
||||
|
||||
if (!steps) {
|
||||
return;
|
||||
}
|
||||
|
||||
var fields = steps.step2.querySelectorAll('input, select, textarea');
|
||||
for (var i = 0; i < fields.length; i++) {
|
||||
fields[i].disabled = toggle.checked;
|
||||
}
|
||||
|
||||
// With the questions out of play there is no second step to advance to,
|
||||
// so "Next" would be a dead end — swap it for the submit.
|
||||
steps.next.hidden = toggle.checked;
|
||||
|
||||
if (steps.earlySubmit) {
|
||||
steps.earlySubmit.hidden = !toggle.checked;
|
||||
}
|
||||
}
|
||||
|
||||
toggle.addEventListener('change', sync);
|
||||
sync();
|
||||
|
||||
if (addButton) {
|
||||
addButton.addEventListener('click', function () {
|
||||
var blocks = children.querySelectorAll('.us-child');
|
||||
var clone = blocks[blocks.length - 1].cloneNode(true);
|
||||
|
||||
reindex(clone, nextIndex);
|
||||
nextIndex += 1;
|
||||
|
||||
children.insertBefore(clone, addButton.parentNode);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var forms = document.querySelectorAll('.us-register-form form[data-steps="1"]');
|
||||
var forms = document.querySelectorAll('.us-register-form form');
|
||||
|
||||
for (var i = 0; i < forms.length; i++) {
|
||||
enhance(forms[i]);
|
||||
var steps = forms[i].getAttribute('data-steps') === '1'
|
||||
? enhanceSteps(forms[i])
|
||||
: null;
|
||||
|
||||
enhanceGuardian(forms[i], steps);
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user