maybeRenderDetail( 'us-scheduler', false ) ) { return; } $this->handleEtransferUpdate( false ); $rows = array_map( fn( Lesson $lesson ): array => $this->row( $lesson ), $this->repository->findAllUpcoming() ); $this->renderLessonsPage( $rows, 'us-scheduler' ); } public function renderInstructorLessons(): void { if ( ! current_user_can( RoleManager::CAP_VIEW_LESSONS ) ) { wp_die( esc_html__( 'You do not have permission to view lessons.', 'unsupervised-schedular' ) ); } if ( $this->maybeRenderDetail( 'us-my-lessons', true ) ) { return; } $this->handleEtransferUpdate( true ); $rows = array_map( fn( Lesson $lesson ): array => $this->row( $lesson ), $this->repository->findUpcomingForInstructor( get_current_user_id() ) ); $this->renderLessonsPage( $rows, 'us-my-lessons' ); } /** * When the request targets a single lesson (`?lesson_id=`), render its detail * view and report that the page has been handled. Instructors may only open * their own lessons; the studio dashboard ($onlyOwn = false) may open any. */ private function maybeRenderDetail( string $pageSlug, bool $onlyOwn ): bool { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only lesson selector. $lessonId = absint( Val::int( $_GET['lesson_id'] ?? 0 ) ); if ( $lessonId <= 0 ) { return false; } $lesson = $this->repository->findById( $lessonId ); $backUrl = admin_url( 'admin.php?page=' . $pageSlug ); if ( null === $lesson || ( $onlyOwn && get_current_user_id() !== $lesson->instructorId ) ) { $row = null; $answers = []; $accepts = []; } else { $row = $this->row( $lesson ); $answers = $this->detail->answers( $lessonId ); $accepts = $this->detail->acceptances( $lessonId ); } include USC_PLUGIN_DIR . 'templates/admin/lesson-detail.php'; return true; } /** * Render the lessons template with its calendar view state: week (default) * or list, plus which week the week view shows. * * @param list> $rows */ private function renderLessonsPage( array $rows, string $pageSlug ): void { // View-state query params only (which view, which week) — nothing is // mutated from them, so no nonce applies. // phpcs:disable WordPress.Security.NonceVerification.Recommended $view = 'list' === sanitize_key( Val::string( wp_unslash( $_GET['usc_view'] ?? '' ) ) ) ? 'list' : 'week'; $requestedWeek = sanitize_text_field( Val::string( wp_unslash( $_GET['usc_week'] ?? '' ) ) ); // phpcs:enable WordPress.Security.NonceVerification.Recommended $weekStart = WeekCalendar::weekStart( $requestedWeek, Val::int( get_option( 'start_of_week', 1 ) ), current_time( 'Y-m-d' ) ); $weekDays = WeekCalendar::bucket( $weekStart, $rows, static fn( array $row ): string => Val::string( $row['day'] ) ); $prevWeek = ( new \DateTimeImmutable( $weekStart ) )->modify( '-7 days' )->format( 'Y-m-d' ); $nextWeek = ( new \DateTimeImmutable( $weekStart ) )->modify( '+7 days' )->format( 'Y-m-d' ); $baseUrl = admin_url( 'admin.php?page=' . $pageSlug ); include USC_PLUGIN_DIR . 'templates/admin/lessons.php'; } /** * Handle a per-lesson payment override (e-transfer email or HST rate). When * $onlyOwn, the payment must belong to the current instructor. */ private function handleEtransferUpdate( bool $onlyOwn ): void { if ( ! isset( $_POST['usc_action'] ) || ! check_admin_referer( 'usc_lesson_action' ) ) { return; } // phpcs:disable WordPress.Security.NonceVerification.Missing -- nonce checked above. $action = sanitize_key( Val::string( wp_unslash( $_POST['usc_action'] ) ) ); $paymentId = absint( Val::int( $_POST['payment_id'] ?? 0 ) ); $email = sanitize_email( Val::string( wp_unslash( $_POST['etransfer_email'] ?? '' ) ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Val::float() coerces to float; slashes cannot survive numeric coercion. $taxRate = isset( $_POST['tax_rate'] ) ? max( 0.0, Val::float( $_POST['tax_rate'] ) ) : 0.0; // phpcs:enable WordPress.Security.NonceVerification.Missing if ( $paymentId <= 0 || ! in_array( $action, [ 'set_etransfer', 'set_tax' ], true ) ) { return; } $payment = $this->payments->findById( $paymentId ); if ( null === $payment || ( $onlyOwn && get_current_user_id() !== $payment->instructorId ) ) { return; } if ( 'set_tax' === $action ) { $this->payments->updateTax( $paymentId, $taxRate ); return; } $this->payments->updateEtransferEmail( $paymentId, '' !== $email ? $email : null ); } /** * Build a display row for a lesson, including its e-transfer and HST payment * overrides. * * @return array */ private function row( Lesson $lesson ): array { $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 ); $offering = null !== $lesson->offeringId ? $this->offerings->findById( $lesson->offeringId ) : null; return [ 'lesson_id' => (int) $lesson->id, 'student' => $student ? $student->display_name : (string) $lesson->studentId, 'instructor' => $instructor ? $instructor->display_name : (string) $lesson->instructorId, 'offering' => $offering ? $offering->title : '—', 'duration' => null !== $offering && null !== $offering->durationMinutes ? $offering->durationMinutes : 0, 'recurrence' => $lesson->recurrence, 'time' => $slot ? $this->formatSlotTime( $slot ) : '—', 'day' => $slot ? substr( $slot->startDt, 0, 10 ) : '', 'time_short' => $slot ? Val::string( mysql2date( 'g:i A', $slot->startDt ) ) : '—', 'status' => $lesson->status, 'notes' => $lesson->notes ?? '', 'payment_id' => $payment ? (int) $payment->id : 0, 'currency' => $payment ? (string) $payment->currency : '', 'amount' => $payment ? (float) $payment->amount : 0.0, 'tax_rate' => $payment ? (float) $payment->taxRate : 0.0, 'tax_amount' => $payment ? (float) $payment->taxAmount : 0.0, 'total' => $payment ? $payment->total() : 0.0, 'etransfer_email' => $payment ? (string) $payment->etransferEmail : '', 'etransfer_editable' => null !== $payment && Payment::METHOD_ETRANSFER === $payment->method && ! $payment->isPaid(), '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 ) ); } }