query("SELECT * FROM platform_profile WHERE id = 1"); $profile = $stmt->fetch(PDO::FETCH_ASSOC); if (!$profile) { $profile = ['name' => 'LMS Platform', 'description' => '', 'logo_path' => '', 'favicon_path' => '']; } } return $profile; } function app_name(): string { $prof = get_platform_profile(); if (!empty($prof['name'])) { return trim($prof['name']); } return trim((string) ($_SERVER['PROJECT_NAME'] ?? 'Aula')) ?: 'Aula'; } function project_description(): string { return (string) ($_SERVER['PROJECT_DESCRIPTION'] ?? 'Modern multilingual e-learning classrooms with subscriptions, teachers, students, Google Meet live sessions, Thawani billing, and WhatsApp notifications.'); } function get_landing_settings(): array { static $settings = null; if ($settings === null) { $settings = []; try { $stmt = db()->query("SELECT setting_key, value_en, value_ar FROM landing_settings"); while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) { $settings[$row['setting_key']] = $row; } } catch (Exception $e) { } } return $settings; } function landing_setting(string $key, string $default_en, string $default_ar = ""): string { $settings = get_landing_settings(); $lang = current_lang(); if (isset($settings[$key])) { $val = $lang === 'ar' ? $settings[$key]['value_ar'] : $settings[$key]['value_en']; if (trim((string)$val) !== '') { return $val; } } return $lang === 'ar' ? $default_ar : $default_en; } function current_lang(): string { $lang = $_GET['lang'] ?? $_SESSION['lang'] ?? 'ar'; $lang = in_array($lang, ['en', 'ar'], true) ? $lang : 'ar'; $_SESSION['lang'] = $lang; return $lang; } function is_rtl(): bool { return current_lang() === 'ar'; } function t(string $en, string $ar): string { return current_lang() === 'ar' ? $ar : $en; } function asset_url(string $path): string { $full = __DIR__ . '/../' . ltrim($path, '/'); $version = is_file($full) ? (string) filemtime($full) : (string) time(); return ltrim($path, '/') . '?v=' . $version; } function app_url(string $path, array $params = []): string { if (!isset($params['lang'])) { $params['lang'] = current_lang(); } $query = http_build_query($params); return $path . ($query ? '?' . $query : ''); } function page_lang_link(string $lang): string { $params = $_GET; $params['lang'] = $lang; $query = http_build_query($params); return basename($_SERVER['PHP_SELF']) . ($query ? '?' . $query : ''); } function subjects_catalog_static(): array { return [ 'english-fluency' => [ 'slug' => 'english-fluency', 'title_en' => 'English Fluency Studio', 'title_ar' => 'استوديو الطلاقة في الإنجليزية', 'summary_en' => 'Speaking-first classrooms for teens and adults with weekly live workshops.', 'summary_ar' => 'فصول تركّز على المحادثة للمراهقين والبالغين مع ورش مباشرة أسبوعية.', 'teacher_en' => 'Sarah Coleman', 'teacher_ar' => 'سارة كولمان', 'level_en' => 'Intermediate', 'level_ar' => 'متوسط', 'duration_en' => '12 weeks', 'duration_ar' => '12 أسبوعاً', 'next_live_en' => 'Wednesday, April 8, 2026 · 6:00 PM UTC', 'next_live_ar' => 'الأربعاء 8 أبريل 2026 · 6:00 مساءً UTC', 'meet_url' => 'https://meet.google.com/', 'modules_en' => ['Live speaking labs', 'Pronunciation clinic', 'Writing feedback', 'Weekly vocabulary sprints'], 'modules_ar' => ['مختبرات محادثة مباشرة', 'عيادة النطق', 'ملاحظات على الكتابة', 'دفعات مفردات أسبوعية'], ], 'stem-lab' => [ 'slug' => 'stem-lab', 'title_en' => 'STEM Lab Foundations', 'title_ar' => 'أساسيات مختبر STEM', 'summary_en' => 'Math and science pathways with assignments, quizzes, and teacher office hours.', 'summary_ar' => 'مسارات في الرياضيات والعلوم مع واجبات واختبارات وساعات مكتبية للمعلم.', 'teacher_en' => 'Omar Al-Harthy', 'teacher_ar' => 'عمر الحارثي', 'level_en' => 'Beginner to Advanced', 'level_ar' => 'من مبتدئ إلى متقدم', 'duration_en' => '16 weeks', 'duration_ar' => '16 أسبوعاً', 'next_live_en' => 'Thursday, April 9, 2026 · 5:00 PM UTC', 'next_live_ar' => 'الخميس 9 أبريل 2026 · 5:00 مساءً UTC', 'meet_url' => 'https://meet.google.com/', 'modules_en' => ['Algebra tracks', 'Physics demos', 'Guided worksheets', 'Lab challenge reviews'], 'modules_ar' => ['مسارات الجبر', 'عروض فيزياء', 'أوراق عمل موجهة', 'مراجعات تحديات المختبر'], ], 'arabic-academy' => [ 'slug' => 'arabic-academy', 'title_en' => 'Arabic Language Academy', 'title_ar' => 'أكاديمية اللغة العربية', 'summary_en' => 'Modern Standard Arabic with reading circles and live grammar sessions.', 'summary_ar' => 'العربية الفصحى مع حلقات قراءة وجلسات قواعد مباشرة.', 'teacher_en' => 'Maha Al-Rashdi', 'teacher_ar' => 'مها الراشدي', 'level_en' => 'All levels', 'level_ar' => 'جميع المستويات', 'duration_en' => '10 weeks', 'duration_ar' => '10 أسابيع', 'next_live_en' => 'Saturday, April 11, 2026 · 4:00 PM UTC', 'next_live_ar' => 'السبت 11 أبريل 2026 · 4:00 مساءً UTC', 'meet_url' => 'https://meet.google.com/', 'modules_en' => ['Reading comprehension', 'Live grammar board', 'Listening practice', 'Culture sessions'], 'modules_ar' => ['فهم المقروء', 'سبورة قواعد مباشرة', 'تدريب الاستماع', 'جلسات ثقافية'], ], 'design-systems' => [ 'slug' => 'design-systems', 'title_en' => 'Design Systems for Creators', 'title_ar' => 'أنظمة التصميم للمبدعين', 'summary_en' => 'UI foundations, product thinking, and portfolio reviews in live cohorts.', 'summary_ar' => 'أساسيات الواجهات والتفكير المنتج ومراجعات المحافظ ضمن مجموعات مباشرة.', 'teacher_en' => 'Lina Mercer', 'teacher_ar' => 'لينا ميرسر', 'level_en' => 'Professional', 'level_ar' => 'احترافي', 'duration_en' => '8 weeks', 'duration_ar' => '8 أسابيع', 'next_live_en' => 'Monday, April 13, 2026 · 7:00 PM UTC', 'next_live_ar' => 'الإثنين 13 أبريل 2026 · 7:00 مساءً UTC', 'meet_url' => 'https://meet.google.com/', 'modules_en' => ['UI critique rooms', 'Systems thinking', 'Component audits', 'Portfolio feedback'], 'modules_ar' => ['غرف نقد الواجهات', 'تفكير الأنظمة', 'تدقيق المكونات', 'ملاحظات على المحفظة'], ], ]; } function plans_catalog_static(): array { return [ 'core' => [ 'key' => 'core', 'name_en' => 'Core Plan', 'name_ar' => 'الخطة الأساسية', 'price_monthly' => 29, 'price_yearly' => 290, 'subjects_limit' => 2, 'features_en' => ['Access to 2 subjects', 'Weekly live classroom', 'Student dashboard', 'Thawani-ready billing'], 'features_ar' => ['الوصول إلى مادتين', 'فصل مباشر أسبوعي', 'لوحة الطالب', 'دفع جاهز لثواني'], ], 'plus' => [ 'key' => 'plus', 'name_en' => 'Plus Plan', 'name_ar' => 'الخطة المتقدمة', 'price_monthly' => 59, 'price_yearly' => 590, 'subjects_limit' => 4, 'features_en' => ['All subjects', 'Unlimited live rooms', 'Teacher Q&A', 'WhatsApp reminders'], 'features_ar' => ['جميع المواد', 'غرف مباشرة غير محدودة', 'أسئلة وأجوبة مع المعلم', 'تذكيرات عبر واتساب'], ], 'pro' => [ 'key' => 'pro', 'name_en' => 'Pro Campus', 'name_ar' => 'الخطة الاحترافية', 'price_monthly' => 89, 'price_yearly' => 890, 'subjects_limit' => 999, 'features_en' => ['Priority support', 'Arabic + English tracks', 'Admin reporting', 'Live classroom operations'], 'features_ar' => ['دعم أولوية', 'مسارات عربية وإنجليزية', 'تقارير إدارية', 'تشغيل الفصول المباشرة'], ], ]; } function ensure_catalog_tables(): void { db()->exec(" CREATE TABLE IF NOT EXISTS subjects ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, slug VARCHAR(100) UNIQUE NOT NULL, title_en VARCHAR(255) NOT NULL, title_ar VARCHAR(255) NOT NULL, summary_en TEXT, summary_ar TEXT, teacher_en VARCHAR(120), teacher_ar VARCHAR(120), level_en VARCHAR(50), level_ar VARCHAR(50), duration_en VARCHAR(50), duration_ar VARCHAR(50), next_live_en VARCHAR(120), next_live_ar VARCHAR(120), meet_url VARCHAR(255), modules_en JSON, modules_ar JSON, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS plans ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, plan_key VARCHAR(50) UNIQUE NOT NULL, name_en VARCHAR(120) NOT NULL, name_ar VARCHAR(120) NOT NULL, price_monthly DECIMAL(10,3) NOT NULL, price_yearly DECIMAL(10,3) NOT NULL, subjects_limit INT DEFAULT 1, features_en JSON, features_ar JSON ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; "); $count = db()->query("SELECT COUNT(*) FROM subjects")->fetchColumn(); if ($count == 0) { $subjects = subjects_catalog_static(); $stmt = db()->prepare("INSERT INTO subjects (slug, title_en, title_ar, summary_en, summary_ar, teacher_en, teacher_ar, level_en, level_ar, duration_en, duration_ar, next_live_en, next_live_ar, meet_url, modules_en, modules_ar) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); foreach ($subjects as $s) { $stmt->execute([ $s['slug'], $s['title_en'], $s['title_ar'], $s['summary_en'], $s['summary_ar'], $s['teacher_en'], $s['teacher_ar'], $s['level_en'], $s['level_ar'], $s['duration_en'], $s['duration_ar'], $s['next_live_en'], $s['next_live_ar'], $s['meet_url'], json_encode($s['modules_en']), json_encode($s['modules_ar']) ]); } } $count = db()->query("SELECT COUNT(*) FROM plans")->fetchColumn(); if ($count == 0) { $plans = plans_catalog_static(); $stmt = db()->prepare("INSERT INTO plans (plan_key, name_en, name_ar, price_monthly, price_yearly, subjects_limit, features_en, features_ar) VALUES (?, ?, ?, ?, ?, ?, ?, ?)"); foreach ($plans as $p) { $stmt->execute([ $p['key'], $p['name_en'], $p['name_ar'], $p['price_monthly'], $p['price_yearly'], $p['subjects_limit'], json_encode($p['features_en']), json_encode($p['features_ar']) ]); } } } function subjects_catalog(): array { ensure_catalog_tables(); $stmt = db()->query("SELECT s.* FROM subjects s LEFT JOIN classes c ON s.class_id = c.id WHERE s.status = 'active' AND (s.class_id IS NULL OR s.class_id = 0 OR c.status = 'active')"); $rows = $stmt->fetchAll(); $result = []; foreach ($rows as $row) { $row['modules_en'] = json_decode($row['modules_en'] ? (string)$row['modules_en'] : '[]', true) ?: []; $row['modules_ar'] = json_decode($row['modules_ar'] ? (string)$row['modules_ar'] : '[]', true) ?: []; $class_id = (int)$row['class_id']; $subject_id = (int)$row['id']; // Dynamically fetch active assigned teacher $teacher_stmt = db()->prepare( "SELECT t.name FROM teacher_assignments ta JOIN teachers t ON ta.teacher_id = t.id WHERE t.status = 'active' AND ta.class_id = ? AND (ta.subject_id = ? OR ta.subject_id = 0) LIMIT 1" ); $teacher_stmt->execute([$class_id, $subject_id]); $teacher_name = $teacher_stmt->fetchColumn(); if ($teacher_name) { $row['teacher_en'] = $teacher_name; $row['teacher_ar'] = $teacher_name; } else { // Check if there is an inactive teacher assigned. If so, override with empty or "No Teacher". $inactive_stmt = db()->prepare( "SELECT t.name FROM teacher_assignments ta JOIN teachers t ON ta.teacher_id = t.id WHERE t.status = 'inactive' AND ta.class_id = ? AND (ta.subject_id = ? OR ta.subject_id = 0) LIMIT 1" ); $inactive_stmt->execute([$class_id, $subject_id]); if ($inactive_stmt->fetchColumn()) { $row['teacher_en'] = 'No Active Teacher'; $row['teacher_ar'] = 'لا يوجد معلم نشط'; } } $result[$row['slug']] = $row; } return $result; } function plans_catalog(): array { ensure_catalog_tables(); $stmt = db()->query('SELECT * FROM plans'); $rows = $stmt->fetchAll(); $result = []; foreach ($rows as $row) { $row['key'] = $row['plan_key']; $row['features_en'] = json_decode($row['features_en'] ?? '[]', true) ?: []; $row['features_ar'] = json_decode($row['features_ar'] ?? '[]', true) ?: []; $result[$row['key']] = $row; } return $result; } function ensure_progress_table(): void { db()->exec(" CREATE TABLE IF NOT EXISTS module_progress ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, subscription_id INT UNSIGNED NOT NULL, subject_slug VARCHAR(100) NOT NULL, module_index INT UNSIGNED NOT NULL, completed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY(subscription_id, subject_slug, module_index) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; "); } function get_completed_modules(int $subscriptionId, string $subjectSlug): array { ensure_progress_table(); $stmt = db()->prepare("SELECT module_index FROM module_progress WHERE subscription_id = ? AND subject_slug = ?"); $stmt->execute([$subscriptionId, $subjectSlug]); return $stmt->fetchAll(PDO::FETCH_COLUMN) ?: []; } function toggle_module_progress(int $subscriptionId, string $subjectSlug, int $moduleIndex, bool $completed): void { ensure_progress_table(); if ($completed) { $stmt = db()->prepare("INSERT IGNORE INTO module_progress (subscription_id, subject_slug, module_index) VALUES (?, ?, ?)"); $stmt->execute([$subscriptionId, $subjectSlug, $moduleIndex]); } else { $stmt = db()->prepare("DELETE FROM module_progress WHERE subscription_id = ? AND subject_slug = ? AND module_index = ?"); $stmt->execute([$subscriptionId, $subjectSlug, $moduleIndex]); } } function get_subject(string $slug): ?array { $subjects = subjects_catalog(); return $subjects[$slug] ?? null; } function get_plan(string $key): ?array { $plans = plans_catalog(); return $plans[$key] ?? null; } function plan_name(array $plan): string { return current_lang() === 'ar' ? $plan['name_ar'] : $plan['name_en']; } function subject_title(array $subject): string { return current_lang() === 'ar' ? $subject['title_ar'] : $subject['title_en']; } function subject_summary(array $subject): string { return current_lang() === 'ar' ? $subject['summary_ar'] : $subject['summary_en']; } function subject_teacher(array $subject): string { return (string)(current_lang() === 'ar' ? ($subject['teacher_ar'] ?? '') : ($subject['teacher_en'] ?? '')); } function subject_level(array $subject): string { return (string)(current_lang() === 'ar' ? ($subject['level_ar'] ?? '') : ($subject['level_en'] ?? '')); } function subject_duration(array $subject): string { return (string)(current_lang() === 'ar' ? ($subject['duration_ar'] ?? '') : ($subject['duration_en'] ?? '')); } function subject_next_live(array $subject): string { return (string)(current_lang() === 'ar' ? ($subject['next_live_ar'] ?? '') : ($subject['next_live_en'] ?? '')); } function subject_modules(array $subject): array { return current_lang() === 'ar' ? $subject['modules_ar'] : $subject['modules_en']; } function format_price(float $amount): string { return number_format($amount, 3) . ' ' . t('OMR', 'ر.ع.'); } function price_label(array $plan, string $cycle = 'monthly'): string { $amount = $cycle === 'yearly' ? $plan['price_yearly'] : $plan['price_monthly']; $suffix = $cycle === 'yearly' ? t('/year', '/سنة') : t('/month', '/شهر'); return format_price((float)$amount) . ' ' . $suffix; } function ensure_subscription_table(): void { db()->exec( "CREATE TABLE IF NOT EXISTS student_subscriptions ( id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, full_name VARCHAR(120) NOT NULL, email VARCHAR(190) NOT NULL, whatsapp VARCHAR(40) NOT NULL, preferred_language VARCHAR(5) NOT NULL DEFAULT 'en', plan_key VARCHAR(20) NOT NULL, billing_cycle VARCHAR(20) NOT NULL DEFAULT 'monthly', payment_status VARCHAR(20) NOT NULL DEFAULT 'active', payment_gateway VARCHAR(30) NOT NULL DEFAULT 'Thawani', thawani_reference VARCHAR(80) NOT NULL, wablas_opt_in TINYINT(1) NOT NULL DEFAULT 0, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4" ); } function create_subscription(array $data): int { ensure_subscription_table(); $stmt = db()->prepare( 'INSERT INTO student_subscriptions (full_name, email, whatsapp, preferred_language, plan_key, billing_cycle, payment_status, payment_gateway, thawani_reference, wablas_opt_in) VALUES (:full_name, :email, :whatsapp, :preferred_language, :plan_key, :billing_cycle, :payment_status, :payment_gateway, :thawani_reference, :wablas_opt_in)' ); $stmt->bindValue(':full_name', $data['full_name']); $stmt->bindValue(':email', $data['email']); $stmt->bindValue(':whatsapp', $data['whatsapp']); $stmt->bindValue(':preferred_language', $data['preferred_language']); $stmt->bindValue(':plan_key', $data['plan_key']); $stmt->bindValue(':billing_cycle', $data['billing_cycle']); $stmt->bindValue(':payment_status', $data['payment_status']); $stmt->bindValue(':payment_gateway', $data['payment_gateway']); $stmt->bindValue(':thawani_reference', $data['thawani_reference']); $stmt->bindValue(':wablas_opt_in', $data['wablas_opt_in'], PDO::PARAM_INT); $stmt->execute(); return (int) db()->lastInsertId(); } function fetch_subscription(int $id): ?array { ensure_subscription_table(); $stmt = db()->prepare('SELECT * FROM student_subscriptions WHERE id = :id LIMIT 1'); $stmt->bindValue(':id', $id, PDO::PARAM_INT); $stmt->execute(); $row = $stmt->fetch(); return $row ?: null; } function fetch_subscriptions_by_email(string $email): array { ensure_subscription_table(); $stmt = db()->prepare('SELECT * FROM student_subscriptions WHERE email = :email ORDER BY created_at DESC'); $stmt->bindValue(':email', $email); $stmt->execute(); return $stmt->fetchAll(); } function fetch_recent_subscriptions(int $limit = 8): array { ensure_subscription_table(); $stmt = db()->prepare('SELECT * FROM student_subscriptions ORDER BY created_at DESC LIMIT :limit'); $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->execute(); return $stmt->fetchAll(); } function subscription_metrics(): array { ensure_subscription_table(); $metrics = [ 'total' => 0, 'arabic' => 0, 'active' => 0, 'latest' => null, ]; $totals = db()->query("SELECT COUNT(*) AS total, SUM(preferred_language = 'ar') AS arabic, SUM(payment_status = 'active') AS active FROM student_subscriptions")->fetch(); if ($totals) { $metrics['total'] = (int) ($totals['total'] ?? 0); $metrics['arabic'] = (int) ($totals['arabic'] ?? 0); $metrics['active'] = (int) ($totals['active'] ?? 0); } $latest = db()->query('SELECT * FROM student_subscriptions ORDER BY created_at DESC LIMIT 1')->fetch(); $metrics['latest'] = $latest ?: null; return $metrics; } function current_subscription(): ?array { $id = isset($_GET['subscription_id']) ? (int) $_GET['subscription_id'] : (int) ($_SESSION['subscription_id'] ?? 0); if ($id > 0) { $row = fetch_subscription($id); if ($row) { $_SESSION['subscription_id'] = (int) $row['id']; $_SESSION['student_email'] = $row['email']; return $row; } } $email = (string) ($_SESSION['student_email'] ?? ''); if ($email !== '') { $rows = fetch_subscriptions_by_email($email); if ($rows) { $_SESSION['subscription_id'] = (int) $rows[0]['id']; return $rows[0]; } } return null; } function page_title(string $title): string { return $title . ' · ' . app_name(); } function render_head(string $title, string $description = ''): void { $pageDescription = $description !== '' ? $description : project_description(); $projectDescription = $_SERVER['PROJECT_DESCRIPTION'] ?? ''; $projectImageUrl = $_SERVER['PROJECT_IMAGE_URL'] ?? ''; ?>