From 6ff733a71fb5600260874efb0e93ef426ca1c165 Mon Sep 17 00:00:00 2001 From: James Griffin Date: Sun, 5 Jul 2026 15:51:54 -0300 Subject: [PATCH] Replace Slot ID column in lessons list with the lesson's date/time The admin dashboard and instructor My Lessons pages showed the raw availability-slot database ID, which is meaningless to admins and instructors. LessonController now takes AvailabilityRepository, looks up each lesson's slot, and renders its window as e.g. "Jul 6, 2026 9:00 AM-10:00 AM" via mysql2date. The date is repeated on the end time only when a slot crosses midnight, and lessons whose slot row no longer exists show an em dash. Closes #47 Co-Authored-By: Claude Fable 5 --- src/AdminMenu.php | 2 +- src/Booking/LessonController.php | 18 ++- templates/admin/lessons.php | 6 +- tests/Unit/Booking/LessonControllerTest.php | 120 ++++++++++++++++++++ 4 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 tests/Unit/Booking/LessonControllerTest.php diff --git a/src/AdminMenu.php b/src/AdminMenu.php index 994a83f..e5109fb 100644 --- a/src/AdminMenu.php +++ b/src/AdminMenu.php @@ -48,7 +48,7 @@ class AdminMenu { public function __construct( AvailabilityRepository $availability, BookingRepository $bookings, OfferingRepository $offerings, QuestionRepository $questions, PolicyRepository $policies, PolicyVersionRepository $policyVersions, PolicyService $policyService, InviteRepository $invites, EnrollmentRepository $enrollments, StudioSettings $settings, PaymentRepository $payments, PaymentService $paymentService, BillingMethodResolver $resolver ) { $this->availabilityController = new AvailabilityController( $availability, $offerings ); - $this->lessonController = new LessonController( $bookings, $payments ); + $this->lessonController = new LessonController( $bookings, $payments, $availability ); $this->offeringController = new OfferingController( $offerings ); $this->questionController = new QuestionController( $questions, $offerings ); $this->policyController = new PolicyController( $policies, $policyVersions, $policyService ); diff --git a/src/Booking/LessonController.php b/src/Booking/LessonController.php index b83d7b8..ee87ab2 100644 --- a/src/Booking/LessonController.php +++ b/src/Booking/LessonController.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Unsupervised\Schedular\Booking; use Unsupervised\Schedular\Auth\RoleManager; +use Unsupervised\Schedular\Availability\AvailabilityRepository; +use Unsupervised\Schedular\Availability\AvailabilitySlot; use Unsupervised\Schedular\Payment\Payment; use Unsupervised\Schedular\Payment\PaymentRepository; use Unsupervised\Schedular\Val; @@ -13,6 +15,7 @@ class LessonController { public function __construct( private BookingRepository $repository, private PaymentRepository $payments, + private AvailabilityRepository $availability, ) {} public function renderAdminDashboard(): void { @@ -83,11 +86,12 @@ class LessonController { $student = get_userdata( $lesson->studentId ); $instructor = get_userdata( $lesson->instructorId ); $payment = null !== $lesson->paymentId ? $this->payments->findById( $lesson->paymentId ) : null; + $slot = $this->availability->findById( $lesson->slotId ); return [ 'student' => $student ? $student->display_name : (string) $lesson->studentId, 'instructor' => $instructor ? $instructor->display_name : (string) $lesson->instructorId, - 'slot_id' => (int) $lesson->slotId, + 'time' => $slot ? $this->formatSlotTime( $slot ) : '—', 'status' => $lesson->status, 'notes' => $lesson->notes ?? '', 'payment_id' => $payment ? (int) $payment->id : 0, @@ -101,4 +105,16 @@ class LessonController { 'tax_editable' => null !== $payment && ! $payment->isPaid(), ]; } + + /** + * Format a slot's window as e.g. "Jul 6, 2026 9:00 AM–10:00 AM", repeating the + * date on the end time only when the slot crosses midnight. + */ + private function formatSlotTime( AvailabilitySlot $slot ): string { + $sameDay = substr( $slot->startDt, 0, 10 ) === substr( $slot->endDt, 0, 10 ); + + return Val::string( mysql2date( 'M j, Y g:i A', $slot->startDt ) ) + . '–' + . Val::string( mysql2date( $sameDay ? 'g:i A' : 'M j, Y g:i A', $slot->endDt ) ); + } } diff --git a/templates/admin/lessons.php b/templates/admin/lessons.php index 691d84f..8b90373 100644 --- a/templates/admin/lessons.php +++ b/templates/admin/lessons.php @@ -5,7 +5,7 @@ if (! defined('ABSPATH')) { exit; } -/** @var list $rows */ +/** @var list $rows */ ?>

@@ -18,7 +18,7 @@ if (! defined('ABSPATH')) { - + @@ -31,7 +31,7 @@ if (! defined('ABSPATH')) { - + diff --git a/tests/Unit/Booking/LessonControllerTest.php b/tests/Unit/Booking/LessonControllerTest.php new file mode 100644 index 0000000..3ced1b8 --- /dev/null +++ b/tests/Unit/Booking/LessonControllerTest.php @@ -0,0 +1,120 @@ +bookings = Mockery::mock(BookingRepository::class); + $this->payments = Mockery::mock(PaymentRepository::class); + $this->availability = Mockery::mock(AvailabilityRepository::class); + $this->controller = new LessonController($this->bookings, $this->payments, $this->availability); + + $_POST = []; + + Functions\when('current_user_can')->justReturn(true); + Functions\when('get_userdata')->justReturn(false); + Functions\when('mysql2date')->alias( + static fn (string $format, string $date) => date($format, (int) strtotime($date)) + ); + } + + public function testAdminDashboardShowsSlotDateTimeInsteadOfSlotId(): void + { + $lesson = new Lesson(slotId: 10, studentId: 5, instructorId: 3, id: 1); + $slot = new AvailabilitySlot( + instructorId: 3, + startDt: '2026-07-06 09:00:00', + endDt: '2026-07-06 10:00:00', + id: 10 + ); + + $this->bookings->shouldReceive('findAllUpcoming')->once()->andReturn([$lesson]); + $this->availability->shouldReceive('findById')->once()->with(10)->andReturn($slot); + + $html = $this->render(); + + self::assertStringContainsString('Jul 6, 2026 9:00 AM–10:00 AM', $html); + self::assertStringContainsString('Date/Time', $html); + self::assertStringNotContainsString('Slot ID', $html); + } + + public function testSlotCrossingMidnightRepeatsTheDateOnTheEndTime(): void + { + $lesson = new Lesson(slotId: 11, studentId: 5, instructorId: 3, id: 2); + $slot = new AvailabilitySlot( + instructorId: 3, + startDt: '2026-07-06 23:00:00', + endDt: '2026-07-07 00:30:00', + id: 11 + ); + + $this->bookings->shouldReceive('findAllUpcoming')->once()->andReturn([$lesson]); + $this->availability->shouldReceive('findById')->once()->with(11)->andReturn($slot); + + $html = $this->render(); + + self::assertStringContainsString('Jul 6, 2026 11:00 PM–Jul 7, 2026 12:30 AM', $html); + } + + public function testMissingSlotRendersDash(): void + { + $lesson = new Lesson(slotId: 99, studentId: 5, instructorId: 3, id: 3); + + $this->bookings->shouldReceive('findAllUpcoming')->once()->andReturn([$lesson]); + $this->availability->shouldReceive('findById')->once()->with(99)->andReturn(null); + + $html = $this->render(); + + self::assertStringContainsString('—', $html); + } + + public function testInstructorLessonsShowSlotDateTime(): void + { + Functions\when('get_current_user_id')->justReturn(3); + + $lesson = new Lesson(slotId: 12, studentId: 5, instructorId: 3, id: 4); + $slot = new AvailabilitySlot( + instructorId: 3, + startDt: '2026-08-01 14:00:00', + endDt: '2026-08-01 15:00:00', + id: 12 + ); + + $this->bookings->shouldReceive('findUpcomingForInstructor')->once()->with(3)->andReturn([$lesson]); + $this->availability->shouldReceive('findById')->once()->with(12)->andReturn($slot); + + ob_start(); + $this->controller->renderInstructorLessons(); + $html = (string) ob_get_clean(); + + self::assertStringContainsString('Aug 1, 2026 2:00 PM–3:00 PM', $html); + } + + private function render(): string + { + ob_start(); + $this->controller->renderAdminDashboard(); + + return (string) ob_get_clean(); + } +}