A parent registers once and manages lessons for one or more children, who need no login of their own. A child is a real wp_users row with the student role but no usable login — so student_id keeps meaning "a WordPress user" on every table, and booking, credits, policies and enrolments work unchanged. A us_guardians link table maps guardian to child. The signup form gains a parent/guardian tick that reveals a block per child, with the account-signup questions asked per child rather than per guardian — they describe the student, not the account holder. Signup policies are recorded once per child with the guardian as the acceptor, which is the record that actually means something. A family that half-creates is rolled back entirely rather than leaving a guardian who cannot re-register. The booking and enrolment forms gain a "Who is this for?" picker listing children first, so the default selection is never the parent — booking for the wrong child is correctable, quietly billing a parent for their kid's lesson is not. POST /bookings and POST /enrollments take an optional student_id honoured only for that child's guardian; anything else is a 403. That check is the authorisation boundary of the feature. Payments and credits gain a payer: the charge names the child it was for and the guardian who owes it, so per-child reporting is unchanged while notices, receipts and the payment step reach the parent. Credit is held by the payer, so one child's cancellation can settle a sibling's charge, and the daily billing scan sends a guardian one notice covering every child. Closes #132 Co-Authored-By: Claude Opus 5 <[email protected]>
123 lines
4.5 KiB
PHP
123 lines
4.5 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Unsupervised\Schedular\Booking;
|
|
|
|
use Unsupervised\Schedular\Auth\RegistrationStatus;
|
|
use Unsupervised\Schedular\Auth\RoleManager;
|
|
use Unsupervised\Schedular\Guardian\GuardianService;
|
|
use Unsupervised\Schedular\Val;
|
|
|
|
class BookingPage {
|
|
|
|
/** Booking calendar and the student's upcoming lessons (the default). */
|
|
public const MODE_BOTH = 'both';
|
|
|
|
/** Booking calendar only — no upcoming-lessons panel. */
|
|
public const MODE_BOOKING = 'booking';
|
|
|
|
/** The student's upcoming lessons only — nothing bookable. */
|
|
public const MODE_UPCOMING = 'upcoming';
|
|
|
|
public function __construct( private GuardianService $guardians ) {}
|
|
|
|
/**
|
|
* Renders the booking shortcode/block output.
|
|
*
|
|
* Supported attributes (block / shortcode form):
|
|
* - `loginPageId` / `login_page_id` — where logged-out visitors are sent.
|
|
* - `lessonTypeId` / `lesson_type` — a private-lesson offering id that pins
|
|
* the calendar to one lesson type: only the times bookable as that type
|
|
* are listed, and only it can be booked. 0 or absent shows every type.
|
|
* - `showTypeFilter` / `show_filter` — whether the "Show Only" lesson-type
|
|
* filter is offered (default true; irrelevant when a type is pinned).
|
|
* - `displayMode` / `show` — which halves of the page to embed:
|
|
* {@see self::MODE_BOTH} (default), {@see self::MODE_BOOKING} (calendar
|
|
* only) or {@see self::MODE_UPCOMING} (the student's lessons only).
|
|
*
|
|
* @param array<int|string, mixed> $atts Block or shortcode attributes.
|
|
*/
|
|
public function render( array $atts ): string {
|
|
if ( ! is_user_logged_in() ) {
|
|
$loginPageId = Val::int( $atts['loginPageId'] ?? $atts['login_page_id'] ?? 0 );
|
|
|
|
return sprintf(
|
|
'<p>%s <a href="%s">%s</a>.</p>',
|
|
esc_html__( 'Please', 'unsupervised-schedular' ),
|
|
esc_url( $this->loginUrl( $loginPageId ) ),
|
|
esc_html__( 'log in to book a lesson', 'unsupervised-schedular' )
|
|
);
|
|
}
|
|
|
|
if ( RegistrationStatus::isAwaitingApproval( get_current_user_id() ) ) {
|
|
return '<p>' . esc_html__( 'Your account is awaiting studio approval. You will be able to book once a studio admin approves it.', 'unsupervised-schedular' ) . '</p>';
|
|
}
|
|
|
|
if ( ! current_user_can( RoleManager::CAP_BOOK_LESSON ) ) {
|
|
return '<p>' . esc_html__( 'This page is for students only.', 'unsupervised-schedular' ) . '</p>';
|
|
}
|
|
|
|
wp_enqueue_style( 'us-scheduler' );
|
|
wp_enqueue_script( 'us-scheduler' );
|
|
|
|
$lessonTypeId = absint( Val::int( $atts['lessonTypeId'] ?? $atts['lesson_type'] ?? 0 ) );
|
|
$showTypeFilter = self::toBool( $atts['showTypeFilter'] ?? $atts['show_filter'] ?? true );
|
|
|
|
$mode = self::mode( $atts['displayMode'] ?? $atts['show'] ?? self::MODE_BOTH );
|
|
$showBooking = self::MODE_UPCOMING !== $mode;
|
|
$showUpcoming = self::MODE_BOOKING !== $mode;
|
|
|
|
// Who this account may book for. A single-student account gets one entry
|
|
// (themselves) and no selector at all; a guardian's list leads with their
|
|
// children, so the default choice is never the parent.
|
|
$students = $this->guardians->bookableStudents( get_current_user_id() );
|
|
|
|
ob_start();
|
|
include USC_PLUGIN_DIR . 'templates/frontend/booking-page.php';
|
|
return (string) ob_get_clean();
|
|
}
|
|
|
|
/**
|
|
* Normalises the display-mode attribute; anything unrecognised embeds the
|
|
* whole page, so a typo never silently hides half of it.
|
|
*/
|
|
private static function mode( mixed $value ): string {
|
|
$mode = strtolower( trim( Val::string( $value ) ) );
|
|
|
|
return in_array( $mode, [ self::MODE_BOOKING, self::MODE_UPCOMING ], true ) ? $mode : self::MODE_BOTH;
|
|
}
|
|
|
|
/**
|
|
* Reads a boolean attribute. Block attributes arrive as real booleans,
|
|
* shortcode attributes as strings — where the words people actually write
|
|
* for "off" ("no", "false", "off") are all truthy to PHP, so they are
|
|
* matched explicitly rather than cast.
|
|
*/
|
|
private static function toBool( mixed $value ): bool {
|
|
if ( is_string( $value ) ) {
|
|
return ! in_array( strtolower( trim( $value ) ), [ '', '0', 'no', 'false', 'off' ], true );
|
|
}
|
|
|
|
return Val::bool( $value );
|
|
}
|
|
|
|
/**
|
|
* URL the logged-out prompt sends visitors to: the chosen login page when
|
|
* one is configured (and still exists), otherwise the WordPress login
|
|
* screen with a redirect back to the current page.
|
|
*/
|
|
public function loginUrl( int $loginPageId ): string {
|
|
if ( $loginPageId > 0 ) {
|
|
$url = get_permalink( $loginPageId );
|
|
|
|
if ( is_string( $url ) ) {
|
|
return $url;
|
|
}
|
|
}
|
|
|
|
$permalink = get_permalink();
|
|
|
|
return wp_login_url( false === $permalink ? '' : $permalink );
|
|
}
|
|
}
|