CI / Tests (PHP 8.1) (pull_request) Successful in 1m0s
CI / Tests (PHP 8.2) (pull_request) Successful in 1m0s
CI / No Debug Code (pull_request) Successful in 3s
CI / Coding Standards (pull_request) Successful in 3m8s
CI / Build Plugin Zip (pull_request) Skipped
CI / PHPStan (pull_request) Successful in 2m49s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m44s
Five items from the latest demo pass: - A policy's title can be edited from the Policies screen. Only the title moves; the slug is what the gates resolve policies by, so a rename can never detach a policy from acceptances already recorded against it. - Signup is one page again. The studio's registration questions move from a second step behind "Next" onto the main form, in an "About you" panel above the students being added, and that panel also asks an adult student for their birth year (the same us_birth_year meta a child's uses). register.js disables and hides the whole panel for a pure guardian, since the questions describe a student. - The password is re-scored on submit, not only as it is typed. zxcvbn's dictionary arrives after page load, so a password typed straight away was never scored at all and the first the student heard of it was the server rejecting the whole form. - Group-class sessions appear alongside lessons wherever upcoming lessons are listed: the [us_scheduler] panel (students and instructors) and the admin student detail page. GroupClass\SessionSchedule derives them from Offering::sessionWindows(), the same derivation the billing scan uses. They carry kind = 'group_class' and no Cancel action - a session is one date in a term, not a booked slot. - Deleting a user releases what the account was holding: each upcoming lesson is cancelled, its slot freed for rebooking, its pending payment voided, and active class enrolments cancelled. Past lessons and paid history are left alone. Tests: composer test (851), composer lint, composer cs all pass. Co-Authored-By: Claude Opus 5 <[email protected]>
379 lines
20 KiB
PHP
379 lines
20 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
if (! defined('ABSPATH')) {
|
||
exit;
|
||
}
|
||
|
||
use Unsupervised\Schedular\GroupClass\SessionSchedule;
|
||
|
||
/**
|
||
* @var \WP_User $student
|
||
* @var list<array{id: int, kind: string, start_dt: string, end_dt: string, offering: string, instructor: string, status: string}> $upcoming Booked lessons and upcoming group-class sessions, soonest first.
|
||
* @var list<array{id: int, kind: string, start_dt: string, end_dt: string, offering: string, instructor: string, status: string}> $past
|
||
* @var list<array{id: int, offering: string, status: string}> $enrolments
|
||
* @var list<array{policy: string, version: string, context: string, accepted_at: string}> $acceptances
|
||
* @var list<array{question: string, answer: string, required: bool}> $registrationInfo
|
||
* @var list<array{question: string, answer: string, context: string}> $intake
|
||
* @var list<array{created_at: string, context: string, method: string, status: string, amount: float, tax_amount: float, total: float, currency: string, receipt: string}> $payments
|
||
* @var list<array{created_at: string, amount: float, remaining: float, currency: string, reason: string, status: string}> $credits
|
||
* @var float $creditBalance Balance of the account that settles this student's charges — the guardian's for a child.
|
||
* @var string $creditCurrency
|
||
* @var array{id: int, name: string, email: string}|null $guardian The parent/guardian who books for this student, or null when they book for themselves.
|
||
* @var list<array{id: int, name: string, birth_year: string, relationship: string}> $children Children this student books for.
|
||
* @var array{id: int, name: string, email: string} $payer Who is billed for this student — themselves, or their guardian.
|
||
* @var string $pageSlug
|
||
* @var string $backUrl
|
||
* @var bool $canBilling
|
||
* @var string $billingOverride
|
||
* @var string $billingDefault
|
||
* @var string $notice
|
||
* @var string $error
|
||
*/
|
||
|
||
$renderLessons = static function (array $rows, bool $withActions = false): void {
|
||
if (empty($rows)) {
|
||
echo '<p>' . esc_html__('None.', 'unsupervised-schedular') . '</p>';
|
||
return;
|
||
}
|
||
?>
|
||
<table class="wp-list-table widefat fixed striped">
|
||
<thead>
|
||
<tr>
|
||
<th><?php esc_html_e('When', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Offering', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Instructor', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Status', 'unsupervised-schedular'); ?></th>
|
||
<?php if ($withActions) : ?>
|
||
<th><?php esc_html_e('Actions', 'unsupervised-schedular'); ?></th>
|
||
<?php endif; ?>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($rows as $row) : ?>
|
||
<?php $isGroupSession = ($row['kind'] ?? 'lesson') === SessionSchedule::KIND; ?>
|
||
<tr>
|
||
<td><?php echo esc_html($row['start_dt'] !== '' ? (string) mysql2date('M j, Y g:i A', $row['start_dt']) : '—'); ?></td>
|
||
<td>
|
||
<?php echo esc_html($row['offering']); ?>
|
||
<?php if ($isGroupSession) : ?>
|
||
<span class="description">— <?php esc_html_e('group class', 'unsupervised-schedular'); ?></span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td><?php echo esc_html($row['instructor']); ?></td>
|
||
<td><?php echo esc_html($row['status']); ?></td>
|
||
<?php if ($withActions) : ?>
|
||
<td>
|
||
<?php
|
||
/*
|
||
* A group-class session is one date in a term, not a booked
|
||
* slot: there is nothing to cancel session by session. The
|
||
* whole enrolment is withdrawn from the table below.
|
||
*/
|
||
?>
|
||
<?php if (! $isGroupSession && $row['status'] !== 'cancelled') : ?>
|
||
<form method="post" style="display:inline">
|
||
<?php wp_nonce_field('usc_student_actions'); ?>
|
||
<input type="hidden" name="usc_action" value="cancel_lesson">
|
||
<input type="hidden" name="lesson_id" value="<?php echo esc_attr((string) $row['id']); ?>">
|
||
<button type="submit" class="button-link button-link-delete" onclick="return confirm('<?php echo esc_js(__('Cancel this lesson? The slot is freed and any pending payment is voided.', 'unsupervised-schedular')); ?>');">
|
||
<?php esc_html_e('Cancel lesson', 'unsupervised-schedular'); ?>
|
||
</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
</td>
|
||
<?php endif; ?>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php
|
||
};
|
||
?>
|
||
<div class="wrap">
|
||
<h1>
|
||
<?php echo esc_html($student->display_name); ?>
|
||
<a href="<?php echo esc_url($backUrl); ?>" class="page-title-action"><?php esc_html_e('Back to students', 'unsupervised-schedular'); ?></a>
|
||
</h1>
|
||
|
||
<?php if ($notice !== '') : ?>
|
||
<div class="notice notice-success is-dismissible"><p><?php echo esc_html($notice); ?></p></div>
|
||
<?php endif; ?>
|
||
<?php if ($error !== '') : ?>
|
||
<div class="notice notice-error is-dismissible"><p><?php echo esc_html($error); ?></p></div>
|
||
<?php endif; ?>
|
||
|
||
<h2><?php esc_html_e('Account', 'unsupervised-schedular'); ?></h2>
|
||
<form method="post">
|
||
<?php wp_nonce_field('usc_student_actions'); ?>
|
||
<input type="hidden" name="usc_action" value="update_account">
|
||
<table class="form-table">
|
||
<tr>
|
||
<th><label for="usc-display-name"><?php esc_html_e('Display name', 'unsupervised-schedular'); ?></label></th>
|
||
<td><input type="text" id="usc-display-name" name="display_name" class="regular-text" value="<?php echo esc_attr($student->display_name); ?>" required></td>
|
||
</tr>
|
||
<tr>
|
||
<th><label for="usc-user-email"><?php esc_html_e('Email', 'unsupervised-schedular'); ?></label></th>
|
||
<td><input type="email" id="usc-user-email" name="user_email" class="regular-text" value="<?php echo esc_attr($student->user_email); ?>" required></td>
|
||
</tr>
|
||
<tr>
|
||
<th><?php esc_html_e('Registered', 'unsupervised-schedular'); ?></th>
|
||
<td><?php echo esc_html($student->user_registered); ?></td>
|
||
</tr>
|
||
</table>
|
||
<?php submit_button(esc_html__('Save account details', 'unsupervised-schedular'), 'secondary', 'submit', false); ?>
|
||
</form>
|
||
|
||
<?php if ($guardian !== null || ! empty($children)) : ?>
|
||
<h2><?php esc_html_e('Profile', 'unsupervised-schedular'); ?></h2>
|
||
<?php $detailUrl = static fn(int $id): string => add_query_arg(['page' => $pageSlug, 'student_id' => $id], admin_url('admin.php')); ?>
|
||
<?php if ($guardian !== null) : ?>
|
||
<p>
|
||
<?php
|
||
printf(
|
||
/* translators: 1: linked name of the parent/guardian, 2: their email address. */
|
||
esc_html__('Books and pays through %1$s (%2$s).', 'unsupervised-schedular'),
|
||
'<a href="' . esc_url($detailUrl($guardian['id'])) . '">' . esc_html($guardian['name']) . '</a>',
|
||
esc_html($guardian['email'])
|
||
);
|
||
?>
|
||
</p>
|
||
<p class="description"><?php esc_html_e('This is a managed student account: it has no login of its own, and its email address is a placeholder that cannot receive mail.', 'unsupervised-schedular'); ?></p>
|
||
<?php endif; ?>
|
||
<?php if (! empty($children)) : ?>
|
||
<p><?php esc_html_e('Books and pays for:', 'unsupervised-schedular'); ?></p>
|
||
<ul class="ul-disc">
|
||
<?php foreach ($children as $child) : ?>
|
||
<li>
|
||
<a href="<?php echo esc_url($detailUrl($child['id'])); ?>"><?php echo esc_html($child['name']); ?></a>
|
||
<?php if ($child['birth_year'] !== '') : ?>
|
||
<span class="description"><?php echo esc_html($child['birth_year']); ?></span>
|
||
<?php endif; ?>
|
||
</li>
|
||
<?php endforeach; ?>
|
||
</ul>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
|
||
<h2><?php esc_html_e('Registration Information', 'unsupervised-schedular'); ?></h2>
|
||
<?php if (empty($registrationInfo)) : ?>
|
||
<p><?php esc_html_e('No registration questions are configured.', 'unsupervised-schedular'); ?></p>
|
||
<?php else : ?>
|
||
<table class="wp-list-table widefat fixed striped">
|
||
<thead>
|
||
<tr>
|
||
<th><?php esc_html_e('Question', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Answer', 'unsupervised-schedular'); ?></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($registrationInfo as $row) : ?>
|
||
<tr>
|
||
<td>
|
||
<?php echo esc_html($row['question']); ?>
|
||
<?php if ($row['required']) : ?>
|
||
<span class="us-required" aria-hidden="true">*</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td><?php echo esc_html($row['answer']); ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($canBilling) : ?>
|
||
<h2><?php esc_html_e('Billing method', 'unsupervised-schedular'); ?></h2>
|
||
<form method="post">
|
||
<?php wp_nonce_field('usc_student_billing'); ?>
|
||
<input type="hidden" name="usc_action" value="set_billing">
|
||
<select name="payment_method">
|
||
<option value="">
|
||
<?php
|
||
/* translators: %s: the studio default billing method */
|
||
echo esc_html(sprintf(__('Studio default (%s)', 'unsupervised-schedular'), $billingDefault));
|
||
?>
|
||
</option>
|
||
<option value="card" <?php selected($billingOverride, 'card'); ?>><?php esc_html_e('Credit card', 'unsupervised-schedular'); ?></option>
|
||
<option value="etransfer" <?php selected($billingOverride, 'etransfer'); ?>><?php esc_html_e('E-transfer', 'unsupervised-schedular'); ?></option>
|
||
<option value="comp" <?php selected($billingOverride, 'comp'); ?>><?php esc_html_e('Comp (no charge)', 'unsupervised-schedular'); ?></option>
|
||
</select>
|
||
<?php submit_button(esc_html__('Save', 'unsupervised-schedular'), 'secondary', 'submit', false); ?>
|
||
</form>
|
||
<?php endif; ?>
|
||
|
||
<h2><?php esc_html_e('Upcoming lessons', 'unsupervised-schedular'); ?></h2>
|
||
<?php $renderLessons($upcoming, true); ?>
|
||
|
||
<h2><?php esc_html_e('Past lessons', 'unsupervised-schedular'); ?></h2>
|
||
<?php $renderLessons($past); ?>
|
||
|
||
<h2><?php esc_html_e('Group-class enrolments', 'unsupervised-schedular'); ?></h2>
|
||
<?php if (empty($enrolments)) : ?>
|
||
<p><?php esc_html_e('None.', 'unsupervised-schedular'); ?></p>
|
||
<?php else : ?>
|
||
<table class="wp-list-table widefat fixed striped">
|
||
<thead>
|
||
<tr>
|
||
<th><?php esc_html_e('Class', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Status', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Actions', 'unsupervised-schedular'); ?></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($enrolments as $enrolment) : ?>
|
||
<tr>
|
||
<td><?php echo esc_html($enrolment['offering']); ?></td>
|
||
<td><?php echo esc_html($enrolment['status']); ?></td>
|
||
<td>
|
||
<?php if ($enrolment['status'] === 'active') : ?>
|
||
<form method="post" style="display:inline">
|
||
<?php wp_nonce_field('usc_student_actions'); ?>
|
||
<input type="hidden" name="usc_action" value="withdraw_enrollment">
|
||
<input type="hidden" name="enrollment_id" value="<?php echo esc_attr((string) $enrolment['id']); ?>">
|
||
<button type="submit" class="button-link button-link-delete" onclick="return confirm('<?php echo esc_js(__('Withdraw this student from the class? The seat is freed and any pending payment is voided.', 'unsupervised-schedular')); ?>');">
|
||
<?php esc_html_e('Withdraw', 'unsupervised-schedular'); ?>
|
||
</button>
|
||
</form>
|
||
<?php endif; ?>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
|
||
<h2><?php esc_html_e('Policy acceptances', 'unsupervised-schedular'); ?></h2>
|
||
<?php if (empty($acceptances)) : ?>
|
||
<p><?php esc_html_e('None.', 'unsupervised-schedular'); ?></p>
|
||
<?php else : ?>
|
||
<table class="wp-list-table widefat fixed striped">
|
||
<thead>
|
||
<tr>
|
||
<th><?php esc_html_e('Policy', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Version', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Context', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Accepted', 'unsupervised-schedular'); ?></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($acceptances as $acceptance) : ?>
|
||
<tr>
|
||
<td><?php echo esc_html($acceptance['policy']); ?></td>
|
||
<td><?php echo esc_html($acceptance['version']); ?></td>
|
||
<td><?php echo esc_html($acceptance['context']); ?></td>
|
||
<td><?php echo esc_html($acceptance['accepted_at'] !== '' ? (string) mysql2date('M j, Y g:i A', $acceptance['accepted_at']) : '—'); ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
|
||
<h2><?php esc_html_e('Intake answers', 'unsupervised-schedular'); ?></h2>
|
||
<?php if (empty($intake)) : ?>
|
||
<p><?php esc_html_e('None.', 'unsupervised-schedular'); ?></p>
|
||
<?php else : ?>
|
||
<table class="wp-list-table widefat fixed striped">
|
||
<thead>
|
||
<tr>
|
||
<th><?php esc_html_e('Question', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Answer', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Context', 'unsupervised-schedular'); ?></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($intake as $row) : ?>
|
||
<tr>
|
||
<td><?php echo esc_html($row['question']); ?></td>
|
||
<td><?php echo esc_html($row['answer']); ?></td>
|
||
<td><?php echo esc_html($row['context']); ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($canBilling) : ?>
|
||
<h2><?php esc_html_e('Account credit', 'unsupervised-schedular'); ?></h2>
|
||
<p>
|
||
<?php
|
||
printf(
|
||
/* translators: %s: available credit balance, e.g. "45.00 CAD" */
|
||
esc_html__('Available balance: %s', 'unsupervised-schedular'),
|
||
'<strong>' . esc_html(number_format_i18n($creditBalance, 2) . ' ' . $creditCurrency) . '</strong>'
|
||
);
|
||
?>
|
||
<?php if ($payer['id'] !== (int) $student->ID) : ?>
|
||
<span class="description">
|
||
<?php
|
||
printf(
|
||
/* translators: %s: name of the parent/guardian whose account holds the balance. */
|
||
esc_html__('Held on %s’s account — the profile shares one balance.', 'unsupervised-schedular'),
|
||
esc_html($payer['name'])
|
||
);
|
||
?>
|
||
</span>
|
||
<?php endif; ?>
|
||
<span class="description"><?php esc_html_e('Credit from cancelled paid lessons is applied automatically to upcoming scheduled billing.', 'unsupervised-schedular'); ?></span>
|
||
</p>
|
||
<?php if (! empty($credits)) : ?>
|
||
<table class="wp-list-table widefat fixed striped">
|
||
<thead>
|
||
<tr>
|
||
<th><?php esc_html_e('Date', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Reason', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Amount', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Remaining', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Status', 'unsupervised-schedular'); ?></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($credits as $credit) : ?>
|
||
<tr>
|
||
<td><?php echo esc_html($credit['created_at'] !== '' ? (string) mysql2date('M j, Y g:i A', $credit['created_at']) : '—'); ?></td>
|
||
<td><?php echo esc_html($credit['reason']); ?></td>
|
||
<td><?php echo esc_html(number_format_i18n($credit['amount'], 2) . ' ' . $credit['currency']); ?></td>
|
||
<td><?php echo esc_html(number_format_i18n($credit['remaining'], 2) . ' ' . $credit['currency']); ?></td>
|
||
<td><?php echo esc_html($credit['status']); ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
|
||
<h2><?php esc_html_e('Payment history', 'unsupervised-schedular'); ?></h2>
|
||
<?php if (empty($payments)) : ?>
|
||
<p><?php esc_html_e('None.', 'unsupervised-schedular'); ?></p>
|
||
<?php else : ?>
|
||
<table class="wp-list-table widefat fixed striped">
|
||
<thead>
|
||
<tr>
|
||
<th><?php esc_html_e('Date', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Context', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Method', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Status', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Subtotal', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('HST', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Total', 'unsupervised-schedular'); ?></th>
|
||
<th><?php esc_html_e('Receipt', 'unsupervised-schedular'); ?></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($payments as $payment) : ?>
|
||
<tr>
|
||
<td><?php echo esc_html($payment['created_at'] !== '' ? (string) mysql2date('M j, Y g:i A', $payment['created_at']) : '—'); ?></td>
|
||
<td><?php echo esc_html($payment['context']); ?></td>
|
||
<td><?php echo esc_html($payment['method']); ?></td>
|
||
<td><?php echo esc_html($payment['status']); ?></td>
|
||
<td><?php echo esc_html(number_format_i18n($payment['amount'], 2)); ?></td>
|
||
<td><?php echo esc_html(number_format_i18n($payment['tax_amount'], 2)); ?></td>
|
||
<td><?php echo esc_html(number_format_i18n($payment['total'], 2) . ' ' . $payment['currency']); ?></td>
|
||
<td><?php echo esc_html($payment['receipt']); ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
<?php endif; ?>
|
||
</div>
|