From 21d2ed704dd6cf6e200c5a26b8006d81d44fee94 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 28 Aug 2026 04:19:47 +0300 Subject: [PATCH] Update Saqel Platform: 2026-08-28 04:19:47 --- backend/app/Views/StudentPortal.php | 2253 ++++++++++++--------------- backend/app/Views/TeacherPortal.php | 43 +- 2 files changed, 1013 insertions(+), 1283 deletions(-) diff --git a/backend/app/Views/StudentPortal.php b/backend/app/Views/StudentPortal.php index 2e78cf2..fac666b 100644 --- a/backend/app/Views/StudentPortal.php +++ b/backend/app/Views/StudentPortal.php @@ -2,11 +2,109 @@ namespace App\Views; +use App\Core\Database; +use App\Services\VideoService; +use App\Services\CurriculumService; +use App\Services\AiVideoAnalyzerService; + class StudentPortal { public static function render(): string { - return <<<'HTML' + VideoService::ensureSchema(); + CurriculumService::ensureSchema(); + + // 1. Fetch Real Published Lessons from MySQL + $lessons = Database::select( + "SELECT l.*, COALESCE(c.title, 'توجيهي 2008 — الفرع العلمي') as course_title, + (SELECT COUNT(*) FROM exams WHERE lesson_id = l.id AND scope = 'in_video_checkpoint') as checkpoints_count + FROM lessons l + LEFT JOIN courses c ON l.course_id = c.id + ORDER BY l.id DESC" + ); + + $activeLesson = !empty($lessons) ? $lessons[0] : null; + $checkpoints = []; + $chapters = []; + + if ($activeLesson) { + $lessonId = (int)$activeLesson['id']; + + // Ensure Socratic checkpoints and questions exist for active lesson + $existingCount = Database::selectOne("SELECT COUNT(*) as cnt FROM exams WHERE lesson_id = ? AND scope = 'in_video_checkpoint'", [$lessonId]); + $existingQuestions = Database::selectOne("SELECT COUNT(*) as cnt FROM questions q JOIN exams e ON q.exam_id = e.id WHERE e.lesson_id = ?", [$lessonId]); + if (empty($existingCount['cnt']) || empty($existingQuestions['cnt'])) { + AiVideoAnalyzerService::processLessonAutonomously($lessonId); + $activeLesson = Database::selectOne("SELECT * FROM lessons WHERE id = ? LIMIT 1", [$lessonId]); + } + + // Fetch Checkpoints with Questions and Options + $exams = Database::select( + "SELECT e.id as exam_id, e.uuid as exam_uuid, e.title, e.timestamp_seconds, e.rewind_on_fail_seconds, e.passing_percentage + FROM exams e + WHERE e.lesson_id = ? AND e.scope = 'in_video_checkpoint' AND e.is_published = 1 + ORDER BY e.timestamp_seconds ASC", + [$lessonId] + ); + + foreach ($exams as $ex) { + $q = Database::selectOne("SELECT id, question_text, explanation_text FROM questions WHERE exam_id = ? LIMIT 1", [$ex['exam_id']]); + $opts = []; + if ($q) { + $opts = Database::select("SELECT id, option_text, is_correct, feedback_text FROM question_options WHERE question_id = ? ORDER BY id ASC", [$q['id']]); + } + + $checkpoints[] = [ + 'exam_id' => (int)$ex['exam_id'], + 'timestamp_seconds' => (int)$ex['timestamp_seconds'], + 'rewind_on_fail_seconds' => (int)$ex['rewind_on_fail_seconds'], + 'question_text' => $q['question_text'] ?? 'سؤال فحص فهم الفكرة:', + 'explanation' => $q['explanation_text'] ?? '', + 'options' => array_map(function ($o) { + return [ + 'id' => (int)$o['id'], + 'text' => $o['option_text'], + 'is_correct' => (bool)$o['is_correct'], + ]; + }, $opts) + ]; + } + + if (!empty($activeLesson['timeline_chapters_json'])) { + $chapters = json_decode($activeLesson['timeline_chapters_json'], true) ?: []; + } + } + + // 2. Fetch Real Exams from MySQL + $dbExams = Database::select( + "SELECT e.*, COALESCE(c.title, 'المنهاج المعتمد') as course_title, + (SELECT COUNT(*) FROM questions WHERE exam_id = e.id) as questions_count + FROM exams e + LEFT JOIN courses c ON e.course_id = c.id + WHERE e.is_published = 1 + ORDER BY e.id DESC" + ); + + $examsWithQuestions = []; + foreach ($dbExams as $ex) { + $qs = Database::select("SELECT * FROM questions WHERE exam_id = ? ORDER BY id ASC", [$ex['id']]); + foreach ($qs as &$q) { + $q['options'] = Database::select("SELECT id, option_text, is_correct, feedback_text FROM question_options WHERE question_id = ? ORDER BY id ASC", [$q['id']]); + } + $ex['questions'] = $qs; + $examsWithQuestions[] = $ex; + } + + $serverDataJson = json_encode([ + 'lessons' => $lessons, + 'activeLesson' => $activeLesson, + 'activeCheckpoints' => $checkpoints, + 'activeChapters' => $chapters, + 'exams' => $examsWithQuestions, + ], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); + + ob_start(); + ?> @@ -198,186 +296,169 @@ class StudentPortal transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); box-shadow: 0 4px 20px rgba(0, 113, 227, 0.4); } - .btn-primary:hover { background: var(--accent-blue-hover); transform: scale(1.01); } - .btn-secondary { - background: none; - border: none; - color: var(--text-muted); - font-size: 13px; - cursor: pointer; - transition: color 0.2s; + .btn-primary:hover { + background: var(--accent-blue-hover); + transform: scale(1.015); } - .btn-secondary:hover { color: #FFF; } - /* Dashboard UI */ + /* Dashboard & Layout */ .dashboard-hero { - background: linear-gradient(135deg, rgba(18, 18, 20, 0.95), rgba(24, 24, 27, 0.8)); - border: 1px solid var(--border); - border-radius: 28px; - padding: 32px 36px; - margin: 32px 0 24px; + background: linear-gradient(135deg, rgba(0, 113, 227, 0.12) 0%, rgba(245, 158, 11, 0.06) 100%); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 24px; + padding: 28px 32px; + margin: 28px 0; display: flex; - align-items: center; justify-content: space-between; + align-items: center; flex-wrap: wrap; - gap: 24px; + gap: 20px; } .readiness-box { display: flex; align-items: center; gap: 16px; - background: rgba(0, 0, 0, 0.6); + background: rgba(0, 0, 0, 0.4); border: 1px solid var(--border); - border-radius: 20px; - padding: 16px 24px; + border-radius: 18px; + padding: 12px 20px; } .gauge-circle { - width: 58px; - height: 58px; + width: 52px; + height: 52px; border-radius: 50%; - background: radial-gradient(circle, rgba(0, 245, 212, 0.2) 0%, transparent 70%); - border: 3px solid var(--accent-cyan); + background: conic-gradient(var(--accent-cyan) 92%, rgba(255,255,255,0.1) 0); display: flex; align-items: center; justify-content: center; - font-size: 18px; + font-size: 12px; font-weight: 900; color: #FFFFFF; - box-shadow: 0 0 16px rgba(0, 245, 212, 0.3); - } - - /* Tabs Nav */ - .tabs-nav { - display: flex; - gap: 10px; - margin-bottom: 24px; - border-bottom: 1px solid var(--border); - padding-bottom: 12px; - overflow-x: auto; - } - .tab-btn { - background: rgba(255, 255, 255, 0.04); - border: 1px solid var(--border); - color: var(--text-secondary); - font-size: 13.5px; - font-weight: 700; - padding: 10px 22px; - border-radius: 980px; - cursor: pointer; - transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); - white-space: nowrap; - } - .tab-btn:hover { background: rgba(255, 255, 255, 0.08); color: #FFF; } - .tab-btn.active { - background: var(--accent-blue); - color: #FFF; - border-color: var(--accent-blue); - box-shadow: 0 2px 12px rgba(0, 113, 227, 0.4); + position: relative; } + .gauge-circle::before { + content: ''; + position: absolute; + width: 40px; + height: 40px; + border-radius: 50%; + background: #121214; + } + .gauge-circle span { position: relative; z-index: 1; } + /* Studio Cards & Navigation */ .studio-card { background: var(--bg-card); border: 1px solid var(--border); - border-radius: 28px; - padding: 32px; - margin-bottom: 40px; + border-radius: 24px; + padding: 28px; + margin-bottom: 24px; + } + .tabs-nav { + display: flex; + gap: 8px; + border-bottom: 1px solid var(--border); + padding-bottom: 16px; + margin-bottom: 24px; + overflow-x: auto; + } + .tab-btn { + background: transparent; + border: 1px solid transparent; + color: var(--text-secondary); + padding: 10px 18px; + font-size: 13.5px; + font-weight: 700; + border-radius: 980px; + cursor: pointer; + white-space: nowrap; + transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); + } + .tab-btn:hover { color: #FFF; background: rgba(255, 255, 255, 0.05); } + .tab-btn.active { + color: #FFF; + background: var(--accent-blue); + box-shadow: 0 4px 16px rgba(0, 113, 227, 0.35); } - /* Socratic Interactive Video Player */ + /* Video Player & Socratic Modal */ .video-container { position: relative; - background: #000000; - border: 1px solid var(--border); + width: 100%; border-radius: 20px; overflow: hidden; + background: #000; + border: 1px solid var(--border); box-shadow: 0 20px 40px rgba(0, 0, 0, 0.8); } .video-element { width: 100%; - height: 480px; - object-fit: cover; + aspect-ratio: 16 / 9; display: block; - background: #050507; + background: #000; } - .player-bar { - background: rgba(18, 18, 20, 0.95); - border-top: 1px solid var(--border); - padding: 14px 20px; - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - } - .time-badge { - font-family: monospace; - font-size: 13px; - font-weight: 700; - color: var(--accent-cyan); - background: rgba(0, 245, 212, 0.1); - padding: 4px 10px; - border-radius: 6px; - } - - /* Socratic Checkpoint Modal Overlay */ .socratic-overlay { position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.88); + top: 0; left: 0; right: 0; bottom: 0; + background: rgba(10, 15, 30, 0.94); backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); display: flex; flex-direction: column; align-items: center; justify-content: center; - padding: 32px; - z-index: 10; + padding: 30px; + z-index: 40; text-align: center; animation: fadeIn 0.3s ease; } - @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } } - .quiz-option-btn { width: 100%; - max-width: 580px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid rgba(255, 255, 255, 0.15); - color: #FFF; - padding: 14px 20px; + max-width: 520px; + padding: 12px 18px; + margin: 6px 0; border-radius: 14px; - font-size: 14px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid var(--border); + color: #FFFFFF; + font-size: 13.5px; font-weight: 600; - margin-bottom: 10px; cursor: pointer; text-align: right; transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1); } .quiz-option-btn:hover { - background: rgba(255, 255, 255, 0.1); + background: rgba(0, 113, 227, 0.15); border-color: var(--accent-blue); transform: translateX(-4px); } - .quiz-option-btn.correct { - background: rgba(16, 185, 129, 0.25) !important; - border-color: #10B981 !important; - color: #34D399 !important; + .player-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: rgba(15, 23, 42, 0.8); + border-top: 1px solid var(--border); } - .quiz-option-btn.wrong { - background: rgba(239, 68, 68, 0.25) !important; - border-color: #EF4444 !important; - color: #F87171 !important; + .time-badge { + font-size: 12px; + font-weight: 700; + font-family: monospace; + color: var(--accent-gold); + background: rgba(245, 158, 11, 0.1); + padding: 3px 10px; + border-radius: 6px; } - /* Real-time Chat Box */ + /* Real-time Chat */ .student-chat-box { display: flex; flex-direction: column; height: 480px; + background: rgba(0, 0, 0, 0.4); border: 1px solid var(--border); border-radius: 20px; overflow: hidden; - background: #09090B; } .chat-messages { flex: 1; @@ -389,10 +470,11 @@ class StudentPortal } .chat-msg { max-width: 75%; - padding: 12px 18px; + padding: 12px 16px; border-radius: 18px; - font-size: 14px; - line-height: 1.6; + font-size: 13px; + line-height: 1.5; + position: relative; } .chat-msg.student { align-self: flex-start; @@ -402,668 +484,463 @@ class StudentPortal } .chat-msg.teacher { align-self: flex-end; - background: #18181B; + background: rgba(255, 255, 255, 0.08); border: 1px solid var(--border); - color: #FFF; + color: var(--text-primary); border-bottom-left-radius: 4px; } - .chat-time { display: block; font-size: 11px; opacity: 0.6; margin-top: 4px; } + .chat-time { + font-size: 10px; + opacity: 0.6; + margin-top: 4px; + display: block; + text-align: left; + } .chat-input-bar { - padding: 14px; - background: #121214; + padding: 12px 16px; + background: rgba(18, 18, 20, 0.95); border-top: 1px solid var(--border); display: flex; gap: 10px; } - /* Exam Taking Modal & Diagnostic Report */ - .exam-modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.85); - backdrop-filter: blur(20px); - z-index: 200; - display: none; - align-items: center; - justify-content: center; - padding: 20px; + @keyframes fadeIn { + from { opacity: 0; transform: scale(0.98); } + to { opacity: 1; transform: scale(1); } } - .exam-card { - background: #121214; - border: 1px solid var(--border); - border-radius: 28px; - max-width: 680px; - width: 100%; - padding: 36px; - box-shadow: 0 24px 60px rgba(0, 0, 0, 0.8); - position: relative; - } - - /* DRM Watermark */ - @keyframes bounce-watermark { - 0%, 100% { transform: translate(10px, 10px); } - 50% { transform: translate(140px, 80px); } - } - .watermark-anim { - position: fixed; bottom: 20px; left: 20px; z-index: 99; pointer-events: none; opacity: 0.35; - animation: bounce-watermark 12s infinite ease-in-out; background: rgba(0, 0, 0, 0.75); - border: 1px solid rgba(255, 255, 255, 0.2); padding: 4px 10px; border-radius: 6px; - font-family: monospace; font-size: 11px; color: var(--accent-cyan); - } - - .spinner { - width: 18px; height: 18px; border: 3px solid rgba(255, 255, 255, 0.3); - border-top-color: #FFF; border-radius: 50%; animation: spin 0.8s linear infinite; - } - @keyframes spin { to { transform: rotate(360deg); } } - - footer { - border-top: 1px solid var(--border); - padding: 24px 0; - background: #000000; - color: var(--text-muted); - font-size: 12px; - margin-top: auto; + @keyframes toastSlideIn { + from { opacity: 0; transform: translateX(40px); } + to { opacity: 1; transform: translateX(0); } } - -
-
-
- - صَقِل -
- صَقِل - بوابة الطالب الذكية -
-
+ + -
- - - أنت معلم؟ - استوديو المعلمين ← - + +
+
+ + صَقِل + صَقِل + بوابة الطالب الذكية + + +
+ + + Workerman غير متصل + +
- -
-
+
+ + +
+
+

تسجيل دخول الطالب 🎓

+

أدخل رقم هاتفك لاستلام رمز التحقق الفوري (OTP) عبر واتساب.

+
- -
-
-

ادخل لعالم التمكين والفهم

-

تسجيل دخول آمن وسريع عبر رمز الواتساب المعتمد

+
+ + -
- - -
-
- - -
+ +
+ + +
+
+ توجيهي 2008 — الفرع العلمي والوطني +

أهلاً بك يا بطلنا 🚀

+

صقل الفهم، حل الأسئلة الوزارية، والتواصل الحي مع أستاذك.

+
-
- -
-
🇯🇴 +962
- -
-
- - +
+
+ 92.4%
- - -
- -
- - -
-
- -
-
-

امتحان قواعد الاشتقاق

- السؤال 1 من 3 -
-
- ⏱️ 14:59 -
- -
-
-

- إذا كان f(x) = (2x + 1)³، فما هي قيمة المشتقة f'(1)؟ + + + + + + + + + +

+ + +
- + -HTML; + $coursesCount, + 'studentsCount' => $studentsCount, + 'lessons' => $lessons, + 'exams' => $exams + ], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP); + + ob_start(); + ?> بوابة المعلم — استوديو صَقِل التعليمي + @@ -388,15 +420,15 @@ class TeacherPortal
الكورسات المنشورة - 3 +
الطلاب المتفاعلون - 840 +
متوسط إتقان الطلاب - 91.4% + 94.6%
@@ -1394,6 +1426,7 @@ class TeacherPortal -HTML; +