CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m48s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m42s
CI / Tests (PHP 8.2) (pull_request) Successful in 57s
CI / Tests (PHP 8.1) (pull_request) Successful in 59s
CI / Coding Standards (pull_request) Successful in 3m1s
CI / Build Plugin Zip (pull_request) Skipped
The parent/guardian was only named further down under Profile, where it reads as background rather than as an account fact, and only when there was one — so a page with no such line was ambiguous between "books for themselves" and "the lookup found nothing". It now sits in the Account table beside display name and email, as the guardian's name linked to their own detail page, and always renders: a student who books for themselves says so outright. No email address — theirs is one click away on their own page, and repeating it here only makes the row harder to scan. The Profile section keeps only the note explaining the placeholder email, which is a different point. Tests: composer test (863), composer lint, composer cs all pass. Co-Authored-By: Claude Opus 5 <[email protected]>
400 lines
22 KiB
PHP
400 lines
22 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, schedule: string|null, start_dt: string, end_dt: string, offering: string, instructor: string, status: string}> $upcoming Booked lessons and upcoming group-class sessions, soonest first. `schedule` is wording to show instead of a date, for a class with no class time set.
|
||
* @var list<array{id: int, kind: string, schedule: string|null, 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
|
||
// A class with no class time set has no clock to put it on,
|
||
// so it carries wording ("Tuesdays 4:00pm", or its term
|
||
// dates) that stands in for the date entirely.
|
||
if (($row['schedule'] ?? null) !== null && $row['schedule'] !== '') {
|
||
echo esc_html((string) $row['schedule']);
|
||
} else {
|
||
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; ?>
|
||
|
||
<?php $detailUrl = static fn(int $id): string => add_query_arg(['page' => $pageSlug, 'student_id' => $id], admin_url('admin.php')); ?>
|
||
|
||
<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>
|
||
<?php
|
||
/*
|
||
* Always rendered, both ways round. "Books for themselves" is a fact
|
||
* worth stating outright: an empty row would be indistinguishable
|
||
* from a guardian the page failed to look up.
|
||
*/
|
||
?>
|
||
<tr>
|
||
<th><?php esc_html_e('Booked by', 'unsupervised-schedular'); ?></th>
|
||
<td>
|
||
<?php if ($guardian !== null) : ?>
|
||
<?php // The name alone. Their address is one click away on their own page, and repeating it here only makes the row harder to scan. ?>
|
||
<a href="<?php echo esc_url($detailUrl($guardian['id'])); ?>"><?php echo esc_html($guardian['name']); ?></a>
|
||
<?php else : ?>
|
||
<span class="description"><?php esc_html_e('Nobody — this student books and pays for themselves.', 'unsupervised-schedular'); ?></span>
|
||
<?php endif; ?>
|
||
</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 if ($guardian !== null) : ?>
|
||
<?php // Who they book through is up in Account; this explains the placeholder email. ?>
|
||
<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>
|