The front end used the deadline only to gate the Enrol button; students had
no way to see when enrolment closes. Add an "Enrol by <date>" line to each
class card, shown while enrolment is still open, for the effective deadline
(the instructor's date, or the first class day by default).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Group classes gain an instructor-set enrolment deadline (new
us_offerings.enrollment_deadline column) that defaults to the first day of
the class (term_start). Past the deadline students can no longer self-enrol:
the enrolment endpoint rejects it (403 enrollment_closed) and the front-end
class list shows "Enrolment has closed." in place of the Enrol button.
Instructors keep a manual path: the "Add students directly" control on each
class's details page now renders for public classes too (not just
invite-only) and deliberately bypasses the deadline and capacity, so a
student can be added as a late enrolment after the class has closed. Past
the deadline the details page labels these as late enrolments.
Bumps USC_VERSION to 1.1.3 for the schema change.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
WordPress only renders the "Enable auto-updates" toggle for a plugin that
appears in the update_plugins transient's response or no_update list, which
is what sets core's update-supported flag. UpdateChecker only populated the
response side (when a newer release existed), so between releases the plugin
was absent from the transient and the toggle never showed.
provideUpdate() now returns a no_update payload (installed version, empty
package) whenever no newer release is offered — including when the release
lookup fails — so the plugin stays in the transient and the toggle appears.
The response path (one-click and unattended updates) is unchanged.
Bumps to 1.1.1 so the fix ships to installed sites via the self-updater.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add Auth\UserName::format(), which prefers a user's first + last name, then
their nickname, avoiding display_name (which can be the login/username).
Route the instructor name through it in both the front-end offerings response
(instructor_name) and the back-end group-class summary and details views.
Tests: composer test (513), composer lint, composer cs all pass.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Group classes now carry a specific class time (alongside date and duration)
and an assigned instructor:
- Schema: add `class_time` (TIME) to `us_offerings`; `Offering` gains
`normalizeTime`/`sessionWindows`. (Rides the pending 1.0.0->1.1.0 dbDelta
upgrade, so no version bump.)
- Offering form: class-time field, plus a studio-admin instructor picker
(plain instructors always own their own classes).
- `ClassSlotReconciler`: assigning an instructor clears their open booking
slots overlapping each session and flags already-booked lessons that clash
(a booked lesson is never deleted). Uses new
`AvailabilityRepository::findOverlapping`.
- Front end: `GET /offerings` exposes `instructor_name`; the enrolment page
shows who teaches each class and when it meets.
Back-office group-class views redesigned:
- Instructor **My Group Classes** and studio-admin **Group Classes** are now
per-class summaries with enrolment counts, not flat student lists.
- Each links through (`?class_id=<id>`) to a per-class **details page**
(schedule panel, roster with payment status, and — for invite-only classes
— the add/make-available/invite-by-email controls). Invite-only membership
is managed entirely from this page.
- Invite actions are allowed for the class's owning instructor or any
`view_all_lessons` studio admin, so an owner-operator (studio admin who also
teaches) can reach every class's roster and invites from the Group Classes
page.
Tests: composer test (508), composer lint, composer cs all pass.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Add CHANGELOG.md (one section per version, newest first; the top section
always reflects the current plugin header version — release status is
purely a matter of tagging).
The Release workflow now extracts the tagged version's changelog section
and publishes it as the Gitea release body (POST on create, PATCH when the
release was pre-created via the UI). After a stable release, a new
bump-version job bumps the plugin to the next patch version, opens a fresh
changelog section, and opens a PR. Pre-release tags are skipped.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Group classes can now be marked invite-only (us_offerings.access_mode).
Invite-only classes are hidden from the public catalog and reachable only
when the instructor lets someone in via one of three paths, managed from
My Lessons -> My Group Classes:
- Add students directly: enrols them now with a pending payment.
- Make available: grants registered students access to self-enrol through
the normal paid flow (multi-select, emailed a notice).
- Invite by email: tokenised registration invite tied to the class for a
non-account address; after they register the class becomes enrollable.
Reuses an existing pending invite instead of sending a second link.
New us_group_access table records grants; GET /offerings merges granted
invite-only classes for the caller; enrolment requires a grant
(403 invite_required) and flips it to enrolled on success.
composer test (487), composer lint, composer cs all pass.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Instructors can now see their own group classes under My Lessons →
My Group Classes (view_own_lessons): each class shows its active
enrolment count against capacity plus a roster of enrolled students
with enrolment and payment status.
GroupClassController gains renderInstructorPage(), backed by the
existing per-instructor enrolment query and a newly injected
PaymentRepository for payment status. Wired as a submenu under the
existing My Lessons menu, inside the same !view_all_lessons guard so
owner-operators don't get a duplicate item.
Closes#71
Co-Authored-By: Claude Opus 4.8 <[email protected]>
The Studio Settings cutoff field now takes an integer number of days (step 1,
coerced with Val::int) instead of allowing half-day fractions, and displays the
stored hours rounded to whole days.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Students can no longer cancel their own lesson online once it starts within a
configured window; instructors and studio admins can always cancel.
- Studio default `us_cancellation_cutoff_hours` (stored/computed in hours,
entered and displayed in days under Studio Settings → Cancellations).
- Optional per-offering override `cancellation_cutoff_hours` (entered in hours);
blank inherits the studio default, 0 allows anytime cancellation.
- `Booking\CancellationPolicy` resolves the effective window and decides;
`BookingEndpoint::cancel()` returns a 403 `cancellation_closed` when too late.
The instructor status endpoint and studio-admin student actions bypass it.
Closes#93
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Make the "By Unsupervised" author link go to https://unsupervised.ca
(via a new Author URI header) and the plugin site / "View details" link
point at the Gitea project instead of WordPress.org.
Core's "View details" link opened a thickbox iframe against the
WordPress.org plugin-information API, which 404s ("Plugin not found")
for this off-directory plugin. A plugin_row_meta filter now replaces it
with a new-tab link to the matching Gitea release tag page; embedding
Gitea in the iframe is blocked by its X-Frame-Options anyway.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Studio admins can now define registration questions that every new student
answers as a required second step during signup, with each student's answers
shown under a "Registration Information" section in the admin.
Extends the existing Registration domain: us_questions gains a scope column
(offering | account) and a nullable offering_id, and account answers reuse
us_question_answers with registration_type = 'account'. Authoring reuses the
Offerings -> Questions page via an "Account signup" scope (studio-admin only).
The registration form becomes two steps (progressive enhancement via
assets/js/register.js; works without JS); required answers are validated before
the account is created and apply to all signup paths (invite, group link,
self-approval). StudentHistory::registrationInfo() powers the admin section.
Bumps the plugin version to 1.1.0 so dbDelta runs the schema migration.
Closes#90
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Version header, USC_VERSION, and the README version line move from
1.0.0-rc.3 (README was stale at rc.1) to 1.0.0. Tag v1.0.0 on the merge
commit to publish the release.
Co-Authored-By: Claude Fable 5 <[email protected]>
PR #83 added kind and expires_at to us_invites and the repository started
writing them, but USC_VERSION stayed at 1.0.0-rc.2 — Plugin::boot() only
re-runs Installer/dbDelta on a version mismatch, so upgraded sites never got
the columns. Every invite insert then failed silently: nothing appeared under
Pending Invites while the admin was still shown a registration link whose
token hash was never stored.
- Version / USC_VERSION -> 1.0.0-rc.3 (triggers dbDelta on next load).
- InviteRepository::insert() returns 0 on failure instead of a stale
insert_id, and the Invites page now shows an error notice instead of a
dead link when creation fails (personal and group forms), including
clearer validation messages.
- CLAUDE.md: schema changes must bump the version.
Closes#87
Co-Authored-By: Claude Fable 5 <[email protected]>
Scheduler (view_all_lessons) is a superset of My Lessons — same template,
every instructor's lessons, same payment edit forms — so for an
owner-operator both menu items showed the same data twice. The My Lessons
menu item is now only registered for users without view_all_lessons;
instructors are unaffected.
Closes#85
Co-Authored-By: Claude Fable 5 <[email protected]>
Follow-up demo feedback: the My Availability page now opens in its weekly
calendar (usc_view=list opts back into the table, which keeps the bulk-delete
form), matching the new lessons defaults.
Co-Authored-By: Claude Fable 5 <[email protected]>
A studio admin can generate a shareable group invite link (e.g. for a
newsletter) from the Invites page, choosing a required expiry date. Anyone
with the link may register while it is valid, in any registration mode: the
form collects their own email, they must confirm it via the usual hashed
token, and confirming approves the account immediately — group signups never
enter the Pending Students queue.
- us_invites grows kind (personal/group) and expires_at; an explicit expiry
wins over the personal 14-day window. Group links stay pending (multi-use)
until revoked or expired.
- RegistrationPage: group signups create the account pending with the
us_auto_approve marker and send the confirmation email; no auto-login.
- EmailConfirmationHandler: auto-approve accounts are approved on
confirmation, emailed the approved notice, and redirected to a new
us_confirmed=ready notice with a sign-in link.
Closes#77
Co-Authored-By: Claude Fable 5 <[email protected]>
The register form keyed the read-only, prefilled email off any invite row
matching the token. A stale token (expired / accepted / revoked) with open
registration on therefore showed the stale invite's address read-only while
the submit handler took the open branch and required a posted email the
locked field never submits, dead-ending the form. The lock now applies
exactly when the invite is acceptable; otherwise the editable field renders.
Closes#78
Co-Authored-By: Claude Fable 5 <[email protected]>
A weekly booking on a per-lesson (one_time) priced offering was creating its
single upfront payment for one week's price while reserving up to 12 weeks,
and settling that payment confirmed only the anchor lesson, leaving the rest
of the series pending forever.
- BookingEndpoint now charges price x claimed occurrences for one_time
billing; a full_term price is still charged once since it covers the term.
- PaymentService::confirmRegistration resolves the anchor lesson's series and
confirms every non-cancelled row via the new
BookingRepository::updateStatusForSeries().
Closes#79
Co-Authored-By: Claude Fable 5 <[email protected]>
The front-end booking calendar now opens in the Week view (anchored to the
week of the earliest open slot) with List still available. The Scheduler and
My Lessons admin pages gain a week calendar (usc_view/usc_week, bucketed via
a new generic WeekCalendar::bucket()) and open in it by default; the original
table remains as the List view since it carries the HST / e-transfer forms.
Closes#76
Co-Authored-By: Claude Fable 5 <[email protected]>
Adds the #70 follow-up onto the student detail page: studio admins can now
cancel an upcoming lesson (same path as student cancellation — slot freed,
pending payment voided), withdraw an active group-class enrolment (seat
freed, pending payment voided), and edit the student's display name and
email with validation and uniqueness checks.
Action logic lives in the new Auth\StudentActions (unit-tested with mocked
repositories); the controller routes nonce-protected POSTs to it and shows
success/error notices.
Closes#70
Co-Authored-By: Claude Fable 5 <[email protected]>
The student-administration spec deferred three detail-view sections until
Payments landed. Adds them now: policy-acceptance history (title, version,
context, date), intake answers (label, answer, context), and — gated on
manage_billing — payment history with HST breakdown and receipt numbers.
New Auth\StudentHistory builds the display rows from per-student queries
added to AcceptanceRepository, AnswerRepository, and PaymentRepository;
the Payment model now carries created_at so unpaid rows still have a date.
Closes#69
Co-Authored-By: Claude Fable 5 <[email protected]>
The README still listed Payments as partial with the Stripe card charge
pending, and group-classes.md still described the pre-#7 payment seam.
Both are behind the code: StripeGateway/PaymentEndpoint ship the live
card charge, and enrolments create and link payments via PaymentService.
Fixes#73
Co-Authored-By: Claude Fable 5 <[email protected]>
Closes#67
When a student lands on the registration page from the confirmation
email (?us_confirmed=1), replace the registration form with the
confirmation message and a "Sign in to your account" link — the form
is useless at that point and re-submitting would only produce an
"account already exists" error. A confirmed-but-unapproved student can
already log in (the pending gate only withholds booking), so signing in
is the natural next step.
The link target follows the booking block's pattern: a loginPageId
block attribute (page picker in the editor sidebar) or login_page_id
shortcode attribute, falling back to wp_login_url(). The expired-link
notice keeps the form as before.
Co-Authored-By: Claude Fable 5 <[email protected]>
Closes#65
Declare an Update URI header and answer core's update_plugins_{hostname}
filter from a new Update\UpdateChecker that offers the latest published
Gitea release's zip asset when it is newer than the installed version,
with transient caching and silent degradation on API failures.
Add a release workflow that fires on v* tag pushes: verifies the tag
matches the plugin Version header, runs the tests, builds the plugin zip,
and attaches it to the release (reusing a UI-created release, flagging
hyphenated versions as pre-release so /releases/latest skips them).
Co-Authored-By: Claude Fable 5 <[email protected]>