CI / Coding Standards (pull_request) Successful in 29s
CI / Tests (PHP 8.2) (pull_request) Successful in 31s
CI / Tests (PHP 8.3) (pull_request) Successful in 31s
CI / Tests (PHP 8.5) (pull_request) Successful in 31s
CI / No Debug Code (pull_request) Successful in 9s
CI / Tests (PHP 8.1) (pull_request) Successful in 44s
CI / Static Analysis (pull_request) Successful in 51s
CI / Build Plugin Zip (pull_request) Skipped
The "Payment due" notice the daily billing scan sends was fixed wording
baked into PaymentDueMailer. Studio admins can now view, edit and preview
its subject and body under Studio Settings -> Payment Due Email.
- PaymentDueEmailTemplate stores subject/body/item-line as us_payment_due_email_*
options, each falling back to the built-in default when blank, and renders
via {token} substitution.
- PaymentDueMailer now builds a token map from the same consolidated
items/credit/e-transfer/reference data and renders the stored template; the
default template reproduces the previous wording exactly.
- PaymentEmailController serves the view/edit/save/reset admin page
(manage_billing, nonce-checked) with a server-rendered sample preview.
- PaymentEmailPreviewEndpoint (POST us-scheduler/v1/payment-email/preview,
manage_billing) + payment-email-admin.js drive a live preview of unsaved edits.
- Submenu wired under Studio Settings; endpoint wired in RestRegistrar.
- Bump to 1.6.0 and add CHANGELOG entry. New options default gracefully, so
existing sites need no migration and send the identical notice until edited.
Co-authored-by: anthropic/claude-opus-4-8
398 lines
16 KiB
PHP
398 lines
16 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Unsupervised\Schedular;
|
|
|
|
use Unsupervised\Schedular\Availability\AvailabilityController;
|
|
use Unsupervised\Schedular\Availability\AvailabilityRepository;
|
|
use Unsupervised\Schedular\Availability\WindowValidator;
|
|
use Unsupervised\Schedular\Auth\AccessSettings;
|
|
use Unsupervised\Schedular\Auth\InstructorController;
|
|
use Unsupervised\Schedular\Auth\InviteRepository;
|
|
use Unsupervised\Schedular\Auth\RegistrationApprovalController;
|
|
use Unsupervised\Schedular\Auth\RegistrationController;
|
|
use Unsupervised\Schedular\Auth\RegistrationMailer;
|
|
use Unsupervised\Schedular\Auth\RoleManager;
|
|
use Unsupervised\Schedular\Auth\StudentActions;
|
|
use Unsupervised\Schedular\Auth\StudentController;
|
|
use Unsupervised\Schedular\Guardian\GuardianService;
|
|
use Unsupervised\Schedular\Auth\StudentHistory;
|
|
use Unsupervised\Schedular\Booking\AdminBooking;
|
|
use Unsupervised\Schedular\Booking\BookingRepository;
|
|
use Unsupervised\Schedular\Booking\LessonBooker;
|
|
use Unsupervised\Schedular\Booking\LessonController;
|
|
use Unsupervised\Schedular\GroupClass\EnrollmentRepository;
|
|
use Unsupervised\Schedular\GroupClass\GroupAccessRepository;
|
|
use Unsupervised\Schedular\GroupClass\GroupClassController;
|
|
use Unsupervised\Schedular\GroupClass\SessionSchedule;
|
|
use Unsupervised\Schedular\Offering\BillingModeReconciler;
|
|
use Unsupervised\Schedular\Offering\ClassSlotReconciler;
|
|
use Unsupervised\Schedular\Offering\OfferingController;
|
|
use Unsupervised\Schedular\Offering\OfferingRepository;
|
|
use Unsupervised\Schedular\Payment\BillingMethodResolver;
|
|
use Unsupervised\Schedular\Payment\CreditRepository;
|
|
use Unsupervised\Schedular\Payment\PaymentController;
|
|
use Unsupervised\Schedular\Payment\PaymentEmailController;
|
|
use Unsupervised\Schedular\Payment\PaymentReportController;
|
|
use Unsupervised\Schedular\Payment\PaymentRepository;
|
|
use Unsupervised\Schedular\Payment\PaymentService;
|
|
use Unsupervised\Schedular\Payment\StudioSettings;
|
|
use Unsupervised\Schedular\Policy\AcceptanceRepository;
|
|
use Unsupervised\Schedular\Policy\PolicyController;
|
|
use Unsupervised\Schedular\Policy\PolicyRepository;
|
|
use Unsupervised\Schedular\Policy\PolicyService;
|
|
use Unsupervised\Schedular\Policy\PolicyVersionRepository;
|
|
use Unsupervised\Schedular\Registration\AnswerRepository;
|
|
use Unsupervised\Schedular\Registration\QuestionController;
|
|
use Unsupervised\Schedular\Registration\QuestionRepository;
|
|
use Unsupervised\Schedular\Registration\IntakeAudit;
|
|
use Unsupervised\Schedular\Registration\IntakeRecording;
|
|
use Unsupervised\Schedular\Registration\RegistrationGate;
|
|
|
|
class AdminMenu {
|
|
|
|
/**
|
|
* Hook suffix of the availability screen, captured when the page is added so
|
|
* its script loads on that screen only.
|
|
*/
|
|
private string $availabilityHook = '';
|
|
|
|
/**
|
|
* Hook suffix of the payment-email screen, captured when the page is added so
|
|
* its live-preview script loads on that screen only.
|
|
*/
|
|
private string $paymentEmailHook = '';
|
|
|
|
private AvailabilityController $availabilityController;
|
|
private LessonController $lessonController;
|
|
private OfferingController $offeringController;
|
|
private QuestionController $questionController;
|
|
private PolicyController $policyController;
|
|
private RegistrationController $registrationController;
|
|
private RegistrationApprovalController $registrationApprovalController;
|
|
private GroupClassController $groupClassController;
|
|
private StudentController $studentController;
|
|
private InstructorController $instructorController;
|
|
private StudioSettings $settings;
|
|
private AccessSettings $accessSettings;
|
|
private PaymentController $paymentController;
|
|
private PaymentEmailController $paymentEmailController;
|
|
private PaymentReportController $paymentReportController;
|
|
|
|
public function __construct( AvailabilityRepository $availability, BookingRepository $bookings, OfferingRepository $offerings, QuestionRepository $questions, AnswerRepository $answers, PolicyRepository $policies, PolicyVersionRepository $policyVersions, PolicyService $policyService, AcceptanceRepository $acceptances, InviteRepository $invites, EnrollmentRepository $enrollments, GroupAccessRepository $groupAccess, StudioSettings $settings, PaymentRepository $payments, PaymentService $paymentService, BillingMethodResolver $resolver, RegistrationMailer $registrationMailer, CreditRepository $credits, GuardianService $guardians, LessonBooker $booker, RegistrationGate $gate, BillingModeReconciler $billingModeReconciler ) {
|
|
// One audit presenter and one recorder, shared by the lesson and enrolment
|
|
// detail views: intake is the same thing whichever registration it hangs off.
|
|
$intakeAudit = new IntakeAudit( $answers, $questions, $acceptances, $policies, $policyVersions );
|
|
$intakeRecording = new IntakeRecording( $questions, $answers, $policies, $policyVersions, $acceptances, $gate );
|
|
|
|
$this->availabilityController = new AvailabilityController( $availability, $offerings, new WindowValidator( $offerings ) );
|
|
$this->lessonController = new LessonController( $bookings, $payments, $availability, $offerings, $intakeAudit, new AdminBooking( $availability, $offerings, $booker ), $intakeRecording );
|
|
$this->offeringController = new OfferingController( $offerings, new ClassSlotReconciler( $availability ), $billingModeReconciler );
|
|
$this->questionController = new QuestionController( $questions, $offerings );
|
|
$this->policyController = new PolicyController( $policies, $policyVersions, $policyService );
|
|
$this->registrationController = new RegistrationController( $invites );
|
|
$this->registrationApprovalController = new RegistrationApprovalController( $registrationMailer );
|
|
$this->groupClassController = new GroupClassController( $enrollments, $offerings, $payments, $groupAccess, $paymentService, $invites, $registrationMailer, $intakeAudit, $intakeRecording );
|
|
$this->studentController = new StudentController( $bookings, $availability, $offerings, $enrollments, $resolver, new StudentHistory( $acceptances, $policies, $policyVersions, $answers, $questions, $payments, $credits ), new StudentActions( $bookings, $availability, $enrollments, $paymentService ), $guardians, new SessionSchedule( $enrollments, $offerings ) );
|
|
$this->instructorController = new InstructorController();
|
|
$this->settings = $settings;
|
|
$this->accessSettings = new AccessSettings();
|
|
$this->paymentController = new PaymentController( $payments, $paymentService );
|
|
$this->paymentEmailController = new PaymentEmailController();
|
|
$this->paymentReportController = new PaymentReportController( $payments );
|
|
}
|
|
|
|
public function register(): void {
|
|
add_action( 'admin_menu', [ $this, 'addPages' ] );
|
|
add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAssets' ] );
|
|
add_action( 'admin_post_' . PaymentReportController::EXPORT_ACTION, [ $this->paymentReportController, 'export' ] );
|
|
}
|
|
|
|
/**
|
|
* Load a screen's script on that screen only.
|
|
*
|
|
* @param string $hookSuffix Screen the enqueue is running for.
|
|
*/
|
|
public function enqueueAssets( string $hookSuffix ): void {
|
|
if ( '' !== $this->availabilityHook && $hookSuffix === $this->availabilityHook ) {
|
|
wp_enqueue_script(
|
|
'us-scheduler-availability-admin',
|
|
USC_PLUGIN_URL . 'assets/js/availability-admin.js',
|
|
[],
|
|
USC_VERSION,
|
|
true
|
|
);
|
|
return;
|
|
}
|
|
|
|
if ( '' !== $this->paymentEmailHook && $hookSuffix === $this->paymentEmailHook ) {
|
|
wp_enqueue_script(
|
|
'us-scheduler-payment-email-admin',
|
|
USC_PLUGIN_URL . 'assets/js/payment-email-admin.js',
|
|
[],
|
|
USC_VERSION,
|
|
true
|
|
);
|
|
}
|
|
}
|
|
|
|
public function addPages(): void {
|
|
$this->addStudioSeparators();
|
|
|
|
// Studio-wide dashboard: all upcoming lessons across instructors.
|
|
add_menu_page(
|
|
__( 'Scheduler', 'unsupervised-schedular' ),
|
|
__( 'Scheduler', 'unsupervised-schedular' ),
|
|
RoleManager::CAP_VIEW_ALL_LESSONS,
|
|
'us-scheduler',
|
|
[ $this->lessonController, 'renderAdminDashboard' ],
|
|
'dashicons-calendar-alt',
|
|
40
|
|
);
|
|
|
|
// Instructor: manage their own availability.
|
|
$this->availabilityHook = (string) add_menu_page(
|
|
__( 'My Availability', 'unsupervised-schedular' ),
|
|
__( 'My Availability', 'unsupervised-schedular' ),
|
|
RoleManager::CAP_MANAGE_AVAILABILITY,
|
|
'us-availability',
|
|
[ $this->availabilityController, 'renderPage' ],
|
|
'dashicons-clock',
|
|
41
|
|
);
|
|
|
|
// Studio admin / instructor: manage offerings.
|
|
add_menu_page(
|
|
__( 'Offerings', 'unsupervised-schedular' ),
|
|
__( 'Offerings', 'unsupervised-schedular' ),
|
|
RoleManager::CAP_MANAGE_OFFERINGS,
|
|
'us-offerings',
|
|
[ $this->offeringController, 'renderPage' ],
|
|
'dashicons-tag',
|
|
33
|
|
);
|
|
|
|
// Studio admin / instructor: manage per-offering intake questions.
|
|
add_submenu_page(
|
|
'us-offerings',
|
|
__( 'Questions', 'unsupervised-schedular' ),
|
|
__( 'Questions', 'unsupervised-schedular' ),
|
|
RoleManager::CAP_MANAGE_QUESTIONS,
|
|
'us-questions',
|
|
[ $this->questionController, 'renderPage' ]
|
|
);
|
|
|
|
// Studio admin: draft, version, and publish policies.
|
|
add_menu_page(
|
|
__( 'Policies', 'unsupervised-schedular' ),
|
|
__( 'Policies', 'unsupervised-schedular' ),
|
|
RoleManager::CAP_MANAGE_POLICIES,
|
|
'us-policies',
|
|
[ $this->policyController, 'renderPage' ],
|
|
'dashicons-text-page',
|
|
31
|
|
);
|
|
|
|
// Studio admin: invite students to register.
|
|
add_menu_page(
|
|
__( 'Invites', 'unsupervised-schedular' ),
|
|
__( 'Invites', 'unsupervised-schedular' ),
|
|
RoleManager::CAP_MANAGE_STUDENTS,
|
|
'us-invites',
|
|
[ $this->registrationController, 'renderPage' ],
|
|
'dashicons-email',
|
|
32
|
|
);
|
|
|
|
// Studio admin: all group-class enrolments.
|
|
add_menu_page(
|
|
__( 'Group Classes', 'unsupervised-schedular' ),
|
|
__( 'Group Classes', 'unsupervised-schedular' ),
|
|
RoleManager::CAP_VIEW_ALL_LESSONS,
|
|
'us-group-classes',
|
|
[ $this->groupClassController, 'renderPage' ],
|
|
'dashicons-groups',
|
|
36
|
|
);
|
|
|
|
// Studio admin: create instructors and manage their capabilities.
|
|
add_menu_page(
|
|
__( 'Instructors', 'unsupervised-schedular' ),
|
|
__( 'Instructors', 'unsupervised-schedular' ),
|
|
RoleManager::CAP_MANAGE_INSTRUCTORS,
|
|
InstructorController::PAGE_SLUG,
|
|
[ $this->instructorController, 'renderPage' ],
|
|
'dashicons-businessperson',
|
|
34.5
|
|
);
|
|
|
|
// Studio admin: browse students and their activity.
|
|
add_menu_page(
|
|
__( 'Students', 'unsupervised-schedular' ),
|
|
__( 'Students', 'unsupervised-schedular' ),
|
|
RoleManager::CAP_MANAGE_STUDENTS,
|
|
'us-students',
|
|
[ $this->studentController, 'renderPage' ],
|
|
'dashicons-id',
|
|
35
|
|
);
|
|
|
|
// Studio admin: approve or reject self-signup students (open registration).
|
|
add_submenu_page(
|
|
'us-students',
|
|
__( 'Pending Students', 'unsupervised-schedular' ),
|
|
__( 'Pending Students', 'unsupervised-schedular' ),
|
|
RoleManager::CAP_MANAGE_STUDENTS,
|
|
RegistrationApprovalController::PAGE_SLUG,
|
|
[ $this->registrationApprovalController, 'renderPage' ]
|
|
);
|
|
|
|
// Studio admin: confirm pending (e-transfer) payments.
|
|
add_menu_page(
|
|
__( 'Payments', 'unsupervised-schedular' ),
|
|
__( 'Payments', 'unsupervised-schedular' ),
|
|
RoleManager::CAP_MANAGE_BILLING,
|
|
'us-payments',
|
|
[ $this->paymentController, 'renderPage' ],
|
|
'dashicons-money-alt',
|
|
37
|
|
);
|
|
|
|
// Studio admin (all) / instructor (own): monthly payments report with HST.
|
|
// Gated on export so instructors — who lack manage_billing — can still see it.
|
|
add_menu_page(
|
|
__( 'Payment Report', 'unsupervised-schedular' ),
|
|
__( 'Payment Report', 'unsupervised-schedular' ),
|
|
RoleManager::CAP_EXPORT_PAYMENTS,
|
|
'us-reports',
|
|
[ $this->paymentReportController, 'renderPage' ],
|
|
'dashicons-chart-bar',
|
|
38
|
|
);
|
|
|
|
// Studio admin: Stripe credentials and billing settings.
|
|
add_menu_page(
|
|
__( 'Studio Settings', 'unsupervised-schedular' ),
|
|
__( 'Studio Settings', 'unsupervised-schedular' ),
|
|
RoleManager::CAP_MANAGE_BILLING,
|
|
'us-settings',
|
|
[ $this->settings, 'renderPage' ],
|
|
'dashicons-admin-settings',
|
|
30
|
|
);
|
|
|
|
// Studio admin: view, edit and preview the payment-due email template.
|
|
$this->paymentEmailHook = (string) add_submenu_page(
|
|
'us-settings',
|
|
__( 'Payment Due Email', 'unsupervised-schedular' ),
|
|
__( 'Payment Due Email', 'unsupervised-schedular' ),
|
|
RoleManager::CAP_MANAGE_BILLING,
|
|
'us-payment-email',
|
|
[ $this->paymentEmailController, 'renderPage' ]
|
|
);
|
|
|
|
// Site owner: whether WordPress administrators are studio admins / instructors.
|
|
// Gated on the core manage_options capability — never the plugin's own grants —
|
|
// so an administrator can always reach it to re-enable a disabled grant.
|
|
add_menu_page(
|
|
__( 'Access', 'unsupervised-schedular' ),
|
|
__( 'Access', 'unsupervised-schedular' ),
|
|
'manage_options',
|
|
'us-access',
|
|
[ $this->accessSettings, 'renderPage' ],
|
|
'dashicons-admin-network',
|
|
30.5
|
|
);
|
|
|
|
// Instructor: view their upcoming lessons. Hidden for anyone who can
|
|
// already see the Scheduler — it shows every instructor's lessons
|
|
// (including their own, with the same payment edit forms), so the two
|
|
// menu items would just duplicate each other for an owner-operator.
|
|
if ( ! current_user_can( RoleManager::CAP_VIEW_ALL_LESSONS ) ) {
|
|
add_menu_page(
|
|
__( 'My Lessons', 'unsupervised-schedular' ),
|
|
__( 'My Lessons', 'unsupervised-schedular' ),
|
|
RoleManager::CAP_VIEW_LESSONS,
|
|
'us-my-lessons',
|
|
[ $this->lessonController, 'renderInstructorLessons' ],
|
|
'dashicons-welcome-learn-more',
|
|
42
|
|
);
|
|
|
|
// Instructor: their own group classes with per-class rosters.
|
|
add_submenu_page(
|
|
'us-my-lessons',
|
|
__( 'My Group Classes', 'unsupervised-schedular' ),
|
|
__( 'My Group Classes', 'unsupervised-schedular' ),
|
|
RoleManager::CAP_VIEW_LESSONS,
|
|
'us-my-group-classes',
|
|
[ $this->groupClassController, 'renderInstructorPage' ]
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Insert sidebar separators around the studio menus so they sit visually
|
|
* apart from the core WordPress items and split into three sections —
|
|
* mirroring the dividers core uses. Each separator is only added when the user
|
|
* can see a menu in the following section, to avoid orphaned dividers.
|
|
*
|
|
* Layout: [29] · setup (Settings/Policies/Invites/Offerings) · [34] ·
|
|
* people & money (Students/Group Classes/Payments/Payment Report) · [39] ·
|
|
* operations (Scheduler) and instructor menus.
|
|
*/
|
|
private function addStudioSeparators(): void {
|
|
$this->addSeparatorAt( 29, $this->userSeesStudioMenu() );
|
|
$this->addSeparatorAt( 34, $this->userSeesPeopleSection() );
|
|
$this->addSeparatorAt( 39, current_user_can( RoleManager::CAP_VIEW_ALL_LESSONS ) );
|
|
}
|
|
|
|
private function addSeparatorAt( int $position, bool $visible ): void {
|
|
if ( ! $visible ) {
|
|
return;
|
|
}
|
|
|
|
global $menu;
|
|
if ( ! is_array( $menu ) || isset( $menu[ $position ] ) ) {
|
|
return;
|
|
}
|
|
|
|
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- writing to $menu is the supported way to add an admin sidebar separator.
|
|
$menu[ $position ] = [ '', 'read', 'us-studio-separator-' . $position, '', 'wp-menu-separator' ];
|
|
}
|
|
|
|
private function userSeesPeopleSection(): bool {
|
|
return current_user_can( RoleManager::CAP_MANAGE_INSTRUCTORS )
|
|
|| current_user_can( RoleManager::CAP_MANAGE_STUDENTS )
|
|
|| current_user_can( RoleManager::CAP_VIEW_ALL_LESSONS )
|
|
|| current_user_can( RoleManager::CAP_MANAGE_BILLING );
|
|
}
|
|
|
|
private function userSeesStudioMenu(): bool {
|
|
// Administrators always see the Access page, so the leading separator
|
|
// should show for them even if both capability grants are disabled.
|
|
if ( current_user_can( 'manage_options' ) ) {
|
|
return true;
|
|
}
|
|
|
|
$caps = [
|
|
RoleManager::CAP_VIEW_ALL_LESSONS,
|
|
RoleManager::CAP_VIEW_LESSONS,
|
|
RoleManager::CAP_MANAGE_AVAILABILITY,
|
|
RoleManager::CAP_MANAGE_OFFERINGS,
|
|
RoleManager::CAP_MANAGE_QUESTIONS,
|
|
RoleManager::CAP_MANAGE_POLICIES,
|
|
RoleManager::CAP_MANAGE_STUDENTS,
|
|
RoleManager::CAP_MANAGE_BILLING,
|
|
];
|
|
|
|
foreach ( $caps as $cap ) {
|
|
if ( current_user_can( $cap ) ) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|