Files
unsupervised-scheduler/src/BlockPreview.php
T
thatguygriffandClaude Opus 5 b772e1811e Let parents register once and book for their children
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]>
2026-07-29 16:07:52 -03:00

211 lines
8.0 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
declare(strict_types=1);
namespace Unsupervised\Schedular;
/**
* Static, script-free markup for the editor previews of the front-end blocks.
*
* The booking and group-class pages are populated by JavaScript on the live
* site, and the registration page requires a valid invite token — none of
* which exist inside the block editor. These previews reproduce the same
* wrapper elements and CSS classes the live pages use, filled with
* representative placeholder content, so themes can be styled against
* realistic markup without firing REST calls, redirects, or Stripe.js.
*/
class BlockPreview {
/**
* Sample booking page.
*
* @param string $mode Which halves the block embeds — one of
* {@see Booking\BookingPage::MODE_BOTH},
* `MODE_BOOKING` or `MODE_UPCOMING`. The preview shows
* the same sections the published page would.
*/
public static function booking( string $mode = Booking\BookingPage::MODE_BOTH ): string {
if ( Booking\BookingPage::MODE_UPCOMING === $mode ) {
return sprintf(
'<div id="us-booking-app">%s<div id="us-my-lessons">%s</div></div>',
self::note( __( 'Editor preview — students see their own lessons on the published page.', 'unsupervised-schedular' ) ),
self::upcomingLessons()
);
}
$days = [
[
'label' => __( 'Monday', 'unsupervised-schedular' ),
'slots' => [
[ '4:00 PM4:30 PM', 30 ],
[ '4:30 PM5:00 PM', 30 ],
],
],
[
'label' => __( 'Wednesday', 'unsupervised-schedular' ),
'slots' => [
[ '5:00 PM5:45 PM', 45 ],
],
],
];
$dayHtml = '';
foreach ( $days as $day ) {
$slotHtml = '';
foreach ( $day['slots'] as $slot ) {
$slotHtml .= sprintf(
'<div class="us-slot"><span>%s (%d min)</span><button type="button" class="us-book-btn" disabled>%s</button></div>',
esc_html( $slot[0] ),
(int) $slot[1],
esc_html__( 'Book', 'unsupervised-schedular' )
);
}
$dayHtml .= sprintf(
'<div class="us-day"><h3 class="us-day-heading">%s</h3>%s</div>',
esc_html( $day['label'] ),
$slotHtml
);
}
$lessons = Booking\BookingPage::MODE_BOOKING === $mode
? ''
: sprintf( '<div id="us-my-lessons">%s</div>', self::upcomingLessons() );
return sprintf(
'<div id="us-booking-app">%s%s<div id="us-slot-list">%s</div></div>',
self::note( __( 'Editor preview — students see live availability on the published page.', 'unsupervised-schedular' ) ),
$lessons,
$dayHtml
);
}
/**
* Sample "your upcoming lessons" panel, shared by the booking preview's
* full and upcoming-only modes.
*/
private static function upcomingLessons(): string {
return sprintf(
'<div class="us-my-lessons"><h3>%s</h3>'
. '<div class="us-my-lesson"><div class="us-my-lesson-info">'
. '<strong class="us-my-lesson-title">%s <span class="us-my-lesson-duration">(30 min)</span></strong>'
. '<span class="us-my-lesson-when">%s</span></div>'
. '<div class="us-my-lesson-actions">'
. '<span class="us-lesson-status us-lesson-status-confirmed">%s</span>'
. '<button type="button" class="us-cancel-lesson" disabled>%s</button>'
. '</div></div></div>',
esc_html__( 'Your upcoming lessons', 'unsupervised-schedular' ),
esc_html__( 'Piano Lesson', 'unsupervised-schedular' ),
esc_html__( 'Monday · 4:00 PM4:30 PM', 'unsupervised-schedular' ),
esc_html__( 'Confirmed', 'unsupervised-schedular' ),
esc_html__( 'Cancel', 'unsupervised-schedular' )
);
}
/**
* Sample group-class card.
*
* @param bool $singleClass Whether the block is pinned to one class, in
* which case the live page omits the class
* description and the preview does too.
*/
public static function groupClasses( bool $singleClass = false ): string {
$note = $singleClass
? __( 'Editor preview — the published page shows the chosen class with its live schedule and enrolment status.', 'unsupervised-schedular' )
: __( 'Editor preview — students see live group classes on the published page.', 'unsupervised-schedular' );
$description = $singleClass
? ''
: '<p>' . esc_html__( 'A sample class shown so the page can be styled.', 'unsupervised-schedular' ) . '</p>';
return sprintf(
'<div id="us-group-app">%s<div id="us-group-list"><div class="us-class"><h3>%s</h3><p class="us-class-when">%s</p>%s<p class="us-class-price">%s</p><p class="us-enrol-deadline">%s</p><button type="button" class="us-enrol-btn" disabled>%s</button></div></div></div>',
self::note( $note ),
esc_html__( 'Beginner Group Class', 'unsupervised-schedular' ),
esc_html__( 'Saturdays 10:00 AM11:00 AM', 'unsupervised-schedular' ),
$description,
// Prices on the live page always carry their cadence, so the sample does too.
esc_html__( '25.00 CAD up front', 'unsupervised-schedular' ),
esc_html__( 'Enrol by Sep 6, 2026', 'unsupervised-schedular' ),
esc_html__( 'Enrol', 'unsupervised-schedular' )
);
}
/**
* The live login form renders fine without any request state, so the
* preview includes the real template (the editing user is logged in, which
* would otherwise short-circuit to an "already logged in" message).
*/
public static function login(): string {
$error = '';
ob_start();
include USC_PLUGIN_DIR . 'templates/frontend/login-page.php';
return self::note( __( 'Editor preview — logged-in visitors are offered a link to the booking page instead.', 'unsupervised-schedular' ) ) . (string) ob_get_clean();
}
public static function registration(): string {
$fields = sprintf(
'<p><label for="us-reg-email">%s</label><input type="email" id="us-reg-email" value="[email protected]" readonly></p>',
esc_html__( 'Email', 'unsupervised-schedular' )
);
$fields .= sprintf(
'<p><label for="us-reg-name">%s</label><input type="text" id="us-reg-name"></p>',
esc_html__( 'Your name', 'unsupervised-schedular' )
);
$fields .= sprintf(
'<p><label for="us-reg-pass">%s</label><input type="password" id="us-reg-pass"></p>',
esc_html__( 'Password', 'unsupervised-schedular' )
);
$fields .= sprintf(
'<p><input type="submit" value="%s" disabled></p>',
esc_attr__( 'Create Account', 'unsupervised-schedular' )
);
return sprintf(
'<div class="us-register-form">%s<form>%s</form></div>',
self::note( __( 'Editor preview — the live form requires a valid invite link and lists signup policies.', 'unsupervised-schedular' ) ),
$fields
);
}
/**
* Sample family (manage-children) page: two representative children and the
* add form, with the controls inert so the editor preview cannot post.
*/
public static function family(): string {
$children = '';
foreach ( [ 'Ada Lovelace', 'Alan Turing' ] as $name ) {
$children .= sprintf(
'<li class="us-family-child"><span class="us-family-child-name">%s</span>'
. '<span class="us-family-child-actions"><a href="#">%s</a> <button type="button" disabled>%s</button></span></li>',
esc_html( $name ),
esc_html__( 'Edit', 'unsupervised-schedular' ),
esc_html__( 'Remove', 'unsupervised-schedular' )
);
}
$add = sprintf(
'<h4>%s</h4><p><label for="us-child-name">%s</label><input type="text" id="us-child-name"></p>'
. '<p><label for="us-child-dob">%s</label><input type="date" id="us-child-dob"></p>'
. '<p><button type="button" disabled>%s</button></p>',
esc_html__( 'Add a child', 'unsupervised-schedular' ),
esc_html__( 'Name', 'unsupervised-schedular' ),
esc_html__( 'Date of birth', 'unsupervised-schedular' ),
esc_html__( 'Add child', 'unsupervised-schedular' )
);
return sprintf(
'<div class="us-family">%s<h3>%s</h3><ul class="us-family-list">%s</ul><form class="us-family-add">%s</form></div>',
self::note( __( 'Editor preview — signed-in guardians see and manage their own children here.', 'unsupervised-schedular' ) ),
esc_html__( 'Your family', 'unsupervised-schedular' ),
$children,
$add
);
}
private static function note( string $text ): string {
return '<p class="us-editor-note">' . esc_html( $text ) . '</p>';
}
}