availabilityController = new AvailabilityController( $availability, $offerings, new WindowValidator( $offerings ) ); $this->lessonController = new LessonController( $bookings, $payments, $availability, $offerings, new LessonDetail( $answers, $questions, $acceptances, $policies, $policyVersions ) ); $this->offeringController = new OfferingController( $offerings, new ClassSlotReconciler( $availability ) ); $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 ); $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->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 ) { return; } wp_enqueue_script( 'us-scheduler-availability-admin', USC_PLUGIN_URL . 'assets/js/availability-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 ); // 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; } }