CI / Tests (PHP 8.2) (pull_request) Successful in 42s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m45s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.1) (pull_request) Failing after 52s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m52s
CI / Coding Standards (pull_request) Successful in 2m57s
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]>
69 lines
2.3 KiB
JavaScript
69 lines
2.3 KiB
JavaScript
/**
|
|
* "Who is this for?" picker, shared by the lesson-booking and group-class
|
|
* registration forms.
|
|
*
|
|
* The list arrives from the server already ordered children-first, with the
|
|
* account holder last, and this module preserves that order: a guardian's
|
|
* default selection is their first child, never themselves. Booking for the
|
|
* wrong child is a correctable mistake; quietly enrolling the parent in a class
|
|
* meant for their kid is not.
|
|
*/
|
|
(function () {
|
|
'use strict';
|
|
|
|
function escHtml(str) {
|
|
return String(str)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
}
|
|
|
|
/**
|
|
* Read the server-rendered student list off a `data-students` attribute.
|
|
* Anything unparseable degrades to an empty list, which renders no picker
|
|
* and books for the signed-in user — the pre-guardian behaviour.
|
|
*/
|
|
function parseStudents(raw) {
|
|
if (!raw) return [];
|
|
try {
|
|
const list = JSON.parse(raw);
|
|
return Array.isArray(list) ? list : [];
|
|
} catch (e) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The picker's markup, or an empty string when there is nothing to choose:
|
|
* an account with only itself on the list never sees the question.
|
|
*/
|
|
function selectorHtml(students, id) {
|
|
if (!students || students.length < 2) return '';
|
|
|
|
const options = students.map((s) => {
|
|
// The account holder reads as "Myself" — their own name next to their
|
|
// children's is ambiguous about which row is the parent.
|
|
const label = s.is_self ? `Myself (${s.name})` : s.name;
|
|
return `<option value="${Number(s.id)}">${escHtml(label)}</option>`;
|
|
}).join('');
|
|
|
|
return `
|
|
<p class="us-student-picker">
|
|
<label for="${id}">Who is this for?<br>
|
|
<select id="${id}" required>${options}</select></label>
|
|
</p>`;
|
|
}
|
|
|
|
/**
|
|
* The chosen student id, or 0 when no picker was rendered — the server
|
|
* reads 0 as "the caller books for themselves".
|
|
*/
|
|
function selectedId(id) {
|
|
const el = document.getElementById(id);
|
|
return el ? Number(el.value) || 0 : 0;
|
|
}
|
|
|
|
window.usGuardian = { parseStudents, selectorHtml, selectedId };
|
|
}());
|