Signup and the profile page now ask for a four-digit year between 1900 and the current year. Anything else — a short year, a full date, a year in the future — is discarded rather than stored, so a typo cannot leave a nonsense age on the record. The year lives in a new us_birth_year user meta rather than reusing us_date_of_birth, which would have left one key holding two formats. The old key is not migrated in bulk. Instead GuardianService handles it in two halves: birthYear() falls back to the year of the old date when the new key is absent, so a student added before this change still shows one, and setBirthYear() deletes the old date on every save. That deletion is what makes the fallback safe rather than merely tidy. Without it, clearing the birth year on a student who predates the change would leave the old date behind for the fallback to read straight back, and the year could never be cleared at all. Stored in user meta, so no Schema.php change and no USC_VERSION bump. Closes #147 Co-Authored-By: Claude Opus 5 <[email protected]>
364 lines
19 KiB
PHP
364 lines
19 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
if (! defined('ABSPATH')) {
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* @var \WP_User $student
|
||
* @var list<array{id: int, start_dt: string, end_dt: string, offering: string, instructor: string, status: string}> $upcoming
|
||
* @var list<array{id: int, 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) : ?>
|
||
<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']); ?></td>
|
||
<td><?php echo esc_html($row['instructor']); ?></td>
|
||
<td><?php echo esc_html($row['status']); ?></td>
|
||
<?php if ($withActions) : ?>
|
||
<td>
|
||
<?php if ($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>
|