diff --git a/CHANGELOG.md b/CHANGELOG.md index 72063ab..2a27426 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ each change under the current top section as you work. ## [1.5.1] +### Fixed +- **A parent can now enrol more than one child in the same group class.** Enrolling the first student worked, and then the class card switched to "You are enrolled in this class." with a **Withdraw** button — for the whole account. There was no way to sign up a second child short of withdrawing the first, even though nothing was ever actually full or forbidden: the class page was matching enrolments to the account rather than to the student, so one child's seat spoke for everybody. Each enrolled student now gets their own line on the card, named — "Ada is enrolled in this class." — with their own Withdraw button, and the Enrol button stays put, reading **Enrol another student**, until everyone on the account is in. The form's "Who is this for?" list offers only the students not yet enrolled, so the class cannot be double-booked for the same child by accident. Enrolments already recorded are unaffected; the seats were always separate on the studio's side, and this is the page catching up with that. + ## [1.5.0] ### Added diff --git a/assets/js/group-classes.js b/assets/js/group-classes.js index 3e2ad01..24faf82 100644 --- a/assets/js/group-classes.js +++ b/assets/js/group-classes.js @@ -137,7 +137,78 @@ return !o.withdrawal_deadline || todayYmd() <= o.withdrawal_deadline; } - function renderClasses(offerings, enrolledMap) { + // Active enrolments grouped by class. A household can hold several in the + // same class — one per student — so the value is a list, never a single id. + function activeByOffering(enrollments) { + const map = new Map(); + enrollments + .filter((e) => e.status === 'active') + .forEach((e) => { + const key = Number(e.offering_id); + const held = map.get(key) || []; + held.push({ id: e.id, studentId: Number(e.student_id) }); + map.set(key, held); + }); + return map; + } + + // Who on this account could still be enrolled in a class: everyone the + // account may enrol, minus those already holding an active enrolment in it. + // The per-student check is the point — the account used to be treated as a + // single enrollee, so enrolling one child hid the Enrol button from the rest + // of the household even though the server would have taken them happily. + function availableStudents(offeringId, enrolled) { + const held = enrolled.get(Number(offeringId)) || []; + + // Degraded case: an unparseable student list leaves no id to compare + // against, so any existing enrolment is read as covering the account. + if (!students.length) return held.length ? [] : [{ id: 0, name: '', is_self: true }]; + + const taken = new Set(held.map((e) => e.studentId)); + return students.filter((s) => !taken.has(Number(s.id))); + } + + // The enrolled student's name, or '' when there is nobody to tell them apart + // from: an account with a single student reads better in the second person. + function studentName(studentId) { + if (students.length < 2) return ''; + const s = students.find((st) => Number(st.id) === Number(studentId)); + return s && !s.is_self ? s.name : ''; + } + + function enrolledRow(o, e) { + const name = studentName(e.studentId); + return ` +
${name ? `${escHtml(name)} is` : 'You are'} enrolled in this class.
+ ${isWithdrawalOpen(o) + ? `` + : `Withdrawal${name ? ` for ${escHtml(name)}` : ''} has closed — contact the studio to withdraw.
`}`; + } + + function classCard(o, enrolled) { + const held = enrolled.get(Number(o.id)) || []; + const available = availableStudents(o.id, enrolled); + const canEnrol = available.length > 0 && isEnrollmentOpen(o); + + return ` +${escHtml(whenLabel(o))}
` : ''} + ${o.instructor_name ? `With ${escHtml(o.instructor_name)}
` : ''} + ${o.schedule_note ? `${escHtml(o.schedule_note)}
` : ''} + ${!singleOfferingId && o.description ? `${escHtml(o.description)}
` : ''} +${escHtml(window.usPricing.priceLabel(o))}
+ ${canEnrol && enrolmentDeadline(o) + ? `Enrol by ${escHtml(formatDate(enrolmentDeadline(o)))}
` + : ''} + ${held.map((e) => enrolledRow(o, e)).join('')} + ${canEnrol + ? `` + : (available.length ? 'Enrolment has closed.
' : '')} +${escHtml(whenLabel(o))}
` : ''} - ${o.instructor_name ? `With ${escHtml(o.instructor_name)}
` : ''} - ${o.schedule_note ? `${escHtml(o.schedule_note)}
` : ''} - ${!singleOfferingId && o.description ? `${escHtml(o.description)}
` : ''} -${escHtml(window.usPricing.priceLabel(o))}
- ${!enrolledMap.has(Number(o.id)) && isEnrollmentOpen(o) && enrolmentDeadline(o) - ? `Enrol by ${escHtml(formatDate(enrolmentDeadline(o)))}
` - : ''} - ${enrolledMap.has(Number(o.id)) - ? `You are enrolled in this class.
- ${isWithdrawalOpen(o) - ? `` - : 'Withdrawal has closed — contact the studio to withdraw.
'}` - : (isEnrollmentOpen(o) - ? `` - : 'Enrolment has closed.
')} -For ${only.is_self ? 'yourself' : escHtml(only.name)}.
` + : ''}`; + } + + function renderEnrolment(offering, questions, policies, available) { list.innerHTML = `