CI / Tests (PHP 8.2) (pull_request) Successful in 58s
CI / PHPStan (pull_request) Successful in 2m53s
CI / Coding Standards (pull_request) Successful in 2m58s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m47s
CI / Build Plugin Zip (pull_request) Skipped
CI / Tests (PHP 8.1) (pull_request) Successful in 1m1s
CI / No Debug Code (pull_request) Successful in 3s
Three fixes from testing the branch. A group class was only listed when its schedule resolved to exact datetimes, which needs a class time *and* a duration — both optional on the offering form, and the schedule note exists precisely so a studio can write "Tuesdays 4:00pm" instead. A class configured that way vanished from the list, which is the one thing this feature must never do. So Offering::sessionStarts() splits "when does it meet" from "how long does it run" (sessionWindows() is that plus the duration, unchanged), and SessionSchedule degrades instead of disappearing: dated rows with an open end when there is no duration, and a single row carrying Offering::scheduleLabel() when there is no time to derive dates from. Only a class whose last day has passed drops out. Deleting a guardian now deletes the children linked to them, releasing each one's lessons and enrolments first. A child account is login-less and exists only so the guardian has somebody to book for; without the guardian nobody can reach it, book for it, or be billed for it, so it was left stranded on the roster still holding slots. A `handled` set makes the re-entrant delete_user each child deletion fires a no-op, and stops a circular link recursing. The upcoming panel never stated its own line-height, so a theme setting line-height: 0 above it — the usual icon-font reset — was inherited straight through. Below 1 that produces both reported symptoms at once: stacked lines overlap, and the status pill's background is shorter than the text in it. Pinned at the same id-level specificity as the rest. Tests: composer test (863), composer lint, composer cs all pass. Co-Authored-By: Claude Opus 5 <[email protected]>
134 lines
7.7 KiB
Markdown
134 lines
7.7 KiB
Markdown
# Feature: Student Administration
|
|
|
|
## Overview
|
|
A studio-admin area to browse students, drill into one student's history and
|
|
upcoming activity — lessons and group-class enrolments — and act on their
|
|
behalf: cancel a lesson, withdraw them from a group class, or fix their account
|
|
details.
|
|
|
|
## Data Model
|
|
No new tables. The views are composed from existing data:
|
|
- Students are WordPress users with the `us_student` role (`get_users`, `get_userdata`).
|
|
- Lessons come from `{prefix}us_lessons` (with `{prefix}us_availability` for slot times).
|
|
- Group-class enrolments come from `{prefix}us_group_enrollments`.
|
|
- Policy acceptances come from `{prefix}us_policy_acceptances` (with the policy
|
|
and version tables for titles/numbers).
|
|
- Intake answers come from `{prefix}us_question_answers` (with `{prefix}us_questions`
|
|
for labels).
|
|
- Payments come from `{prefix}us_payments`.
|
|
|
|
## Admin Interface
|
|
**Students** in wp-admin (`manage_students`, studio admin only):
|
|
|
|
- **List** — every `us_student` user: display name, email, registered date, and
|
|
quick counts (upcoming lessons, active group enrolments). Each row links to the
|
|
detail view.
|
|
- **Detail** (`?student_id=`):
|
|
- **Account** — display name, email, registered date.
|
|
- **Upcoming lessons** and **Past lessons** — split by the linked availability
|
|
slot's `start_dt`; each shows date/time, offering, instructor, and status.
|
|
**Upcoming lessons** also lists the student's upcoming group-class sessions
|
|
(`GroupClass\SessionSchedule`, marked "group class"), so one table answers
|
|
"what are they booked into next week?". Only upcoming ones: past dates would
|
|
bury the lessons, and the enrolment table below already holds the history.
|
|
- **Group-class enrolments** — active/past, with offering title and status.
|
|
- **Policy acceptances** — every acceptance the student has recorded, newest
|
|
first: policy title, version, context (account signup / lesson / enrolment),
|
|
and when it was accepted.
|
|
- **Intake answers** — every registration-question answer, newest first:
|
|
question label, answer, and the registration it was given for.
|
|
- **Account credit** (`manage_billing` only) — the student's available credit
|
|
balance plus every credit (date, reason, amount, remaining, status). Credit
|
|
comes from cancelled paid lessons and is applied automatically to upcoming
|
|
scheduled billing. See `credits.md`.
|
|
- **Payment history** (`manage_billing` only) — every payment, newest first:
|
|
date, context, method, status, subtotal, HST, total, and receipt number.
|
|
|
|
### Admin actions (detail view)
|
|
All actions are nonce-protected POSTs handled on the detail page:
|
|
|
|
- **Edit account** — display name and email. The email must be valid and not in
|
|
use by another account.
|
|
- **Cancel lesson** — on any non-cancelled upcoming lesson. Uses the same path
|
|
as student-initiated cancellation: the lesson is marked `cancelled`, the
|
|
availability slot is freed for rebooking, and a still-pending payment is
|
|
voided. A paid lesson is credited back to the student's account (see
|
|
`credits.md`) rather than refunded.
|
|
- **Withdraw** — on an active group-class enrolment: marked `cancelled` (freeing
|
|
its capacity seat), with the same pending-payment voiding. This is the only way
|
|
to remove a class; the group-class rows in **Upcoming lessons** carry no Cancel
|
|
action, because there is no such thing as cancelling one session of a term.
|
|
|
|
## Deleting a user
|
|
Deleting a WordPress user is a core action that knows nothing about lessons, so
|
|
`Auth\DeletedUserCleanup` hooks `delete_user` (and `wpmu_delete_user`) and gives
|
|
back what the account was holding: every **upcoming** lesson is marked
|
|
`cancelled`, its availability slot released for rebooking, and its still-pending
|
|
payment voided; every **active** group-class enrolment is cancelled and its
|
|
pending payment voided. Without it the slots stayed marked booked and unbookable
|
|
by anyone else, the lessons stayed on the instructor's schedule under a name that
|
|
no longer resolved, and a class kept a seat filled by nobody.
|
|
|
|
**A guardian takes their children with them.** A child account is login-less and
|
|
exists only so the guardian has somebody to book for; without the guardian nobody
|
|
can reach it, book for it, or be billed for it, so leaving it behind leaves an
|
|
unreachable student on the roster holding slots that will never be used. Each
|
|
child's bookings are released on the same terms, the `us_guardians` link row is
|
|
deleted, and the account goes. Deleting a child fires `delete_user` again and
|
|
re-enters the same handler; a `handled` set of user ids makes that a no-op and
|
|
also stops a self-referential or circular link recursing.
|
|
|
|
(This is a different rule from the family screen's **Remove**, which still refuses
|
|
a child with any lesson or enrolment history — that is a guardian tidying up, not
|
|
an admin deleting an account, and `GuardianService::removeChild()` is unchanged.)
|
|
|
|
Past lessons are deliberately untouched: they happened, they may have been paid
|
|
for, and the payment report has to keep adding up. No account credit is issued
|
|
for a paid lesson either, unlike a cancellation the student asks for — a credit
|
|
can only be spent on the account being deleted, so a refund owed to someone who
|
|
has left is the studio's decision to make and record.
|
|
|
|
## Capabilities
|
|
- `manage_students` — studio admin (administrators inherit it via the
|
|
`user_has_cap` filter). No new capabilities or tables.
|
|
|
|
## Implementation
|
|
- Admin controller: `Unsupervised\Schedular\Auth\StudentController` (list + detail)
|
|
- Templates: `templates/admin/students.php`, `templates/admin/student-detail.php`
|
|
- Reuses `Booking\BookingRepository::findByStudent` + `countUpcomingForStudent`,
|
|
`Availability\AvailabilityRepository::findById`,
|
|
`Offering\OfferingRepository::findById`,
|
|
`GroupClass\EnrollmentRepository::findByStudent` + `countActiveForStudent`
|
|
- History sections: `Auth\StudentHistory` builds the display rows from
|
|
`Policy\AcceptanceRepository::findByStudent`,
|
|
`Registration\AnswerRepository::findByStudent`, and
|
|
`Payment\PaymentRepository::findByStudent`, resolving policy/version titles and
|
|
question labels (unit-tested with mocked repositories).
|
|
- Actions: `Auth\StudentActions` — cancel lesson / withdraw enrolment (both
|
|
refuse records that don't belong to the student, and reuse
|
|
`Payment\PaymentService::voidPending`) and account updates via
|
|
`wp_update_user` (unit-tested with mocked repositories).
|
|
- Group-class sessions in the upcoming table: `GroupClass\SessionSchedule::upcomingForStudent()`
|
|
- Deletion cleanup: `Auth\DeletedUserCleanup` (hooked in `Plugin::boot()`)
|
|
- Upcoming/past split: `Auth\StudentSchedule::partition()` (pure, unit-tested)
|
|
- The upcoming/past split is extracted into a small pure helper so it is
|
|
unit-testable (the controller itself follows the repo convention of not being
|
|
unit-tested).
|
|
|
|
## Tests
|
|
- `tests/Unit/Auth/StudentScheduleTest.php` (the pure upcoming/past split helper)
|
|
- `tests/Unit/Auth/DeletedUserCleanupTest.php` (release on user deletion)
|
|
- `tests/Unit/Auth/StudentHistoryTest.php` (history display rows + fallbacks)
|
|
- `tests/Unit/Auth/StudentActionsTest.php` (cancel/withdraw guards + side
|
|
effects, account validation)
|
|
- `findByStudent` coverage in `tests/Unit/Policy/AcceptanceRepositoryTest.php`,
|
|
`tests/Unit/Registration/AnswerRepositoryTest.php`, and
|
|
`tests/Unit/Payment/PaymentRepositoryTest.php`
|
|
|
|
## Family Relationships
|
|
The students list gains a **Profile** column — a child links to their guardian,
|
|
a guardian lists their children — and the student screen a **Profile** panel. A
|
|
child's listed email is their guardian's, since a child's own address is an
|
|
undeliverable placeholder, and the credit balance shown is the payer's, labelled
|
|
with whose account holds it. See `parent-guardian-accounts.md`.
|