Replace Slot ID column in lessons list with the lesson's date/time #48

Merged
thatguygriff merged 1 commits from feature/lessons-datetime-column into main 2026-07-05 18:57:16 +00:00
4 changed files with 141 additions and 5 deletions
+1 -1
View File
@@ -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 );
+17 -1
View File
@@ -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 AM10: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 ) );
}
}
+3 -3
View File
@@ -5,7 +5,7 @@ if (! defined('ABSPATH')) {
exit;
}
/** @var list<array{student: string, instructor: string, slot_id: int, status: string, notes: string, payment_id: int, currency: string, amount: float, tax_rate: float, tax_amount: float, total: float, etransfer_email: string, etransfer_editable: bool, tax_editable: bool}> $rows */
/** @var list<array{student: string, instructor: string, time: string, status: string, notes: string, payment_id: int, currency: string, amount: float, tax_rate: float, tax_amount: float, total: float, etransfer_email: string, etransfer_editable: bool, tax_editable: bool}> $rows */
?>
<div class="wrap">
<h1><?php esc_html_e('Lessons', 'unsupervised-schedular'); ?></h1>
@@ -18,7 +18,7 @@ if (! defined('ABSPATH')) {
<tr>
<th><?php esc_html_e('Student', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Instructor', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Slot ID', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Date/Time', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Status', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('HST', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Total', 'unsupervised-schedular'); ?></th>
@@ -31,7 +31,7 @@ if (! defined('ABSPATH')) {
<tr>
<td><?php echo esc_html($row['student']); ?></td>
<td><?php echo esc_html($row['instructor']); ?></td>
<td><?php echo esc_html((string) $row['slot_id']); ?></td>
<td><?php echo esc_html($row['time']); ?></td>
<td><?php echo esc_html($row['status']); ?></td>
<td>
<?php if ($row['tax_editable']) : ?>
+120
View File
@@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Unsupervised\Schedular\Tests\Unit\Booking;
use Brain\Monkey\Functions;
use Mockery;
use Unsupervised\Schedular\Availability\AvailabilityRepository;
use Unsupervised\Schedular\Availability\AvailabilitySlot;
use Unsupervised\Schedular\Booking\BookingRepository;
use Unsupervised\Schedular\Booking\Lesson;
use Unsupervised\Schedular\Booking\LessonController;
use Unsupervised\Schedular\Payment\PaymentRepository;
use Unsupervised\Schedular\Tests\Unit\TestCase;
class LessonControllerTest extends TestCase
{
private BookingRepository&Mockery\MockInterface $bookings;
private PaymentRepository&Mockery\MockInterface $payments;
private AvailabilityRepository&Mockery\MockInterface $availability;
private LessonController $controller;
protected function setUp(): void
{
parent::setUp();
$this->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 AM10: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 PMJul 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 PM3:00 PM', $html);
}
private function render(): string
{
ob_start();
$this->controller->renderAdminDashboard();
return (string) ob_get_clean();
}
}