| |
| <?php
|
| <?php
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| if (!defined('ABSPATH')) {
|
| exit;
|
| }
|
|
|
| class GymCalendarCore {
|
|
|
| private $wpdb;
|
| private $table_classes;
|
| private $table_instances;
|
| private $table_registrations;
|
| private $table_settings;
|
| private $table_credits;
|
|
|
| public function __construct() {
|
| global $wpdb;
|
| $this->wpdb = $wpdb;
|
| $this->table_classes = $wpdb->prefix . 'gym_classes';
|
| $this->table_instances = $wpdb->prefix . 'gym_class_instances';
|
| $this->table_registrations = $wpdb->prefix . 'gym_class_registrations';
|
| $this->table_settings = $wpdb->prefix . 'gym_calendar_settings';
|
| $this->table_credits = $wpdb->prefix . 'gym_credits';
|
|
|
|
|
| $this->ensureHolidayInstances();
|
| }
|
|
|
|
|
|
|
|
|
| private function ensureHolidayInstances() {
|
| $current_year = date('Y');
|
| $next_year = $current_year + 1;
|
|
|
|
|
| $this->generateHolidayInstances($current_year);
|
| $this->generateHolidayInstances($next_year);
|
| }
|
|
|
|
|
|
|
|
|
| private function generateHolidayInstances($year) {
|
|
|
| $holiday_class_id = $this->wpdb->get_var(
|
| $this->wpdb->prepare(
|
| "SELECT id FROM {$this->table_classes} WHERE class_type = 'holiday' LIMIT 1"
|
| )
|
| );
|
|
|
| if (!$holiday_class_id) {
|
| return;
|
| }
|
|
|
| $holiday_dates = $this->getStatutoryHolidayDates($year);
|
|
|
| foreach ($holiday_dates as $date => $name) {
|
|
|
| $existing = $this->wpdb->get_var(
|
| $this->wpdb->prepare(
|
| "SELECT id FROM {$this->table_instances}
|
| WHERE class_id = %d AND class_date = %s",
|
| $holiday_class_id, $date
|
| )
|
| );
|
|
|
| if (!$existing) {
|
|
|
| $this->wpdb->insert(
|
| $this->table_instances,
|
| array(
|
| 'class_id' => $holiday_class_id,
|
| 'class_date' => $date,
|
| 'start_time' => '00:00:00',
|
| 'end_time' => '23:59:59',
|
| 'current_capacity' => 0,
|
| 'max_capacity' => 0,
|
| 'status' => 'scheduled',
|
| 'special_notes' => $name
|
| ),
|
| array('%d', '%s', '%s', '%s', '%d', '%d', '%s', '%s')
|
| );
|
| }
|
| }
|
| }
|
|
|
|
|
|
|
|
|
| private function getStatutoryHolidayDates($year) {
|
| $holidays = array();
|
|
|
|
|
| $holidays[$year . '-01-01'] = "<b>New Year's Day</b><br>Gym Closed";
|
|
|
|
|
| $family_day = new DateTime("third monday of february $year");
|
| $holidays[$family_day->format('Y-m-d')] = "<b>Family Day</b><br>Gym Closed";
|
|
|
|
|
| $easter_dates = array(
|
| 2025 => '2025-04-20', 2026 => '2026-04-05', 2027 => '2027-03-28',
|
| 2028 => '2028-04-16', 2029 => '2029-04-01', 2030 => '2030-04-21',
|
| 2031 => '2031-04-13', 2032 => '2032-03-28', 2033 => '2033-04-17',
|
| 2034 => '2034-04-09', 2035 => '2035-03-25'
|
| );
|
|
|
| if (isset($easter_dates[$year])) {
|
| $easter = new DateTime($easter_dates[$year]);
|
|
|
|
|
| $good_friday = clone $easter;
|
| $good_friday->modify('-2 days');
|
| $holidays[$good_friday->format('Y-m-d')] = "<b>Good Friday</b><br>Gym Closed";
|
|
|
|
|
| $holidays[$easter->format('Y-m-d')] = "<b>Easter Sunday</b><br>Gym Closed";
|
| }
|
|
|
|
|
| $victoria_day = new DateTime("may 24 $year");
|
| while ($victoria_day->format('w') != 1) {
|
| $victoria_day->modify('-1 day');
|
| }
|
| $holidays[$victoria_day->format('Y-m-d')] = "<b>Victoria Day</b><br>Gym Closed";
|
|
|
|
|
| $holidays[$year . '-07-01'] = "<b>Canada Day</b><br>Gym Closed";
|
|
|
|
|
| $labour_day = new DateTime("first monday of september $year");
|
| $holidays[$labour_day->format('Y-m-d')] = "<b>Labour Day</b><br>Gym Closed";
|
|
|
|
|
| $thanksgiving = new DateTime("second monday of october $year");
|
| $holidays[$thanksgiving->format('Y-m-d')] = "<b>Thanksgiving Day</b><br>Gym Closed";
|
|
|
|
|
| $holidays[$year . '-12-25'] = "<b>Christmas Day</b><br>Gym Closed";
|
|
|
| return $holidays;
|
| }
|
|
|
|
|
|
|
|
|
| public function getCalendarData($year, $month) {
|
| $start_date = date('Y-m-01', mktime(0, 0, 0, $month, 1, $year));
|
| $end_date = date('Y-m-t', mktime(0, 0, 0, $month, 1, $year));
|
|
|
|
|
| $query = "
|
| SELECT
|
| ci.id as instance_id,
|
| ci.class_date,
|
| ci.start_time,
|
| ci.end_time,
|
| ci.current_capacity,
|
| ci.max_capacity,
|
| ci.status,
|
| ci.special_notes,
|
| gc.id as class_id,
|
| gc.class_name,
|
| gc.description,
|
| gc.age_restriction,
|
| gc.gender_restriction,
|
| gc.instructor,
|
| gc.class_type
|
| FROM {$this->table_instances} ci
|
| JOIN {$this->table_classes} gc ON ci.class_id = gc.id
|
| WHERE ci.class_date BETWEEN %s AND %s
|
| AND ci.status = 'scheduled'
|
| AND gc.is_active = 1
|
| ORDER BY ci.class_date, ci.start_time
|
| ";
|
|
|
| $all_classes = $this->wpdb->get_results(
|
| $this->wpdb->prepare($query, $start_date, $end_date),
|
| ARRAY_A
|
| );
|
|
|
|
|
| $holiday_dates = [];
|
| $filtered_classes = [];
|
|
|
|
|
| foreach ($all_classes as $class) {
|
| if ($class['class_type'] === 'holiday') {
|
| $holiday_dates[] = $class['class_date'];
|
| }
|
| }
|
|
|
|
|
| foreach ($all_classes as $class) {
|
| if ($class['class_type'] === 'holiday') {
|
|
|
| $filtered_classes[] = $class;
|
| } elseif (!in_array($class['class_date'], $holiday_dates)) {
|
|
|
| $filtered_classes[] = $class;
|
| }
|
| }
|
|
|
| return $filtered_classes;
|
| }
|
|
|
|
|
|
|
|
|
| public function getUserRegistrations($user_id, $start_date = null, $end_date = null) {
|
| if (!$start_date) {
|
| $start_date = date('Y-m-d');
|
| }
|
| if (!$end_date) {
|
| $end_date = date('Y-m-d', strtotime('+6 months'));
|
| }
|
|
|
| $query = "
|
| SELECT
|
| cr.id as registration_id,
|
| cr.status as registration_status,
|
| cr.registration_date,
|
| cr.credits_used,
|
| ci.id as instance_id,
|
| ci.class_date,
|
| ci.start_time,
|
| ci.end_time,
|
| gc.class_name,
|
| gc.instructor,
|
| gc.class_type
|
| FROM {$this->table_registrations} cr
|
| JOIN {$this->table_instances} ci ON cr.class_instance_id = ci.id
|
| JOIN {$this->table_classes} gc ON ci.class_id = gc.id
|
| WHERE cr.user_id = %d
|
| AND ci.class_date BETWEEN %s AND %s
|
| AND cr.status IN ('registered', 'attended')
|
| ORDER BY ci.class_date, ci.start_time
|
| ";
|
|
|
| return $this->wpdb->get_results(
|
| $this->wpdb->prepare($query, $user_id, $start_date, $end_date),
|
| ARRAY_A
|
| );
|
| }
|
|
|
|
|
|
|
|
|
| public function registerUserForClass($user_id, $instance_id, $credits_to_use = 60) {
|
|
|
| if (!$this->hasUserSufficientCredits($user_id, $credits_to_use)) {
|
| return ['success' => false, 'message' => 'Insufficient credits'];
|
| }
|
|
|
|
|
| $instance = $this->getClassInstance($instance_id);
|
| if (!$instance) {
|
| return ['success' => false, 'message' => 'Class not found'];
|
| }
|
|
|
| if ($instance['current_capacity'] >= $instance['max_capacity']) {
|
| return ['success' => false, 'message' => 'Class is full'];
|
| }
|
|
|
|
|
| $existing = $this->wpdb->get_var(
|
| $this->wpdb->prepare(
|
| "SELECT id FROM {$this->table_registrations}
|
| WHERE user_id = %d AND class_instance_id = %d",
|
| $user_id, $instance_id
|
| )
|
| );
|
|
|
| if ($existing) {
|
| return ['success' => false, 'message' => 'Already registered for this class'];
|
| }
|
|
|
|
|
| $this->wpdb->query('START TRANSACTION');
|
|
|
| try {
|
|
|
| $result = $this->wpdb->insert(
|
| $this->table_registrations,
|
| [
|
| 'user_id' => $user_id,
|
| 'class_instance_id' => $instance_id,
|
| 'credits_used' => $credits_to_use,
|
| 'status' => 'registered'
|
| ],
|
| ['%d', '%d', '%d', '%s']
|
| );
|
|
|
| if ($result === false) {
|
| throw new Exception('Failed to insert registration');
|
| }
|
|
|
|
|
| $this->wpdb->query(
|
| $this->wpdb->prepare(
|
| "UPDATE {$this->table_instances}
|
| SET current_capacity = current_capacity + 1
|
| WHERE id = %d",
|
| $instance_id
|
| )
|
| );
|
|
|
|
|
| $this->deductUserCredits($user_id, $credits_to_use);
|
|
|
| $registration_id = $this->wpdb->insert_id;
|
|
|
| $this->wpdb->query('COMMIT');
|
|
|
|
|
| $this->sendConfirmationEmail($user_id, $registration_id);
|
|
|
| return [
|
| 'success' => true,
|
| 'message' => 'Successfully registered for class',
|
| 'registration_id' => $registration_id
|
| ];
|
|
|
| } catch (Exception $e) {
|
| $this->wpdb->query('ROLLBACK');
|
| return ['success' => false, 'message' => 'Registration failed: ' . $e->getMessage()];
|
| }
|
| }
|
|
|
|
|
|
|
|
|
| public function registerUserForMultipleWeeks($user_id, $instance_id, $weeks_count, $credits_per_class = 60) {
|
| error_log("CALENDAR DEBUG - registerUserForMultipleWeeks called:");
|
| error_log("user_id: " . $user_id);
|
| error_log("instance_id: " . $instance_id);
|
| error_log("weeks_count: " . $weeks_count);
|
| error_log("credits_per_class: " . $credits_per_class);
|
|
|
|
|
| $base_instance = $this->getClassInstance($instance_id);
|
| if (!$base_instance) {
|
| error_log("CALENDAR DEBUG - Base instance not found for ID: " . $instance_id);
|
| return ['success' => false, 'message' => 'Class not found'];
|
| }
|
|
|
| error_log("CALENDAR DEBUG - Base instance found: " . print_r($base_instance, true));
|
|
|
|
|
| $total_credits_needed = $credits_per_class * $weeks_count;
|
|
|
|
|
| if (!$this->hasUserSufficientCredits($user_id, $total_credits_needed)) {
|
| return ['success' => false, 'message' => "Insufficient credits. Need {$total_credits_needed} credits for {$weeks_count} weeks"];
|
| }
|
|
|
|
|
| $instances_to_book = [];
|
| $base_date = new DateTime($base_instance['class_date']);
|
|
|
| for ($week = 0; $week < $weeks_count; $week++) {
|
| $target_date = clone $base_date;
|
| $target_date->add(new DateInterval('P' . ($week * 7) . 'D'));
|
|
|
| $instance = $this->wpdb->get_row(
|
| $this->wpdb->prepare(
|
| "SELECT * FROM {$this->table_instances}
|
| WHERE class_id = %d AND class_date = %s AND start_time = %s",
|
| $base_instance['class_id'],
|
| $target_date->format('Y-m-d'),
|
| $base_instance['start_time']
|
| ),
|
| ARRAY_A
|
| );
|
|
|
| if (!$instance) {
|
| return ['success' => false, 'message' => "Class not available for week " . ($week + 1) . " on " . $target_date->format('M j, Y')];
|
| }
|
|
|
|
|
| if ($instance['current_capacity'] >= $instance['max_capacity']) {
|
| return ['success' => false, 'message' => "Class is full for week " . ($week + 1) . " on " . $target_date->format('M j, Y')];
|
| }
|
|
|
|
|
| $existing = $this->wpdb->get_var(
|
| $this->wpdb->prepare(
|
| "SELECT id FROM {$this->table_registrations}
|
| WHERE user_id = %d AND class_instance_id = %d",
|
| $user_id, $instance['id']
|
| )
|
| );
|
|
|
| if ($existing) {
|
| return ['success' => false, 'message' => "Already registered for week " . ($week + 1) . " on " . $target_date->format('M j, Y')];
|
| }
|
|
|
| $instances_to_book[] = $instance;
|
| }
|
|
|
|
|
| $this->wpdb->query('START TRANSACTION');
|
|
|
| try {
|
| $registration_ids = [];
|
|
|
|
|
| foreach ($instances_to_book as $instance) {
|
|
|
| $result = $this->wpdb->insert(
|
| $this->table_registrations,
|
| [
|
| 'user_id' => $user_id,
|
| 'class_instance_id' => $instance['id'],
|
| 'credits_used' => $credits_per_class,
|
| 'status' => 'registered'
|
| ],
|
| ['%d', '%d', '%d', '%s']
|
| );
|
|
|
| if ($result === false) {
|
| throw new Exception('Failed to insert registration for ' . $instance['class_date']);
|
| }
|
|
|
| $registration_ids[] = $this->wpdb->insert_id;
|
|
|
|
|
| $this->wpdb->query(
|
| $this->wpdb->prepare(
|
| "UPDATE {$this->table_instances}
|
| SET current_capacity = current_capacity + 1
|
| WHERE id = %d",
|
| $instance['id']
|
| )
|
| );
|
| }
|
|
|
|
|
| $this->deductUserCredits($user_id, $total_credits_needed);
|
|
|
| $this->wpdb->query('COMMIT');
|
|
|
|
|
| $this->sendMultiWeekConfirmationEmail($user_id, $registration_ids);
|
|
|
| return [
|
| 'success' => true,
|
| 'message' => "Successfully registered for {$weeks_count} weeks",
|
| 'registration_ids' => $registration_ids,
|
| 'weeks_count' => $weeks_count
|
| ];
|
|
|
| } catch (Exception $e) {
|
| $this->wpdb->query('ROLLBACK');
|
| return ['success' => false, 'message' => 'Multi-week registration failed: ' . $e->getMessage()];
|
| }
|
| }
|
|
|
|
|
|
|
|
|
| public function generateClassInstances($weeks_ahead = 8) {
|
| $classes = $this->wpdb->get_results(
|
| "SELECT * FROM {$this->table_classes} WHERE is_active = 1",
|
| ARRAY_A
|
| );
|
|
|
| $start_date = date('Y-m-d');
|
| $end_date = date('Y-m-d', strtotime("+{$weeks_ahead} weeks"));
|
|
|
| foreach ($classes as $class) {
|
| $this->generateInstancesForClass($class, $start_date, $end_date);
|
| }
|
| }
|
|
|
|
|
|
|
|
|
| private function generateInstancesForClass($class, $start_date, $end_date) {
|
| $current_date = new DateTime($start_date);
|
| $end_date_obj = new DateTime($end_date);
|
|
|
| while ($current_date <= $end_date_obj) {
|
|
|
| if ($current_date->format('w') == $class['day_of_week']) {
|
|
|
| $existing = $this->wpdb->get_var(
|
| $this->wpdb->prepare(
|
| "SELECT id FROM {$this->table_instances}
|
| WHERE class_id = %d AND class_date = %s AND start_time = %s",
|
| $class['id'], $current_date->format('Y-m-d'), $class['start_time']
|
| )
|
| );
|
|
|
| if (!$existing) {
|
| $this->wpdb->insert(
|
| $this->table_instances,
|
| [
|
| 'class_id' => $class['id'],
|
| 'class_date' => $current_date->format('Y-m-d'),
|
| 'start_time' => $class['start_time'],
|
| 'end_time' => $class['end_time'],
|
| 'max_capacity' => $class['max_capacity'],
|
| 'status' => 'scheduled'
|
| ],
|
| ['%d', '%s', '%s', '%s', '%d', '%s']
|
| );
|
| }
|
| }
|
| $current_date->add(new DateInterval('P1D'));
|
| }
|
| }
|
|
|
| private function getClassInstance($instance_id) {
|
| return $this->wpdb->get_row(
|
| $this->wpdb->prepare(
|
| "SELECT * FROM {$this->table_instances} WHERE id = %d",
|
| $instance_id
|
| ),
|
| ARRAY_A
|
| );
|
| }
|
|
|
| private function hasUserSufficientCredits($user_id, $required_credits) {
|
|
|
| $user_meta = get_userdata($user_id);
|
| if ($user_meta && in_array('administrator', $user_meta->roles)) {
|
| return true;
|
| }
|
|
|
| $credits = $this->wpdb->get_var(
|
| $this->wpdb->prepare(
|
| "SELECT credits_remaining FROM {$this->table_credits} WHERE user_id = %d AND is_active = 1",
|
| $user_id
|
| )
|
| );
|
|
|
| return ($credits >= $required_credits);
|
| }
|
|
|
| private function deductUserCredits($user_id, $credits) {
|
| $this->wpdb->query(
|
| $this->wpdb->prepare(
|
| "UPDATE {$this->table_credits}
|
| SET credits_remaining = credits_remaining - %d,
|
| updated_at = NOW()
|
| WHERE user_id = %d AND is_active = 1",
|
| $credits, $user_id
|
| )
|
| );
|
| }
|
|
|
| private function addUserCredits($user_id, $credits) {
|
| $this->wpdb->query(
|
| $this->wpdb->prepare(
|
| "UPDATE {$this->table_credits}
|
| SET credits_remaining = credits_remaining + %d,
|
| updated_at = NOW()
|
| WHERE user_id = %d AND is_active = 1",
|
| $credits, $user_id
|
| )
|
| );
|
| }
|
|
|
| private function getSetting($setting_name, $default = null) {
|
| $value = $this->wpdb->get_var(
|
| $this->wpdb->prepare(
|
| "SELECT setting_value FROM {$this->table_settings} WHERE setting_name = %s",
|
| $setting_name
|
| )
|
| );
|
|
|
| return $value !== null ? $value : $default;
|
| }
|
|
|
|
|
|
|
|
|
| private function sendMultiWeekConfirmationEmail($user_id, $registration_ids) {
|
| $user = get_user_by('id', $user_id);
|
| if (!$user) {
|
| error_log("User not found for ID: {$user_id}");
|
| return false;
|
| }
|
|
|
|
|
| $registrations_data = [];
|
| foreach ($registration_ids as $registration_id) {
|
| $data = $this->getRegistrationData($registration_id);
|
| if ($data) {
|
| $registrations_data[] = $data;
|
| }
|
| }
|
|
|
| if (empty($registrations_data)) {
|
| error_log("No registration data found for IDs: " . implode(', ', $registration_ids));
|
| return false;
|
| }
|
|
|
|
|
| usort($registrations_data, function($a, $b) {
|
| return strtotime($a['class_date']) - strtotime($b['class_date']);
|
| });
|
|
|
| $first_class = $registrations_data[0];
|
| $weeks_count = count($registrations_data);
|
|
|
| $subject = "Class Registration Confirmation - {$first_class['class_name']} ({$weeks_count} weeks)";
|
| $message = $this->generateMultiWeekEmailContent($user, $registrations_data);
|
|
|
|
|
| $headers = [
|
| 'Content-Type: text/html; charset=UTF-8',
|
| 'From: The Den Mixed Martial Arts <[email protected]>'
|
| ];
|
|
|
|
|
| $attachments = [];
|
| if ($this->isICalEnabled()) {
|
| $ical_file = $this->generateMultiWeekICalFile($registrations_data);
|
| if ($ical_file) {
|
| $attachments[] = $ical_file;
|
| }
|
| }
|
|
|
|
|
| if ($this->getSetting('email_confirmations', 'true') !== 'true') {
|
| error_log("Email confirmations disabled - skipping multi-week email for registrations: " . implode(', ', $registration_ids));
|
| return true;
|
| }
|
|
|
|
|
| $sent = wp_mail($user->user_email, $subject, $message, $headers, $attachments);
|
|
|
|
|
| if (!empty($attachments)) {
|
| foreach ($attachments as $file) {
|
| if (file_exists($file)) {
|
| unlink($file);
|
| }
|
| }
|
| }
|
|
|
| if ($sent) {
|
| error_log("Multi-week confirmation email sent successfully to {$user->user_email} for {$weeks_count} registrations");
|
| } else {
|
| error_log("CRITICAL: Failed to send multi-week confirmation email to {$user->user_email}");
|
| }
|
|
|
| return $sent;
|
| }
|
|
|
|
|
|
|
|
|
| private function sendConfirmationEmail($user_id, $registration_id) {
|
|
|
| $registration_data = $this->getRegistrationData($registration_id);
|
| if (!$registration_data) {
|
| error_log("No registration data found for ID: {$registration_id}");
|
| return false;
|
| }
|
|
|
| $user = get_user_by('id', $user_id);
|
| if (!$user) {
|
| error_log("User not found for ID: {$user_id}");
|
| return false;
|
| }
|
|
|
| $subject = "Class Registration Confirmation - {$registration_data['class_name']}";
|
| $message = $this->generateEmailContent($user, $registration_data);
|
|
|
|
|
| $headers = [
|
| 'Content-Type: text/html; charset=UTF-8',
|
| 'From: The Den Mixed Martial Arts <[email protected]>'
|
| ];
|
|
|
|
|
| $attachments = [];
|
| if ($this->isICalEnabled()) {
|
| $ical_file = $this->generateICalFile($registration_data);
|
| if ($ical_file) {
|
| $attachments[] = $ical_file;
|
| }
|
| }
|
|
|
|
|
| if ($this->getSetting('email_confirmations', 'true') !== 'true') {
|
| error_log("Email confirmations disabled - skipping email for registration: {$registration_id}");
|
| return true;
|
| }
|
|
|
|
|
| $sent = wp_mail($user->user_email, $subject, $message, $headers, $attachments);
|
|
|
|
|
| if (!empty($attachments) && file_exists($attachments[0])) {
|
| unlink($attachments[0]);
|
| }
|
|
|
| if ($sent) {
|
| error_log("Confirmation email sent successfully to {$user->user_email} for registration {$registration_id}");
|
| } else {
|
| error_log("CRITICAL: Failed to send confirmation email to {$user->user_email} for registration {$registration_id}");
|
| }
|
|
|
| return $sent;
|
| }
|
|
|
| private function generateEmailContent($user, $registration_data) {
|
| $class_date_formatted = date('l, F j, Y', strtotime($registration_data['class_date']));
|
| $start_time_formatted = date('g:i A', strtotime($registration_data['start_time']));
|
| $end_time_formatted = date('g:i A', strtotime($registration_data['end_time']));
|
|
|
| return "
|
| <style>
|
| body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f5f5f5; }
|
| .container { background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
|
| .header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 3px solid #800000; }
|
| .header img { max-height: 80px; margin-bottom: 10px; }
|
| .header h1 { color: #800000; margin: 0; font-size: 28px; }
|
| .class-details { background: #f8f9fa; padding: 15px; margin: 15px 0; border-radius: 5px; border-left: 4px solid #800000; }
|
| .detail-row { margin: 8px 0; }
|
| .detail-label { font-weight: bold; color: #800000; }
|
| .detail-value { margin-left: 10px; }
|
| .btn { background: #800000; color: white !important; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block; margin: 10px 5px; }
|
| .logo { max-height: 60px; vertical-align: middle; margin-right: 15px; }
|
| </style>
|
|
|
| <div class='container'>
|
| <div class='header'>
|
| <img src='https://thedenmartialarts.ca/wp-content/uploads/2025/05/cropped-logoBlk.png' alt='The Den Mixed Martial Arts' class='logo'>
|
| <h1>Class Registration Confirmed!</h1>
|
| </div>
|
|
|
| <p>Hello {$user->first_name},</p>
|
|
|
| <p>Great news! You've successfully registered for a class at The Den Mixed Martial Arts. Here are your class details:</p>
|
|
|
| <div class='class-details'>
|
| <div class='detail-row'>
|
| <span class='detail-label'>Class:</span>
|
| <span class='detail-value'>{$registration_data['class_name']}</span>
|
| </div>
|
| <div class='detail-row'>
|
| <span class='detail-label'>Date:</span>
|
| <span class='detail-value'>{$class_date_formatted}</span>
|
| </div>
|
| <div class='detail-row'>
|
| <span class='detail-label'>Time:</span>
|
| <span class='detail-value'>{$start_time_formatted} - {$end_time_formatted}</span>
|
| </div>
|
| " . ($registration_data['instructor'] ? "
|
| <div class='detail-row'>
|
| <span class='detail-label'>Instructor:</span>
|
| <span class='detail-value'>{$registration_data['instructor']}</span>
|
| </div>
|
| " : "") . "
|
| <div class='detail-row'>
|
| <span class='detail-label'>Credits Used:</span>
|
| <span class='detail-value'>{$registration_data['credits_used']} minutes</span>
|
| </div>
|
| </div>
|
|
|
| <p><strong>Important:</strong> Please arrive 10-15 minutes before your class starts. Don't forget to bring water and a towel!</p>
|
|
|
| <div style='text-align: center; margin: 30px 0;'>
|
| <a href='https://thedenmartialarts.ca/my-membership/' class='btn'>View My Classes</a>
|
| <a href='https://thedenmartialarts.ca/gym-calendar/' class='btn'>Browse More Classes</a>
|
| </div>
|
|
|
| <p>If you need to cancel or reschedule, please contact us as soon as possible.</p>
|
|
|
| <p>Looking forward to seeing you on the mats!</p>
|
|
|
| <p>Best regards,<br>
|
| The Den Mixed Martial Arts Team<br>
|
| <a href='mailto:[email protected]'>[email protected]</a><br>
|
| (613) 831-3362</p>
|
| </div>";
|
| }
|
|
|
| private function generateMultiWeekEmailContent($user, $registrations_data) {
|
| $first_class = $registrations_data[0];
|
| $weeks_count = count($registrations_data);
|
| $total_credits = array_sum(array_column($registrations_data, 'credits_used'));
|
|
|
| return "
|
| <body style=\"font-family: Arial, sans-serif; line-height: 1.6; color: #333; max-width: 600px; margin: 0 auto; padding: 20px; background-color: #f5f5f5;\">
|
| <div class=\"container\" style=\"background: white; padding: 30px; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1);\">
|
| <div class=\"header\" style=\"text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 3px solid #800000;\">
|
| <img src=\"https://thedenmartialarts.ca/wp-content/uploads/2025/05/cropped-logoBlk.png\" alt=\"The Den Mixed Martial Arts\" style=\"max-height: 80px; margin-bottom: 10px;\">
|
| <h1 style=\"color: #800000; margin: 0; font-size: 28px;\">Multi-Week Registration Confirmed!</h1>
|
| </div>
|
|
|
| <p>Hello {$user->first_name},</p>
|
|
|
| <p>Excellent! You've successfully registered for <strong>{$weeks_count} weeks</strong> of <strong>{$first_class['class_name']}</strong> at The Den Mixed Martial Arts.</p>
|
|
|
| <div class=\"class-schedule\" style=\"background: #f8f9fa; padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid #800000;\">
|
| <h3 style=\"color: #800000; margin-top: 0;\">Your Class Schedule</h3>";
|
|
|
| foreach ($registrations_data as $index => $registration) {
|
| $class_date_formatted = date('l, F j, Y', strtotime($registration['class_date']));
|
| $start_time_formatted = date('g:i A', strtotime($registration['start_time']));
|
| $end_time_formatted = date('g:i A', strtotime($registration['end_time']));
|
|
|
| $content .= "
|
| <div class=\"week-entry\" style=\"margin-bottom: 15px; padding: 10px; background: white; border-radius: 5px;\">
|
| <div style=\"font-weight: bold; color: #800000;\">Week " . ($index + 1) . "</div>
|
| <div>{$class_date_formatted}</div>
|
| <div>{$start_time_formatted} - {$end_time_formatted}</div>
|
| " . ($registration['instructor'] ? "<div>{$registration['instructor']}</div>" : "") . "
|
| <div>💳 {$registration['credits_used']} credits</div>
|
| </div>";
|
| }
|
|
|
| $content .= "
|
| <div style=\"margin-top: 15px; padding-top: 10px; border-top: 1px solid #ddd;\">
|
| <strong>Total Credits Used: {$total_credits} minutes</strong>
|
| </div>
|
| </div>
|
|
|
| <p><strong>What's Next?</strong></p>
|
| <ul>
|
| <li>Mark your calendar for all {$weeks_count} classes</li>
|
| <li>Arrive 10-15 minutes early for each session</li>
|
| <li>Bring water and a towel to every class</li>
|
| <li>Contact us if you need to reschedule any individual sessions</li>
|
| </ul>
|
|
|
| <div style=\"text-align: center; margin: 30px 0;\">
|
| <a href=\"https://thedenmartialarts.ca/my-membership/\" style=\"display: inline-block; background: #800000; color: white; padding: 12px 25px; text-decoration: none; border-radius: 5px; margin: 0 10px;\">View My Classes</a>
|
| <a href=\"https://thedenmartialarts.ca/gym-calendar/\" style=\"display: inline-block; background: #6c757d; color: white; padding: 12px 25px; text-decoration: none; border-radius: 5px; margin: 0 10px;\">Browse More Classes</a>
|
| </div>
|
|
|
| <p>We're excited to see your progress over the next {$weeks_count} weeks! Consistency is key to mastering martial arts.</p>
|
|
|
| <p>Best regards,<br>
|
| The Den Mixed Martial Arts Team<br>
|
| <a href=\"mailto:[email protected]\">[email protected]</a><br>
|
| (613) 831-3362</p>
|
| </div>
|
| <div class=\"footer\" style=\"text-align: center; margin-top: 30px; color: #666; font-size: 12px;\">
|
| <p>The Den Mixed Martial Arts | Stittsville, Ontario, Canada</p>
|
| </div>
|
| </body>";
|
|
|
| return $content;
|
| }
|
|
|
| private function getRegistrationData($registration_id) {
|
| return $this->wpdb->get_row(
|
| $this->wpdb->prepare(
|
| "SELECT
|
| cr.id as registration_id,
|
| cr.credits_used,
|
| cr.registration_date,
|
| ci.class_date,
|
| ci.start_time,
|
| ci.end_time,
|
| gc.class_name,
|
| gc.description,
|
| gc.instructor
|
| FROM {$this->table_registrations} cr
|
| JOIN {$this->table_instances} ci ON cr.class_instance_id = ci.id
|
| JOIN {$this->table_classes} gc ON ci.class_id = gc.id
|
| WHERE cr.id = %d",
|
| $registration_id
|
| ),
|
| ARRAY_A
|
| );
|
| }
|
|
|
| private function isICalEnabled() {
|
| return $this->getSetting('ical_attachments', 'true') === 'true';
|
| }
|
|
|
|
|
|
|
|
|
| private function generateICalFile($registration_data) {
|
| $class_date = $registration_data['class_date'];
|
| $start_time = $registration_data['start_time'];
|
| $end_time = $registration_data['end_time'];
|
| $class_name = $registration_data['class_name'];
|
| $description = $registration_data['description'] ?: '';
|
| $instructor = $registration_data['instructor'] ?: '';
|
|
|
|
|
| $start_datetime = new DateTime($class_date . ' ' . $start_time);
|
| $end_datetime = new DateTime($class_date . ' ' . $end_time);
|
|
|
|
|
| $start_datetime->setTimezone(new DateTimeZone('UTC'));
|
| $end_datetime->setTimezone(new DateTimeZone('UTC'));
|
|
|
| $start_ical = $start_datetime->format('Ymd\THis\Z');
|
| $end_ical = $end_datetime->format('Ymd\THis\Z');
|
| $now_ical = (new DateTime())->format('Ymd\THis\Z');
|
|
|
|
|
| $uid = 'gym-class-' . $registration_data['registration_id'] . '@thedenmartialarts.ca';
|
|
|
|
|
| $ical_content = "BEGIN:VCALENDAR\r\n";
|
| $ical_content .= "VERSION:2.0\r\n";
|
| $ical_content .= "PRODID:-//The Den Mixed Martial Arts//Class Registration//EN\r\n";
|
| $ical_content .= "CALSCALE:GREGORIAN\r\n";
|
| $ical_content .= "METHOD:PUBLISH\r\n";
|
| $ical_content .= "BEGIN:VEVENT\r\n";
|
| $ical_content .= "UID:" . $uid . "\r\n";
|
| $ical_content .= "DTSTAMP:" . $now_ical . "\r\n";
|
| $ical_content .= "DTSTART:" . $start_ical . "\r\n";
|
| $ical_content .= "DTEND:" . $end_ical . "\r\n";
|
| $ical_content .= "SUMMARY:" . $class_name . "\r\n";
|
|
|
| $event_description = $description;
|
| if ($instructor) {
|
| $event_description .= ($event_description ? "\n" : "") . "Instructor: " . $instructor;
|
| }
|
| $event_description .= ($event_description ? "\n" : "") . "Location: The Den Mixed Martial Arts, Kanata, ON";
|
|
|
| if ($event_description) {
|
| $ical_content .= "DESCRIPTION:" . str_replace(["\n", "\r"], ["\\n", ""], $event_description) . "\r\n";
|
| }
|
|
|
| $ical_content .= "LOCATION:The Den Mixed Martial Arts, Stittsville, ON\r\n";
|
| $ical_content .= "STATUS:CONFIRMED\r\n";
|
| $ical_content .= "END:VEVENT\r\n";
|
| $ical_content .= "END:VCALENDAR\r\n";
|
|
|
|
|
| $temp_file = tempnam(sys_get_temp_dir(), 'gym_class_') . '.ics';
|
| file_put_contents($temp_file, $ical_content);
|
|
|
| return $temp_file;
|
| }
|
|
|
|
|
|
|
|
|
| private function generateMultiWeekICalFile($registrations_data) {
|
| if (empty($registrations_data)) {
|
| return false;
|
| }
|
|
|
| $now_ical = (new DateTime())->format('Ymd\THis\Z');
|
|
|
|
|
| $ical_content = "BEGIN:VCALENDAR\r\n";
|
| $ical_content .= "VERSION:2.0\r\n";
|
| $ical_content .= "PRODID:-//The Den Mixed Martial Arts//Multi-Week Class Registration//EN\r\n";
|
| $ical_content .= "CALSCALE:GREGORIAN\r\n";
|
| $ical_content .= "METHOD:PUBLISH\r\n";
|
|
|
| foreach ($registrations_data as $registration) {
|
| $class_date = $registration['class_date'];
|
| $start_time = $registration['start_time'];
|
| $end_time = $registration['end_time'];
|
| $class_name = $registration['class_name'];
|
| $description = $registration['description'] ?: '';
|
| $instructor = $registration['instructor'] ?: '';
|
|
|
|
|
| $start_datetime = new DateTime($class_date . ' ' . $start_time);
|
| $end_datetime = new DateTime($class_date . ' ' . $end_time);
|
|
|
|
|
| $start_datetime->setTimezone(new DateTimeZone('UTC'));
|
| $end_datetime->setTimezone(new DateTimeZone('UTC'));
|
|
|
| $start_ical = $start_datetime->format('Ymd\THis\Z');
|
| $end_ical = $end_datetime->format('Ymd\THis\Z');
|
|
|
|
|
| $uid = 'gym-class-' . $registration['registration_id'] . '@thedenmartialarts.ca';
|
|
|
| $ical_content .= "BEGIN:VEVENT\r\n";
|
| $ical_content .= "UID:" . $uid . "\r\n";
|
| $ical_content .= "DTSTAMP:" . $now_ical . "\r\n";
|
| $ical_content .= "DTSTART:" . $start_ical . "\r\n";
|
| $ical_content .= "DTEND:" . $end_ical . "\r\n";
|
| $ical_content .= "SUMMARY:" . $class_name . "\r\n";
|
|
|
| $event_description = $description;
|
| if ($instructor) {
|
| $event_description .= ($event_description ? "\n" : "") . "Instructor: " . $instructor;
|
| }
|
| $event_description .= ($event_description ? "\n" : "") . "Location: The Den Mixed Martial Arts, Kanata, ON";
|
|
|
| if ($event_description) {
|
| $ical_content .= "DESCRIPTION:" . str_replace(["\n", "\r"], ["\\n", ""], $event_description) . "\r\n";
|
| }
|
|
|
| $ical_content .= "LOCATION:The Den Mixed Martial Arts, Stittsville, ON\r\n";
|
| $ical_content .= "STATUS:CONFIRMED\r\n";
|
| $ical_content .= "END:VEVENT\r\n";
|
| }
|
|
|
| $ical_content .= "END:VCALENDAR\r\n";
|
|
|
|
|
| $temp_file = tempnam(sys_get_temp_dir(), 'gym_multiweek_') . '.ics';
|
| file_put_contents($temp_file, $ical_content);
|
|
|
| return $temp_file;
|
| }
|
|
|
|
|
|
|
|
|
| public function testEmailSystem($test_email) {
|
|
|
| $test_data = [
|
| 'registration_id' => 999999,
|
| 'class_name' => 'Test Class - Brazilian Jiu-Jitsu',
|
| 'class_date' => date('Y-m-d', strtotime('+1 day')),
|
| 'start_time' => '19:00:00',
|
| 'end_time' => '20:30:00',
|
| 'instructor' => 'Test Instructor',
|
| 'description' => 'This is a test email to verify the email system is working correctly.',
|
| 'credits_used' => 90
|
| ];
|
|
|
|
|
| $test_user = (object) [
|
| 'first_name' => 'Test',
|
| 'last_name' => 'Member',
|
| 'user_email' => $test_email
|
| ];
|
|
|
| $subject = "Test Email - Calendar System Working";
|
| $message = $this->generateEmailContent($test_user, $test_data);
|
|
|
| $headers = [
|
| 'Content-Type: text/html; charset=UTF-8',
|
| 'From: The Den Mixed Martial Arts <[email protected]>'
|
| ];
|
|
|
| $sent = wp_mail($test_email, $subject, $message, $headers);
|
|
|
| if ($sent) {
|
| return ['success' => true, 'message' => "Test email sent successfully to {$test_email}"];
|
| } else {
|
| return ['success' => false, 'message' => "Failed to send test email to {$test_email}"];
|
| }
|
| }
|
| }
|
|
|
| class GymCalendarShortcode {
|
|
|
| private $calendar_core;
|
|
|
| public function __construct() {
|
| $this->calendar_core = new GymCalendarCore();
|
|
|
|
|
| add_shortcode('gym_calendar', [$this, 'renderCalendar']);
|
|
|
|
|
| add_action('wp_ajax_gym_calendar_action', [$this, 'handleAjaxRequest']);
|
| add_action('wp_ajax_nopriv_gym_calendar_action', [$this, 'handleAjaxRequest']);
|
|
|
|
|
| add_action('wp_enqueue_scripts', [$this, 'enqueueAssets']);
|
| }
|
|
|
|
|
|
|
|
|
| public function enqueueAssets() {
|
|
|
| global $post;
|
| if (is_a($post, 'WP_Post') && has_shortcode($post->post_content, 'gym_calendar')) {
|
| wp_enqueue_script('jquery');
|
| }
|
| }
|
|
|
|
|
|
|
|
|
| public function renderCalendar($atts = []) {
|
|
|
| $atts = shortcode_atts([
|
| 'month' => date('n'),
|
| 'year' => date('Y')
|
| ], $atts);
|
|
|
| $month = intval($atts['month']);
|
| $year = intval($atts['year']);
|
|
|
|
|
| $classes = $this->calendar_core->getCalendarData($year, $month);
|
|
|
|
|
| $user_classes = [];
|
| if (is_user_logged_in()) {
|
| $user_id = get_current_user_id();
|
| $start_date = date('Y-m-01', mktime(0, 0, 0, $month, 1, $year));
|
| $end_date = date('Y-m-t', mktime(0, 0, 0, $month, 1, $year));
|
| $user_classes = $this->calendar_core->getUserRegistrations($user_id, $start_date, $end_date);
|
| }
|
|
|
|
|
| $html = $this->generateCalendarHTML($year, $month, $classes, $user_classes);
|
| $html .= $this->generateModalHTML();
|
| $html .= $this->generateJavaScript();
|
|
|
| return $html;
|
| }
|
|
|
|
|
|
|
|
|
| private function generateCalendarHTML($year, $month, $classes, $user_classes) {
|
| $month_name = date('F', mktime(0, 0, 0, $month, 1, $year));
|
|
|
|
|
| $prev_month = $month - 1;
|
| $prev_year = $year;
|
| if ($prev_month < 1) {
|
| $prev_month = 12;
|
| $prev_year--;
|
| }
|
|
|
| $next_month = $month + 1;
|
| $next_year = $year;
|
| if ($next_month > 12) {
|
| $next_month = 1;
|
| $next_year++;
|
| }
|
|
|
| $html = '<div id="gym-calendar-container">';
|
| $html .= '<div class="calendar-header">';
|
| $html .= '<div class="calendar-nav">';
|
| $html .= '<button class="nav-btn prev-month" data-year="' . $prev_year . '" data-month="' . $prev_month . '">‹ Previous</button>';
|
| $html .= '<h2 class="calendar-title">' . $month_name . ' ' . $year . '</h2>';
|
| $html .= '<button class="nav-btn next-month" data-year="' . $next_year . '" data-month="' . $next_month . '">Next ›</button>';
|
| $html .= '</div>';
|
| $html .= '</div>';
|
|
|
| $html .= '<div class="calendar-grid">';
|
|
|
|
|
| $html .= '<div class="calendar-row calendar-header-row">';
|
| $days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
| foreach ($days as $day) {
|
| $html .= '<div class="calendar-day-header">' . $day . '</div>';
|
| }
|
| $html .= '</div>';
|
|
|
|
|
| $first_day = date('w', mktime(0, 0, 0, $month, 1, $year));
|
| $days_in_month = date('t', mktime(0, 0, 0, $month, 1, $year));
|
| $today = date('Y-m-d');
|
|
|
|
|
| $classes_by_date = [];
|
| foreach ($classes as $class) {
|
| $date = $class['class_date'];
|
| if (!isset($classes_by_date[$date])) {
|
| $classes_by_date[$date] = [];
|
| }
|
| $classes_by_date[$date][] = $class;
|
| }
|
|
|
| $current_day = 1;
|
| $week_count = 0;
|
|
|
| while ($current_day <= $days_in_month) {
|
| $html .= '<div class="calendar-row">';
|
|
|
| for ($day_of_week = 0; $day_of_week < 7; $day_of_week++) {
|
| if ($week_count == 0 && $day_of_week < $first_day) {
|
|
|
| $html .= '<div class="calendar-day empty"></div>';
|
| } elseif ($current_day > $days_in_month) {
|
|
|
| $html .= '<div class="calendar-day empty"></div>';
|
| } else {
|
|
|
| $date = sprintf('%04d-%02d-%02d', $year, $month, $current_day);
|
| $day_classes = $classes_by_date[$date] ?? [];
|
|
|
| $today_class = ($date === $today) ? ' today' : '';
|
|
|
| $html .= '<div class="calendar-day' . $today_class . '" data-date="' . $date . '">';
|
| $html .= '<div class="day-number">' . $current_day . '</div>';
|
|
|
| if (!empty($day_classes)) {
|
| $html .= '<div class="day-classes">';
|
| foreach ($day_classes as $class) {
|
| $html .= $this->generateClassItemHTML($class, $user_classes);
|
| }
|
| $html .= '</div>';
|
| }
|
|
|
| $html .= '</div>';
|
| $current_day++;
|
| }
|
| }
|
|
|
| $html .= '</div>';
|
| $week_count++;
|
| }
|
|
|
| $html .= '</div>';
|
| $html .= '</div>';
|
|
|
| return $html;
|
| }
|
|
|
|
|
|
|
|
|
| private function generateClassItemHTML($class, $user_classes) {
|
| $start_time = date('g:i A', strtotime($class['start_time']));
|
| $is_registered = $this->isUserRegistered($class['instance_id'], $user_classes);
|
| $is_full = ($class['current_capacity'] >= $class['max_capacity']);
|
| $is_holiday = ($class['class_type'] === 'holiday');
|
|
|
| $css_classes = ['class-item'];
|
|
|
| if ($is_holiday) {
|
| $css_classes[] = 'holiday';
|
| $html = '<div class="' . implode(' ', $css_classes) . '">';
|
| $html .= '<span class="holiday-name"><b>' . htmlspecialchars($class['special_notes']) . '</b><br>Gym Closed</span>';
|
| $html .= '</div>';
|
| return $html;
|
| }
|
|
|
| if ($is_registered) {
|
| $css_classes[] = 'registered';
|
| } elseif ($is_full) {
|
| $css_classes[] = 'full';
|
| }
|
|
|
| $html = '<div class="' . implode(' ', $css_classes) . '"';
|
| $html .= ' data-instance-id="' . $class['instance_id'] . '"';
|
| $html .= ' data-class-name="' . htmlspecialchars($class['class_name']) . '"';
|
| $html .= ' data-start-time="' . $class['start_time'] . '"';
|
| $html .= ' data-end-time="' . $class['end_time'] . '"';
|
| $html .= ' data-capacity="' . $class['max_capacity'] . '"';
|
| $html .= ' data-current="' . $class['current_capacity'] . '"';
|
| $html .= ' data-is-registered="' . ($is_registered ? 'true' : 'false') . '"';
|
|
|
| if ($is_registered) {
|
| $registration = $this->getUserRegistration($class['instance_id'], $user_classes);
|
| $html .= ' data-registration-id="' . $registration['registration_id'] . '"';
|
| }
|
|
|
| $html .= '>';
|
| $html .= '<span class="class-time">' . $start_time . '</span>';
|
| $html .= '<span class="class-name">' . htmlspecialchars($class['class_name']) . '</span>';
|
|
|
| if ($is_registered) {
|
| $html .= '<span class="registered-indicator">✓</span>';
|
| } elseif ($is_full) {
|
| $html .= '<span class="full-indicator">FULL</span>';
|
| }
|
|
|
| $html .= '</div>';
|
|
|
| return $html;
|
| }
|
|
|
|
|
|
|
|
|
| private function generateModalHTML() {
|
| return '
|
| <!-- Booking Modal -->
|
| <div id="booking-modal" class="modal-overlay" style="display: none;">
|
| <div class="modal-content">
|
| <div class="modal-header">
|
| <h3 id="modal-class-title"></h3>
|
| <button class="modal-close">×</button>
|
| </div>
|
| <div class="modal-body">
|
| <div id="modal-class-details"></div>
|
| <div id="modal-booking-options"></div>
|
| </div>
|
| <div class="modal-footer">
|
| <button id="confirm-booking" class="btn btn-primary">Register for Class</button>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <!-- Notification Modal -->
|
| <div id="notification-modal" class="modal-overlay" style="display: none;">
|
| <div class="modal-content">
|
| <div class="modal-header">
|
| <h3 id="notification-title"></h3>
|
| <button class="modal-close">×</button>
|
| </div>
|
| <div class="modal-body">
|
| <div id="notification-message"></div>
|
| </div>
|
| <div class="modal-footer">
|
| <button id="notification-ok" class="btn btn-primary">OK</button>
|
| </div>
|
| </div>
|
| </div>';
|
| }
|
|
|
|
|
|
|
|
|
| private function generateJavaScript() {
|
|
|
| $user_id = get_current_user_id();
|
| $is_logged_in = is_user_logged_in() ? 'true' : 'false';
|
| $nonce = wp_create_nonce('gym_calendar_nonce');
|
|
|
|
|
| $user_credits = 0;
|
| $unlimited_membership = false;
|
|
|
| if ($user_id > 0) {
|
| global $wpdb;
|
| $table_credits = $wpdb->prefix . 'gym_credits';
|
|
|
| $credits_result = $wpdb->get_row(
|
| $wpdb->prepare(
|
| "SELECT credits_remaining, membership_type FROM {$table_credits} WHERE user_id = %d AND is_active = 1",
|
| $user_id
|
| ),
|
| ARRAY_A
|
| );
|
|
|
| if ($credits_result) {
|
| $user_credits = intval($credits_result['credits_remaining']);
|
| $membership_type = $credits_result['membership_type'];
|
| } else {
|
| $user_credits = 0;
|
| $membership_type = '';
|
| }
|
|
|
|
|
| $is_admin = current_user_can('administrator');
|
|
|
|
|
| $unlimited_membership = (
|
| $membership_type === 'unlimited' ||
|
| $user_credits == -1 ||
|
| $user_credits === '-1' ||
|
| $is_admin ||
|
| $membership_type === 'admin' ||
|
| $membership_type === 'staff' ||
|
| $membership_type === 'trainer' ||
|
|
|
| ($user_id > 0 && empty($membership_type) && $user_credits == 0)
|
| );
|
| }
|
|
|
| return "
|
| <script>
|
| (function() {
|
| 'use strict';
|
|
|
| // Calendar namespace isolation - prevent conflicts with other scripts
|
| const CalendarSystem = {
|
| initialized: false,
|
|
|
| // Safe initialization with error handling
|
| init: function() {
|
| if (this.initialized) return;
|
|
|
| try {
|
| this.waitForDependencies();
|
| } catch (error) {
|
| console.error('Calendar System initialization error:', error);
|
| this.fallbackInit();
|
| }
|
| },
|
|
|
| // Wait for required dependencies with timeout
|
| waitForDependencies: function() {
|
| const self = this;
|
| let attempts = 0;
|
| const maxAttempts = 50; // 5 seconds max wait
|
|
|
| const checkDependencies = function() {
|
| attempts++;
|
|
|
| if (typeof jQuery !== 'undefined' && document.readyState === 'complete') {
|
| self.setupCalendar();
|
| return;
|
| }
|
|
|
| if (attempts < maxAttempts) {
|
| setTimeout(checkDependencies, 100);
|
| } else {
|
| console.warn('Calendar System: Dependencies timeout, attempting fallback');
|
| self.fallbackInit();
|
| }
|
| };
|
|
|
| checkDependencies();
|
| },
|
|
|
| // Fallback initialization if dependencies fail
|
| fallbackInit: function() {
|
| console.log('Calendar System: Using fallback initialization');
|
| // Basic functionality without full jQuery support
|
| this.setupBasicEventHandlers();
|
| },
|
|
|
| // Main calendar setup with error isolation
|
| setupCalendar: function() {
|
| const $ = jQuery;
|
|
|
| try {
|
| // Calendar AJAX configuration - isolated in namespace
|
| this.config = {
|
| ajaxurl: '" . admin_url('admin-ajax.php') . "',
|
| nonce: '" . $nonce . "',
|
| is_user_logged_in: " . $is_logged_in . ",
|
| current_user_id: " . $user_id . ",
|
| user_credits: " . $user_credits . ",
|
| unlimited_membership: " . ($unlimited_membership ? 'true' : 'false') . "
|
| };
|
|
|
| // Also set global for backward compatibility, but safely
|
| if (!window.gymCalendar) {
|
| window.gymCalendar = this.config;
|
| }
|
|
|
| this.setupEventHandlers($);
|
| this.initialized = true;
|
|
|
| console.log('Calendar System: Successfully initialized');
|
|
|
| } catch (error) {
|
| console.error('Calendar System setup error:', error);
|
| this.fallbackInit();
|
| }
|
| },
|
|
|
| // Setup event handlers with error protection
|
| setupEventHandlers: function($) {
|
| const self = this;
|
|
|
| try {
|
| // Navigation button clicks with error handling
|
| $(document).on('click', '.prev-month, .next-month', function(e) {
|
| try {
|
| e.preventDefault();
|
| const year = $(this).data('year');
|
| const month = $(this).data('month');
|
| self.loadCalendarMonth(year, month, $);
|
| } catch (error) {
|
| console.error('Navigation click error:', error);
|
| }
|
| });
|
|
|
| // Class item clicks with error protection
|
| $(document).on('click', '.class-item', function(e) {
|
| try {
|
| self.handleClassClick(e, $(this), $);
|
| } catch (error) {
|
| console.error('Class click error:', error);
|
| }
|
| });
|
|
|
| // Modal event handlers with error protection
|
| $(document).on('click', '#confirm-booking', function() {
|
| try {
|
| self.handleBookingConfirm($(this), $);
|
| } catch (error) {
|
| console.error('Booking confirm error:', error);
|
| }
|
| });
|
|
|
| // Other modal handlers
|
| $(document).on('change', 'input[name=\"booking-type\"]', function() {
|
| try {
|
| if ($(this).val() === 'recurring') {
|
| $('#weeks-selection').show();
|
| } else {
|
| $('#weeks-selection').hide();
|
| }
|
| } catch (error) {
|
| console.error('Booking type change error:', error);
|
| }
|
| });
|
|
|
| } catch (error) {
|
| console.error('Event handler setup error:', error);
|
| }
|
| },
|
|
|
| // Load calendar month with error handling
|
| loadCalendarMonth: function(year, month, $) {
|
| try {
|
| const container = $('#gym-calendar-container');
|
| if (container.length === 0) {
|
| console.error('Calendar container not found');
|
| return;
|
| }
|
|
|
| container.html('<div style=\"text-align: center; padding: 40px;\">Loading...</div>');
|
|
|
| $.ajax({
|
| url: this.config.ajaxurl,
|
| type: 'POST',
|
| data: {
|
| action: 'gym_calendar_action',
|
| calendar_action: 'load_month',
|
| year: year,
|
| month: month,
|
| nonce: this.config.nonce
|
| },
|
| success: function(response) {
|
| try {
|
| if (response.success) {
|
| container.html(response.data.html);
|
| } else {
|
| container.html('<div style=\"text-align: center; padding: 40px; color: red;\">Error loading calendar: ' + (response.data || 'Unknown error') + '</div>');
|
| }
|
| } catch (error) {
|
| console.error('AJAX success handler error:', error);
|
| container.html('<div style=\"text-align: center; padding: 40px; color: red;\">Error processing response</div>');
|
| }
|
| },
|
| error: function(xhr, status, error) {
|
| console.error('AJAX request failed:', error);
|
| container.html('<div style=\"text-align: center; padding: 40px; color: red;\">Failed to load calendar. Please refresh the page.</div>');
|
| }
|
| });
|
|
|
| } catch (error) {
|
| console.error('loadCalendarMonth error:', error);
|
| }
|
| },
|
|
|
| // Handle class item clicks with error protection
|
| handleClassClick: function(e, element, $) {
|
| try {
|
| e.preventDefault();
|
| e.stopPropagation();
|
|
|
| if (element.hasClass('full')) {
|
| this.showNotificationModal('Class Full', 'This class is currently full. Please try another class or check back later.', $);
|
| return;
|
| }
|
|
|
| if (element.hasClass('holiday')) {
|
| console.log('Holiday hover - cursor should remain default, no color change');
|
| return;
|
| }
|
|
|
| if (!this.config.is_user_logged_in) {
|
| this.showNotificationModal('Login Required', 'Please log in to register for classes.', $);
|
| return;
|
| }
|
|
|
| // Check credit availability (skip for unlimited membership)
|
| if (!this.config.unlimited_membership && this.config.user_credits < 1) {
|
| this.showNotificationModal('Insufficient Credits', 'You don\\'t have enough credits to register for this class. Please purchase more credits or upgrade to an unlimited membership.', $);
|
| return;
|
| }
|
|
|
| const instanceId = element.data('instance-id');
|
| const className = element.data('class-name');
|
| const startTime = element.data('start-time');
|
| const endTime = element.data('end-time');
|
| const capacity = element.data('capacity');
|
| const isRegistered = element.data('is-registered');
|
| const registrationId = element.data('registration-id');
|
| const date = element.closest('.calendar-day').data('date');
|
|
|
| $('#modal-class-title').text(isRegistered ? 'Reserved Spot' : className);
|
|
|
| if (isRegistered) {
|
| $('#modal-class-details').html('<p>You have a reserved spot in this class. You will not be charged until you arrive at the Gym. If you cannot make this class, kindly let us know. Thanks!</p>');
|
| $('#modal-booking-options').hide();
|
| $('#confirm-booking').text('OK').data('action', 'acknowledge').data('instance-id', instanceId);
|
| } else {
|
| const startFormatted = this.formatTime(startTime);
|
| const endFormatted = this.formatTime(endTime);
|
|
|
| // Calculate actual duration in minutes
|
| const durationMinutes = this.calculateDuration(startTime, endTime);
|
|
|
| const current = element.data('current');
|
| const max = capacity;
|
| const remaining = max - current;
|
|
|
| $('#modal-class-details').html(
|
| '<div class=\"class-detail-item\">' +
|
| '<div class=\"class-detail-label\">Time:</div>' +
|
| '<div class=\"class-detail-value\">' + startFormatted + ' - ' + endFormatted + '</div>' +
|
| '</div>' +
|
| '<div class=\"class-detail-item\">' +
|
| '<div class=\"class-detail-label\">Remaining spots available:</div>' +
|
| '<div class=\"class-detail-value\">' + remaining + '</div>' +
|
| '</div>'
|
| );
|
|
|
| // Show registration options using template approach
|
| var registrationTemplate = document.createElement('div');
|
| registrationTemplate.innerHTML = [
|
| '<h4>Registration Options</h4>',
|
| '<div class=\"registration-options\">',
|
| '<label class=\"registration-label\">',
|
| '<input type=\"radio\" name=\"booking-type\" value=\"single\" checked class=\"registration-radio\">',
|
| 'Single Class (' + durationMinutes + ' minutes)',
|
| '</label>',
|
| '<label class=\"registration-label\">',
|
| '<input type=\"radio\" name=\"booking-type\" value=\"recurring\" class=\"registration-radio\">',
|
| 'Multiple Weeks',
|
| '</label>',
|
| '</div>',
|
| '<div id=\"weeks-selection\" class=\"weeks-selection\">',
|
| '<label for=\"weeks-count\" class=\"weeks-count-label\">Number of weeks:</label>',
|
| '<select id=\"weeks-count\" class=\"weeks-count-select\">',
|
| '<option value=\"1\">1 week</option>',
|
| '<option value=\"2\">2 weeks</option>',
|
| '<option value=\"3\">3 weeks</option>',
|
| '<option value=\"4\">4 weeks</option>',
|
| '<option value=\"5\">5 weeks</option>',
|
| '<option value=\"6\">6 weeks</option>',
|
| '<option value=\"7\">7 weeks</option>',
|
| '<option value=\"8\">8 weeks</option>',
|
| '<option value=\"9\">9 weeks</option>',
|
| '<option value=\"10\">10 weeks</option>',
|
| '<option value=\"11\">11 weeks</option>',
|
| '<option value=\"12\">12 weeks</option>',
|
| '</select>',
|
| '</div>'
|
| ].join('');
|
|
|
| $('#modal-booking-options').html(registrationTemplate.innerHTML).show();
|
|
|
| $('#confirm-booking').text('Register for Class').data('action', 'register').data('instance-id', instanceId);
|
| }
|
|
|
| $('body').addClass('modal-open');
|
| $('#booking-modal').fadeIn(300);
|
|
|
| } catch (error) {
|
| console.error('handleClassClick error:', error);
|
| }
|
| },
|
|
|
| // Handle booking confirmation with error protection
|
| handleBookingConfirm: function(button, $) {
|
| try {
|
| const action = button.data('action');
|
| const instanceId = button.data('instance-id');
|
|
|
| if (action === 'acknowledge') {
|
| // Simply close the modal for acknowledgment
|
| this.closeModal($);
|
| return;
|
| }
|
|
|
| if (action === 'register') {
|
| const bookingType = $('input[name=\"booking-type\"]:checked').val();
|
|
|
| if (bookingType === 'recurring') {
|
| const weeksCount = parseInt($('#weeks-count').val());
|
| this.registerForMultipleWeeks(instanceId, weeksCount, $);
|
| } else {
|
| this.registerForSingleClass(instanceId, $);
|
| }
|
| }
|
|
|
| } catch (error) {
|
| console.error('handleBookingConfirm error:', error);
|
| }
|
| },
|
|
|
| // Register for single class with error handling
|
| registerForSingleClass: function(instanceId, $) {
|
| try {
|
| const self = this;
|
|
|
| $.ajax({
|
| url: this.config.ajaxurl,
|
| type: 'POST',
|
| data: {
|
| action: 'gym_calendar_action',
|
| calendar_action: 'register',
|
| instance_id: instanceId,
|
| nonce: this.config.nonce
|
| },
|
| success: function(response) {
|
| try {
|
| if (response.success) {
|
| self.showNotificationModal('Registration Successful', response.data.message, $);
|
| // Reload current month to update display
|
| const currentDate = new Date();
|
| self.loadCalendarMonth(currentDate.getFullYear(), currentDate.getMonth() + 1, $);
|
| } else {
|
| self.showNotificationModal('Registration Failed', response.data || 'An error occurred during registration.', $);
|
| }
|
| } catch (error) {
|
| console.error('Registration success handler error:', error);
|
| self.showNotificationModal('Error', 'An unexpected error occurred.', $);
|
| }
|
| },
|
| error: function(xhr, status, error) {
|
| console.error('Registration AJAX error:', error);
|
| self.showNotificationModal('Error', 'Failed to register for class. Please try again.', $);
|
| }
|
| });
|
|
|
| } catch (error) {
|
| console.error('registerForSingleClass error:', error);
|
| }
|
| },
|
|
|
| // Register for multiple weeks with error handling
|
| registerForMultipleWeeks: function(instanceId, weeksCount, $) {
|
| try {
|
| const self = this;
|
|
|
| $.ajax({
|
| url: this.config.ajaxurl,
|
| type: 'POST',
|
| data: {
|
| action: 'gym_calendar_action',
|
| calendar_action: 'register_multiple',
|
| instance_id: instanceId,
|
| weeks_count: weeksCount,
|
| nonce: this.config.nonce
|
| },
|
| success: function(response) {
|
| try {
|
| if (response.success) {
|
| self.showNotificationModal('Multi-Week Registration Successful', response.data.message, $);
|
| // Reload current month to update display
|
| const currentDate = new Date();
|
| self.loadCalendarMonth(currentDate.getFullYear(), currentDate.getMonth() + 1, $);
|
| } else {
|
| self.showNotificationModal('Registration Failed', response.data || 'An error occurred during multi-week registration.', $);
|
| }
|
| } catch (error) {
|
| console.error('Multi-week registration success handler error:', error);
|
| self.showNotificationModal('Error', 'An unexpected error occurred.', $);
|
| }
|
| },
|
| error: function(xhr, status, error) {
|
| console.error('Multi-week registration AJAX error:', error);
|
| self.showNotificationModal('Error', 'Failed to register for multiple weeks. Please try again.', $);
|
| }
|
| });
|
|
|
| } catch (error) {
|
| console.error('registerForMultipleWeeks error:', error);
|
| }
|
| },
|
|
|
| // Show notification modal with error protection
|
| showNotificationModal: function(title, message, $) {
|
| try {
|
| $('#notification-title').text(title);
|
| $('#notification-message').html(message);
|
| this.closeModal($);
|
| $('#notification-modal').fadeIn(300);
|
|
|
| // Auto-close notification modal after clicking OK
|
| $('#notification-ok').off('click').on('click', function() {
|
| $('#notification-modal').fadeOut(300);
|
| $('body').removeClass('modal-open');
|
| });
|
|
|
| // Close on background click
|
| $('#notification-modal').off('click').on('click', function(e) {
|
| if (e.target === this) {
|
| $(this).fadeOut(300);
|
| $('body').removeClass('modal-open');
|
| }
|
| });
|
|
|
| } catch (error) {
|
| console.error('showNotificationModal error:', error);
|
| }
|
| },
|
|
|
| // Close modal with error protection
|
| closeModal: function($) {
|
| try {
|
| $('#booking-modal').fadeOut(300);
|
| $('body').removeClass('modal-open');
|
| } catch (error) {
|
| console.error('closeModal error:', error);
|
| }
|
| },
|
|
|
| // Setup basic event handlers for fallback mode
|
| setupBasicEventHandlers: function() {
|
| try {
|
| // Basic modal close functionality without jQuery
|
| document.addEventListener('click', function(e) {
|
| if (e.target.classList.contains('modal-close')) {
|
| const modal = e.target.closest('.modal-overlay');
|
| if (modal) {
|
| modal.style.display = 'none';
|
| document.body.classList.remove('modal-open');
|
| }
|
| }
|
| });
|
|
|
| console.log('Calendar System: Basic event handlers setup complete');
|
| } catch (error) {
|
| console.error('setupBasicEventHandlers error:', error);
|
| }
|
| },
|
|
|
| // Utility functions with error protection
|
| calculateDuration: function(startTime, endTime) {
|
| try {
|
| const start = this.timeToMinutes(startTime);
|
| const end = this.timeToMinutes(endTime);
|
| return Math.max(0, end - start);
|
| } catch (error) {
|
| console.error('calculateDuration error:', error);
|
| return 60; // Default fallback
|
| }
|
| },
|
|
|
| formatTime: function(timeStr) {
|
| try {
|
| const [hours, minutes] = timeStr.split(':').map(Number);
|
| const date = new Date();
|
| date.setHours(hours, minutes);
|
| return date.toLocaleTimeString('en-US', {
|
| hour: 'numeric',
|
| minute: '2-digit',
|
| hour12: true
|
| });
|
| } catch (error) {
|
| return timeStr;
|
| }
|
| },
|
|
|
| timeToMinutes: function(timeStr) {
|
| try {
|
| const [hours, minutes] = timeStr.split(':').map(Number);
|
| return hours * 60 + minutes;
|
| } catch (error) {
|
| return 0;
|
| }
|
| }
|
| };
|
|
|
| // Initialize calendar system
|
| if (document.readyState === 'loading') {
|
| document.addEventListener('DOMContentLoaded', function() {
|
| CalendarSystem.init();
|
| });
|
| } else {
|
| CalendarSystem.init();
|
| }
|
|
|
| })();
|
|
|
| // Legacy compatibility - remove old duplicate handlers
|
| if (typeof loadCalendarMonth !== 'undefined') {
|
| delete window.loadCalendarMonth;
|
| }
|
|
|
| </script>";
|
| }
|
|
|
|
|
|
|
|
|
| private function isUserRegistered($instance_id, $user_classes) {
|
| foreach ($user_classes as $class) {
|
| if ($class['instance_id'] == $instance_id) {
|
| return true;
|
| }
|
| }
|
| return false;
|
| }
|
|
|
|
|
|
|
|
|
| private function getUserRegistration($instance_id, $user_classes) {
|
| foreach ($user_classes as $class) {
|
| if ($class['instance_id'] == $instance_id) {
|
| return $class;
|
| }
|
| }
|
| return null;
|
| }
|
|
|
|
|
|
|
|
|
| public function handleAjaxRequest() {
|
|
|
| if (!wp_verify_nonce($_POST['nonce'], 'gym_calendar_nonce')) {
|
| wp_send_json_error('Invalid nonce');
|
| return;
|
| }
|
|
|
| $calendar_action = sanitize_text_field($_POST['calendar_action']);
|
|
|
| switch ($calendar_action) {
|
| case 'load_month':
|
| $this->handleLoadMonth();
|
| break;
|
|
|
| case 'register':
|
| $this->handleRegistration();
|
| break;
|
|
|
| case 'register_multiple':
|
| $this->handleMultipleWeekRegistration();
|
| break;
|
|
|
| case 'test_email':
|
| $this->handleTestEmail();
|
| break;
|
|
|
| default:
|
| wp_send_json_error('Invalid action');
|
| }
|
| }
|
|
|
| private function handleLoadMonth() {
|
| $year = intval($_POST['year']);
|
| $month = intval($_POST['month']);
|
|
|
| if ($year < 2020 || $year > 2030 || $month < 1 || $month > 12) {
|
| wp_send_json_error('Invalid date');
|
| return;
|
| }
|
|
|
|
|
| $classes = $this->calendar_core->getCalendarData($year, $month);
|
|
|
|
|
| $user_classes = [];
|
| if (is_user_logged_in()) {
|
| $user_id = get_current_user_id();
|
| $start_date = date('Y-m-01', mktime(0, 0, 0, $month, 1, $year));
|
| $end_date = date('Y-m-t', mktime(0, 0, 0, $month, 1, $year));
|
| $user_classes = $this->calendar_core->getUserRegistrations($user_id, $start_date, $end_date);
|
| }
|
|
|
|
|
| $html = $this->generateCalendarHTML($year, $month, $classes, $user_classes);
|
|
|
| wp_send_json_success(['html' => $html]);
|
| }
|
|
|
| private function handleRegistration() {
|
| if (!is_user_logged_in()) {
|
| wp_send_json_error('Must be logged in to register');
|
| return;
|
| }
|
|
|
| $instance_id = intval($_POST['instance_id']);
|
| $user_id = get_current_user_id();
|
|
|
| $result = $this->calendar_core->registerUserForClass($user_id, $instance_id);
|
|
|
| if ($result['success']) {
|
| wp_send_json_success($result);
|
| } else {
|
| wp_send_json_error($result['message']);
|
| }
|
| }
|
|
|
| private function handleMultipleWeekRegistration() {
|
| if (!is_user_logged_in()) {
|
| wp_send_json_error('Must be logged in to register');
|
| return;
|
| }
|
|
|
| $instance_id = intval($_POST['instance_id']);
|
| $weeks_count = intval($_POST['weeks_count']);
|
| $user_id = get_current_user_id();
|
|
|
| if ($weeks_count < 1 || $weeks_count > 12) {
|
| wp_send_json_error('Invalid weeks count');
|
| return;
|
| }
|
|
|
| $result = $this->calendar_core->registerUserForMultipleWeeks($user_id, $instance_id, $weeks_count);
|
|
|
| if ($result['success']) {
|
| wp_send_json_success($result);
|
| } else {
|
| wp_send_json_error($result['message']);
|
| }
|
| }
|
|
|
| private function handleTestEmail() {
|
| if (!current_user_can('administrator')) {
|
| wp_send_json_error('Insufficient permissions');
|
| return;
|
| }
|
|
|
| $test_email = sanitize_email($_POST['test_email'] ?? '');
|
| if (!$test_email) {
|
| $user = wp_get_current_user();
|
| $test_email = $user->user_email;
|
| }
|
|
|
| $result = $this->calendar_core->testEmailSystem($test_email);
|
|
|
| if ($result['success']) {
|
| wp_send_json_success($result['message']);
|
| } else {
|
| wp_send_json_error($result['message']);
|
| }
|
| }
|
| }
|
|
|
|
|
| new GymCalendarShortcode();
|
| |
| |
Comments