Add student administration view (studio-admin)
CI / Tests (PHP 8.1) (pull_request) Successful in 43s
CI / Coding Standards (pull_request) Successful in 56s
CI / PHPStan (pull_request) Successful in 57s
CI / No Debug Code (pull_request) Successful in 2s
CI / Tests (PHP 8.2) (pull_request) Successful in 44s
CI / Tests (PHP 8.3) (pull_request) Successful in 48s
CI / Build Plugin Zip (pull_request) Has been skipped

Implements #22: a read-only Students area for studio admins.

- StudentController (manage_students): a list of us_student users with
  upcoming-lesson and active-enrolment counts, each linking to a detail page
  showing account info, upcoming/past lessons (offering, instructor, status),
  and group-class enrolments.
- StudentSchedule::partition() — pure, unit-tested upcoming/past split.
- Repo counts: BookingRepository::countUpcomingForStudent and
  EnrollmentRepository::countActiveForStudent (single-query, tested).
- Templates: templates/admin/students.php, student-detail.php.
- Students admin menu wired in AdminMenu (no Plugin change — the repos were
  already available there).
- Docs: README status flipped to implemented; feature spec updated.

Payment history slots into the detail when Payments (#7) lands.

Tests: StudentScheduleTest + the two repo count tests. composer test (127),
cs, and PHPStan level 6 all pass.

Refs #22

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-08 09:28:28 -03:00
parent d86e852edc
commit 8fb5ff8270
12 changed files with 415 additions and 4 deletions
+103
View File
@@ -0,0 +1,103 @@
<?php
declare(strict_types=1);
namespace Unsupervised\Schedular\Auth;
use Unsupervised\Schedular\Availability\AvailabilityRepository;
use Unsupervised\Schedular\Booking\BookingRepository;
use Unsupervised\Schedular\Booking\Lesson;
use Unsupervised\Schedular\GroupClass\Enrollment;
use Unsupervised\Schedular\GroupClass\EnrollmentRepository;
use Unsupervised\Schedular\Offering\OfferingRepository;
class StudentController {
public function __construct(
private BookingRepository $bookings,
private AvailabilityRepository $availability,
private OfferingRepository $offerings,
private EnrollmentRepository $enrollments,
) {}
public function renderPage(): void {
if ( ! current_user_can( RoleManager::CAP_MANAGE_STUDENTS ) ) {
wp_die( esc_html__( 'You do not have permission to view students.', 'unsupervised-schedular' ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only student selector.
$studentId = absint( $_GET['student_id'] ?? 0 );
$student = $studentId > 0 ? get_userdata( $studentId ) : false;
if ( $student && in_array( RoleManager::STUDENT, (array) $student->roles, true ) ) {
$this->renderDetail( $student );
return;
}
$students = array_map(
fn( \WP_User $user ): array => [
'id' => (int) $user->ID,
'name' => $user->display_name,
'email' => $user->user_email,
'registered' => $user->user_registered,
'upcoming' => $this->bookings->countUpcomingForStudent( (int) $user->ID ),
'enrolments' => $this->enrollments->countActiveForStudent( (int) $user->ID ),
],
get_users(
[
'role' => RoleManager::STUDENT,
'orderby' => 'display_name',
'order' => 'ASC',
]
)
);
$pageSlug = 'us-students';
include USC_PLUGIN_DIR . 'templates/admin/students.php';
}
private function renderDetail( \WP_User $student ): void {
$now = current_time( 'mysql' );
$rows = array_map(
fn( Lesson $lesson ): array => $this->lessonRow( $lesson ),
$this->bookings->findByStudent( (int) $student->ID )
);
$schedule = StudentSchedule::partition( $rows, $now );
$upcoming = $schedule['upcoming'];
$past = $schedule['past'];
$enrolments = array_map(
function ( Enrollment $enrollment ): array {
$offering = $this->offerings->findById( $enrollment->offeringId );
return [
'offering' => $offering ? $offering->title : (string) $enrollment->offeringId,
'status' => $enrollment->status,
];
},
$this->enrollments->findByStudent( (int) $student->ID )
);
$backUrl = admin_url( 'admin.php?page=us-students' );
include USC_PLUGIN_DIR . 'templates/admin/student-detail.php';
}
/**
* Build a display row for a lesson (slot time, offering, instructor, status).
*
* @return array<string, mixed>
*/
private function lessonRow( Lesson $lesson ): array {
$slot = $this->availability->findById( $lesson->slotId );
$offering = null !== $lesson->offeringId ? $this->offerings->findById( $lesson->offeringId ) : null;
$instructor = get_userdata( $lesson->instructorId );
return [
'start_dt' => $slot ? $slot->startDt : '',
'end_dt' => $slot ? $slot->endDt : '',
'offering' => $offering ? $offering->title : '—',
'instructor' => $instructor ? $instructor->display_name : (string) $lesson->instructorId,
'status' => $lesson->status,
];
}
}