diff --git a/CHANGELOG.md b/CHANGELOG.md index e149469..5809831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ When a `v*` tag is pushed, `.gitea/workflows/release.yml` publishes the matching the plugin to the next patch version and adds a fresh section here for it. Record each change under the current top section as you work. +## [1.1.3] + +### Added +- Group classes now carry an **enrolment deadline** the instructor sets on the offering. It defaults to the first day of the class, and once it passes students can no longer enrol — the enrolment page shows the class as closed and the API rejects late enrolments. +- Instructors can add students to any group class by hand from its details page (**Add students directly**), which now appears for public classes too, not just invite-only ones. This bypasses the enrolment deadline and capacity, so a student can be enrolled as a **late enrolment** after the class has closed to self-enrolment. + ## [1.1.2] ## [1.1.1] diff --git a/assets/js/group-classes.js b/assets/js/group-classes.js index 9fd5444..ed438ca 100644 --- a/assets/js/group-classes.js +++ b/assets/js/group-classes.js @@ -103,6 +103,21 @@ return [termLabel(o), timeLabel(o)].filter(Boolean).join(' · '); } + // Today as a Y-m-d string in the visitor's local timezone, for lexicographic + // comparison against the class's Y-m-d enrolment deadline. + function todayYmd() { + const now = new Date(); + return `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; + } + + // Enrolment closes at the end of the deadline day — the instructor's set + // deadline, or the first class day by default. Mirrors the server-side + // Offering::isEnrollmentOpen() gate. + function isEnrollmentOpen(o) { + const deadline = o.enrollment_deadline || o.term_start || ''; + return !deadline || todayYmd() <= deadline; + } + function renderClasses(offerings, enrolledOfferingIds) { let groups = offerings.filter((o) => o.kind === 'group_class'); if (singleOfferingId) { @@ -125,7 +140,9 @@
${escHtml(Number(o.price).toFixed(2))} ${escHtml(o.currency)}
${enrolledOfferingIds.has(Number(o.id)) ? 'You are enrolled in this class.
' - : ``} + : (isEnrollmentOpen(o) + ? `` + : 'Enrolment has closed.
')} `).join(''); diff --git a/docs/features/group-classes.md b/docs/features/group-classes.md index d0f5b8f..c812399 100644 --- a/docs/features/group-classes.md +++ b/docs/features/group-classes.md @@ -48,6 +48,19 @@ cancelled enrolment does not block re-enrolling). Capacity is enforced at enrolment time by counting `active` rows for the offering; a class at capacity rejects further enrolments. +Enrolment also closes after the class's **enrolment deadline** (the instructor's +`enrollment_deadline`, defaulting to `term_start` — the first class day; see +`offerings.md`). Past the deadline `POST /enrollments` rejects the enrolment with +`403 enrollment_closed`, and the class list shows "Enrolment has closed." in place +of the Enrol button. + +The deadline only bounds student **self**-enrolment. An instructor (or studio admin) +can still enrol someone by hand from the class **details page** — the **Add students +directly** control, available for every group class, deliberately bypasses the +deadline (and capacity) so a **late enrolment** can be added after the class has +closed. Past the deadline the details page labels these as late enrolments. See +**Admin Interface** below. + ## REST API | Method | Endpoint | Permission | |----------|----------------------------------------------|----------------------------------| @@ -74,13 +87,15 @@ flips their grant from `invited` to `enrolled`. Access to an invite-only class is recorded in `{prefix}us_group_access` — a grant per person, separate from the enrolment itself. The instructor manages access from -**My Lessons → My Group Classes**, which renders three controls under each invite-only -class: +**My Lessons → My Group Classes**. **Add students directly** is available on every +class's details page (see **Admin Interface**); invite-only classes add two more +controls beneath it: 1. **Add students directly** — the selected registered students are enrolled immediately (`status = active`) with a **pending payment** at the class price (comp students are settled at once by `PaymentService`). No access grant is needed — this writes straight - to `us_group_enrollments` + `us_payments`. + to `us_group_enrollments` + `us_payments`. It bypasses the enrolment deadline and + capacity, so it doubles as the **late-enrolment** path after a class has closed. 2. **Make available** — the selected registered students get an `invited` grant so the class appears in their own group-class list; they then self-enrol through the normal paid flow. Each is emailed a "you've been added" notice. @@ -121,12 +136,14 @@ class becomes enrollable for them — they choose whether to enrol. against capacity, plus a **View details** link (**View & invite** for invite-only classes). Selecting a class (`?class_id=