Files
unsupervised-scheduler/templates/admin/student-detail.php
T
thatguygriffandClaude Opus 5 122f7a0f53
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
Show "Booked by" in the Account section of a student's detail page
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]>
2026-07-30 12:18:42 -03:00

400 lines
22 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);
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 %ss 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>