CI / Tests (PHP 8.2) (pull_request) Successful in 38s
CI / Tests (PHP 8.1) (pull_request) Successful in 48s
CI / No Debug Code (pull_request) Successful in 3s
CI / Coding Standards (pull_request) Successful in 2m49s
CI / PHPStan (pull_request) Successful in 2m53s
CI / Tests (PHP 8.3) (pull_request) Successful in 2m38s
CI / Build Plugin Zip (pull_request) Skipped
Follow-up demo feedback: the My Availability page now opens in its weekly calendar (usc_view=list opts back into the table, which keeps the bulk-delete form), matching the new lessons defaults. Co-Authored-By: Claude Fable 5 <[email protected]>
116 lines
4.5 KiB
PHP
116 lines
4.5 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Unsupervised\Schedular\Availability;
|
|
|
|
use Unsupervised\Schedular\Auth\RoleManager;
|
|
use Unsupervised\Schedular\Offering\Offering;
|
|
use Unsupervised\Schedular\Offering\OfferingRepository;
|
|
use Unsupervised\Schedular\Val;
|
|
|
|
class AvailabilityController {
|
|
|
|
public function __construct(
|
|
private AvailabilityRepository $repository,
|
|
private OfferingRepository $offerings,
|
|
) {}
|
|
|
|
public function renderPage(): void {
|
|
if ( ! current_user_can( RoleManager::CAP_MANAGE_AVAILABILITY ) ) {
|
|
wp_die( esc_html__( 'You do not have permission to manage availability.', 'unsupervised-schedular' ) );
|
|
}
|
|
|
|
$instructorId = get_current_user_id();
|
|
|
|
if ( isset( $_POST['usc_action'] ) && check_admin_referer( 'usc_availability_action' ) ) {
|
|
$this->handleFormAction( $instructorId );
|
|
}
|
|
|
|
$slots = $this->repository->findByInstructor( $instructorId );
|
|
$offeringChoices = $this->offerings->findAll( $instructorId, Offering::KIND_PRIVATE_LESSON, true );
|
|
|
|
// View-state query params only (which view, which week) — nothing is
|
|
// mutated from them, so no nonce applies.
|
|
// phpcs:disable WordPress.Security.NonceVerification.Recommended
|
|
$view = 'list' === sanitize_key( Val::string( wp_unslash( $_GET['usc_view'] ?? '' ) ) ) ? 'list' : 'week';
|
|
$requestedWeek = sanitize_text_field( Val::string( wp_unslash( $_GET['usc_week'] ?? '' ) ) );
|
|
// phpcs:enable WordPress.Security.NonceVerification.Recommended
|
|
|
|
$weekStart = WeekCalendar::weekStart( $requestedWeek, Val::int( get_option( 'start_of_week', 1 ) ), current_time( 'Y-m-d' ) );
|
|
$weekDays = WeekCalendar::days( $weekStart, $slots );
|
|
$prevWeek = ( new \DateTimeImmutable( $weekStart ) )->modify( '-7 days' )->format( 'Y-m-d' );
|
|
$nextWeek = ( new \DateTimeImmutable( $weekStart ) )->modify( '+7 days' )->format( 'Y-m-d' );
|
|
|
|
include USC_PLUGIN_DIR . 'templates/admin/availability.php';
|
|
}
|
|
|
|
private function handleFormAction( int $instructorId ): void {
|
|
// Nonce is verified by the caller (renderPage) before this method runs.
|
|
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
|
$action = sanitize_key( Val::string( wp_unslash( $_POST['usc_action'] ?? '' ) ) );
|
|
|
|
if ( 'add' === $action ) {
|
|
$this->addSlot( $instructorId );
|
|
}
|
|
|
|
if ( 'delete' === $action ) {
|
|
$this->deleteOwnSlot( absint( Val::int( $_POST['slot_id'] ?? 0 ) ), $instructorId );
|
|
}
|
|
|
|
if ( 'bulk_delete' === $action ) {
|
|
// The array itself carries no data; each element is coerced and
|
|
// absint-sanitized individually below.
|
|
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
|
|
$rawIds = $_POST['slot_ids'] ?? [];
|
|
foreach ( is_array( $rawIds ) ? $rawIds : [] as $rawId ) {
|
|
$this->deleteOwnSlot( absint( Val::int( $rawId ) ), $instructorId );
|
|
}
|
|
}
|
|
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
|
}
|
|
|
|
/**
|
|
* Delete a slot only when it exists and belongs to the given instructor.
|
|
* The repository additionally refuses to delete booked slots.
|
|
*/
|
|
private function deleteOwnSlot( int $slotId, int $instructorId ): void {
|
|
if ( $slotId <= 0 ) {
|
|
return;
|
|
}
|
|
|
|
$slot = $this->repository->findById( $slotId );
|
|
if ( $slot && $slot->instructorId === $instructorId ) {
|
|
$this->repository->delete( $slotId );
|
|
}
|
|
}
|
|
|
|
private function addSlot( int $instructorId ): void {
|
|
// phpcs:disable WordPress.Security.NonceVerification.Missing
|
|
$startDt = AvailabilitySlot::normalizeDateTime( sanitize_text_field( Val::string( wp_unslash( $_POST['start_dt'] ?? '' ) ) ) );
|
|
$endDt = AvailabilitySlot::normalizeDateTime( sanitize_text_field( Val::string( wp_unslash( $_POST['end_dt'] ?? '' ) ) ) );
|
|
|
|
// A window must start and end on the same day (weekly repeat covers longer
|
|
// ranges) and fit at least one lesson; it is stored as lesson-length slots.
|
|
if ( null === $startDt || null === $endDt || $endDt <= $startDt || substr( $startDt, 0, 10 ) !== substr( $endDt, 0, 10 ) ) {
|
|
return;
|
|
}
|
|
|
|
$offeringId = absint( Val::int( $_POST['offering_id'] ?? 0 ) );
|
|
$duration = absint( Val::int( $_POST['duration_minutes'] ?? 0 ) );
|
|
|
|
$window = new AvailabilitySlot(
|
|
instructorId: $instructorId,
|
|
startDt: $startDt,
|
|
endDt: $endDt,
|
|
durationMinutes: $duration > 0 ? $duration : 60,
|
|
offeringId: $offeringId > 0 ? $offeringId : null,
|
|
);
|
|
|
|
$recurrence = sanitize_key( Val::string( wp_unslash( $_POST['recurrence'] ?? 'single' ) ) );
|
|
$weeks = absint( Val::int( $_POST['weeks'] ?? 1 ) );
|
|
|
|
$this->repository->createFromWindow( $window, 'weekly' === $recurrence, $weeks );
|
|
// phpcs:enable WordPress.Security.NonceVerification.Missing
|
|
}
|
|
}
|