Files
unsupervised-scheduler/templates/admin/offerings.php
T
thatguygriffandClaude Opus 4.8 8b90b8d78d
CI / Tests (PHP 8.1) (pull_request) Successful in 41s
CI / Tests (PHP 8.2) (pull_request) Successful in 53s
CI / No Debug Code (pull_request) Successful in 2s
CI / PHPStan (pull_request) Successful in 2m51s
CI / Coding Standards (pull_request) Successful in 2m54s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m38s
CI / Build Plugin Zip (pull_request) Skipped
Add cancellation cutoff limiting how close to a lesson a student can cancel
Students can no longer cancel their own lesson online once it starts within a
configured window; instructors and studio admins can always cancel.

- Studio default `us_cancellation_cutoff_hours` (stored/computed in hours,
  entered and displayed in days under Studio Settings → Cancellations).
- Optional per-offering override `cancellation_cutoff_hours` (entered in hours);
  blank inherits the studio default, 0 allows anytime cancellation.
- `Booking\CancellationPolicy` resolves the effective window and decides;
  `BookingEndpoint::cancel()` returns a 403 `cancellation_closed` when too late.
  The instructor status endpoint and studio-admin student actions bypass it.

Closes #93

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-23 11:57:18 -03:00

179 lines
12 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);
use Unsupervised\Schedular\Offering\Offering;
if (! defined('ABSPATH')) {
exit;
}
/**
* @var list<\Unsupervised\Schedular\Offering\Offering> $offerings
* @var \Unsupervised\Schedular\Offering\Offering|null $editing Offering loaded into the form, or null when adding.
*/
$baseUrl = admin_url('admin.php?page=us-offerings');
// Prefill the sessions control from the stored term dates: a term longer than
// one day was created as weekly sessions one week apart.
$termRecurrence = 'single';
$termSessions = 10;
if ($editing && null !== $editing->termStart && null !== $editing->termEnd && $editing->termEnd !== $editing->termStart) {
$termRecurrence = 'weekly';
$termSessions = (int) round(((int) strtotime($editing->termEnd) - (int) strtotime($editing->termStart)) / 604800) + 1;
}
?>
<div class="wrap">
<h1><?php esc_html_e('Offerings', 'unsupervised-schedular'); ?></h1>
<h2><?php $editing ? esc_html_e('Edit Offering', 'unsupervised-schedular') : esc_html_e('Add Offering', 'unsupervised-schedular'); ?></h2>
<form method="post">
<?php wp_nonce_field('usc_offering_action'); ?>
<?php if ($editing) : ?>
<input type="hidden" name="usc_action" value="update">
<input type="hidden" name="offering_id" value="<?php echo esc_attr((string) $editing->id); ?>">
<?php else : ?>
<input type="hidden" name="usc_action" value="add">
<?php endif; ?>
<table class="form-table">
<tr>
<th><label for="title"><?php esc_html_e('Title', 'unsupervised-schedular'); ?></label></th>
<td><input type="text" name="title" id="title" class="regular-text" required value="<?php echo esc_attr($editing->title ?? ''); ?>"></td>
</tr>
<tr>
<th><label for="kind"><?php esc_html_e('Kind', 'unsupervised-schedular'); ?></label></th>
<td>
<select name="kind" id="kind">
<option value="<?php echo esc_attr(Offering::KIND_PRIVATE_LESSON); ?>"><?php esc_html_e('Private lesson', 'unsupervised-schedular'); ?></option>
<option value="<?php echo esc_attr(Offering::KIND_GROUP_CLASS); ?>" <?php echo $editing && Offering::KIND_GROUP_CLASS === $editing->kind ? 'selected' : ''; ?>><?php esc_html_e('Group class', 'unsupervised-schedular'); ?></option>
</select>
</td>
</tr>
<tr>
<th><label for="description"><?php esc_html_e('Description', 'unsupervised-schedular'); ?></label></th>
<td><textarea name="description" id="description" class="large-text" rows="4"><?php echo esc_textarea($editing->description ?? ''); ?></textarea></td>
</tr>
<tr>
<th><label for="duration_minutes"><?php esc_html_e('Duration (minutes)', 'unsupervised-schedular'); ?></label></th>
<td><input type="number" name="duration_minutes" id="duration_minutes" min="0" step="1" value="<?php echo esc_attr((string) ($editing->durationMinutes ?? '')); ?>"></td>
</tr>
<tr>
<th><label for="price"><?php esc_html_e('Price (dollars)', 'unsupervised-schedular'); ?></label></th>
<td><input type="number" name="price" id="price" min="0" step="0.01" value="<?php echo esc_attr(number_format($editing->price ?? 0.0, 2, '.', '')); ?>"></td>
</tr>
<tr>
<th><label for="billing_mode"><?php esc_html_e('Billing', 'unsupervised-schedular'); ?></label></th>
<td>
<select name="billing_mode" id="billing_mode">
<option value="<?php echo esc_attr(Offering::BILLING_ONE_TIME); ?>"><?php esc_html_e('One-time at booking', 'unsupervised-schedular'); ?></option>
<option value="<?php echo esc_attr(Offering::BILLING_FULL_TERM); ?>" <?php echo $editing && Offering::BILLING_FULL_TERM === $editing->billingMode ? 'selected' : ''; ?>><?php esc_html_e('Full term upfront', 'unsupervised-schedular'); ?></option>
</select>
</td>
</tr>
<tr>
<th><?php esc_html_e('Weekly reservation', 'unsupervised-schedular'); ?></th>
<td><label><input type="checkbox" name="allow_weekly" value="1" <?php echo $editing && $editing->allowWeekly ? 'checked' : ''; ?>> <?php esc_html_e('Allow weekly recurring reservation (private)', 'unsupervised-schedular'); ?></label></td>
</tr>
<tr>
<th><label for="capacity"><?php esc_html_e('Capacity', 'unsupervised-schedular'); ?></label></th>
<td><input type="number" name="capacity" id="capacity" min="0" step="1" value="<?php echo esc_attr((string) ($editing->capacity ?? '')); ?>"> <span class="description"><?php esc_html_e('Group classes only', 'unsupervised-schedular'); ?></span></td>
</tr>
<tr>
<th><label for="term_start"><?php esc_html_e('Start date', 'unsupervised-schedular'); ?></label></th>
<td>
<input type="date" name="term_start" id="term_start" value="<?php echo esc_attr($editing->termStart ?? ''); ?>">
<span class="description"><?php esc_html_e('Group classes only — date of the first class', 'unsupervised-schedular'); ?></span>
</td>
</tr>
<tr>
<th><?php esc_html_e('Sessions', 'unsupervised-schedular'); ?></th>
<td>
<label><input type="radio" name="term_recurrence" value="single" <?php echo 'single' === $termRecurrence ? 'checked' : ''; ?>> <?php esc_html_e('One-off', 'unsupervised-schedular'); ?></label>
&nbsp;
<label><input type="radio" name="term_recurrence" value="weekly" <?php echo 'weekly' === $termRecurrence ? 'checked' : ''; ?>> <?php esc_html_e('Weekly for', 'unsupervised-schedular'); ?></label>
<input type="number" name="term_sessions" min="1" max="52" value="<?php echo esc_attr((string) $termSessions); ?>" style="width:5em;"> <?php esc_html_e('sessions', 'unsupervised-schedular'); ?>
<p class="description"><?php esc_html_e('The end date is calculated from the start date and the number of weekly sessions.', 'unsupervised-schedular'); ?></p>
</td>
</tr>
<tr>
<th><label for="schedule_note"><?php esc_html_e('Schedule note', 'unsupervised-schedular'); ?></label></th>
<td><input type="text" name="schedule_note" id="schedule_note" class="regular-text" placeholder="<?php esc_attr_e('e.g. Tuesdays 4:00pm', 'unsupervised-schedular'); ?>" value="<?php echo esc_attr($editing->scheduleNote ?? ''); ?>"></td>
</tr>
<tr>
<th><label for="etransfer_email"><?php esc_html_e('E-transfer email', 'unsupervised-schedular'); ?></label></th>
<td><input type="email" name="etransfer_email" id="etransfer_email" class="regular-text" placeholder="<?php esc_attr_e('Overrides the studio default', 'unsupervised-schedular'); ?>" value="<?php echo esc_attr($editing->etransferEmail ?? ''); ?>"></td>
</tr>
<tr>
<th><label for="cancellation_cutoff_hours"><?php esc_html_e('Cancellation cutoff (hours)', 'unsupervised-schedular'); ?></label></th>
<td>
<input type="number" name="cancellation_cutoff_hours" id="cancellation_cutoff_hours" min="0" step="1" placeholder="<?php esc_attr_e('Uses the studio default', 'unsupervised-schedular'); ?>" value="<?php echo esc_attr(null === ($editing->cancellationCutoffHours ?? null) ? '' : (string) $editing->cancellationCutoffHours); ?>">
<p class="description"><?php esc_html_e('How many hours before a lesson a student may still cancel it. Leave blank to use the studio default; 0 lets students cancel any time.', 'unsupervised-schedular'); ?></p>
</td>
</tr>
<tr>
<th><?php esc_html_e('Active', 'unsupervised-schedular'); ?></th>
<td><label><input type="checkbox" name="is_active" value="1" <?php echo null === $editing || $editing->isActive ? 'checked' : ''; ?>> <?php esc_html_e('Open for registration', 'unsupervised-schedular'); ?></label></td>
</tr>
</table>
<?php submit_button($editing ? esc_html__('Update Offering', 'unsupervised-schedular') : esc_html__('Add Offering', 'unsupervised-schedular')); ?>
<?php if ($editing) : ?>
<p><a href="<?php echo esc_url($baseUrl); ?>"><?php esc_html_e('Cancel editing', 'unsupervised-schedular'); ?></a></p>
<?php endif; ?>
</form>
<h2><?php esc_html_e('Current Offerings', 'unsupervised-schedular'); ?></h2>
<?php if (empty($offerings)) : ?>
<p><?php esc_html_e('No offerings configured.', 'unsupervised-schedular'); ?></p>
<?php else : ?>
<table class="wp-list-table widefat fixed striped">
<thead>
<tr>
<th style="width:4em;"><?php esc_html_e('ID', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Title', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Kind', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Duration', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Price', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Billing', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Term', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Active', 'unsupervised-schedular'); ?></th>
<th><?php esc_html_e('Actions', 'unsupervised-schedular'); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($offerings as $offering) : ?>
<tr>
<td><?php echo esc_html((string) $offering->id); ?></td>
<td><?php echo esc_html($offering->title); ?></td>
<td><?php echo esc_html($offering->kind); ?></td>
<td><?php echo $offering->durationMinutes ? esc_html((string) $offering->durationMinutes . ' min') : '&mdash;'; ?></td>
<td><?php echo esc_html(number_format($offering->price, 2) . ' ' . $offering->currency); ?></td>
<td><?php echo esc_html($offering->billingMode); ?></td>
<td>
<?php if (null === $offering->termStart) : ?>
&mdash;
<?php elseif (null === $offering->termEnd || $offering->termEnd === $offering->termStart) : ?>
<?php echo esc_html((string) mysql2date('M j, Y', $offering->termStart)); ?>
<?php else : ?>
<?php echo esc_html((string) mysql2date('M j, Y', $offering->termStart) . ' ' . (string) mysql2date('M j, Y', $offering->termEnd)); ?>
<?php endif; ?>
</td>
<td><?php echo $offering->isActive ? esc_html__('Yes', 'unsupervised-schedular') : esc_html__('No', 'unsupervised-schedular'); ?></td>
<td>
<a class="button button-small" href="<?php echo esc_url(add_query_arg('usc_edit', (int) $offering->id, $baseUrl)); ?>"><?php esc_html_e('Edit', 'unsupervised-schedular'); ?></a>
<form method="post" style="display:inline;">
<?php wp_nonce_field('usc_offering_action'); ?>
<input type="hidden" name="usc_action" value="delete">
<input type="hidden" name="offering_id" value="<?php echo esc_attr((string) $offering->id); ?>">
<button type="submit" class="button button-small button-link-delete">
<?php esc_html_e('Delete', 'unsupervised-schedular'); ?>
</button>
</form>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>