diff --git a/backend/app/Controllers/VideoController.php b/backend/app/Controllers/VideoController.php index c3afd3b..b8ac842 100644 --- a/backend/app/Controllers/VideoController.php +++ b/backend/app/Controllers/VideoController.php @@ -297,6 +297,35 @@ class VideoController ]); } + /** + * List all published lessons for student portal + * GET /api/student/lessons + */ + public function getStudentLessons(Request $request, Response $response): void + { + VideoService::ensureSchema(); + CurriculumService::ensureSchema(); + + $courseId = (int)($request->getQuery('course_id') ?: 0); + $where = $courseId > 0 ? "WHERE l.course_id = {$courseId}" : ""; + + $lessons = Database::select( + "SELECT l.id, l.course_id, l.title, l.sequence_order, l.storage_type, l.video_uuid, l.hls_url, l.thumbnail_url, + l.duration_seconds, l.is_free_preview, l.created_at, + c.title as course_title, + (SELECT COUNT(*) FROM exams WHERE lesson_id = l.id AND scope = 'in_video_checkpoint') as checkpoints_count + FROM lessons l + JOIN courses c ON l.course_id = c.id + {$where} + ORDER BY l.id DESC" + ); + + $response->json([ + 'status' => 'success', + 'data' => $lessons + ]); + } + /** * Get Lesson Playback Data with Chapters Roadmap and Socratic Checkpoints * GET /api/lessons/{id}/playback @@ -307,16 +336,23 @@ class VideoController CurriculumService::ensureSchema(); $lessonId = (int)$request->getParam('id'); - if (!$lessonId) { - $response->status(400)->json(['status' => 'error', 'message' => 'معرف الدرس مطلوب']); + $lesson = null; + + if ($lessonId > 0) { + $lesson = Database::selectOne("SELECT * FROM lessons WHERE id = ? LIMIT 1", [$lessonId]); + } + + if (!$lesson) { + // Fallback to latest available lesson + $lesson = Database::selectOne("SELECT * FROM lessons ORDER BY id DESC LIMIT 1"); + } + + if (!$lesson) { + $response->status(404)->json(['status' => 'error', 'message' => 'لا توجد دروس متاحة حالياً']); return; } - $lesson = Database::selectOne("SELECT * FROM lessons WHERE id = ? LIMIT 1", [$lessonId]); - if (!$lesson) { - $response->status(404)->json(['status' => 'error', 'message' => 'الدرس غير موجود']); - return; - } + $lessonId = (int)$lesson['id']; // If lesson has no checkpoints yet, generate them autonomously $existingCount = Database::selectOne("SELECT COUNT(*) as cnt FROM exams WHERE lesson_id = ? AND scope = 'in_video_checkpoint'", [$lessonId]); diff --git a/backend/app/Views/StudentPortal.php b/backend/app/Views/StudentPortal.php index cb22cbf..6202956 100644 --- a/backend/app/Views/StudentPortal.php +++ b/backend/app/Views/StudentPortal.php @@ -600,13 +600,24 @@ class StudentPortal
+ +
+
+ 📚 فهرس الحصص والدروس المتاحة: + جارٍ التحميل... +
+ +
+
-

الدرس 4: قواعد الاشتقاق الأساسية — رياضيات علمي

- الأستاذ حمزة الغويري • Bunny Stream DRM Protection +

الدرس: قواعد الاشتقاق والتحليل الجنائي

+ الأستاذ حمزة الغويري • Cloudflare R2 + HLS Adaptive Stream
- - Checkpoint Active (00:15) + + الذكاء الاصطناعي السقراطي نشط 🧠
@@ -1048,6 +1059,7 @@ class StudentPortal let activeLessonCheckpoints = []; let triggeredCheckpoints = new Set(); let hlsInstance = null; + let activeLessonId = null; document.addEventListener('DOMContentLoaded', async () => { const token = localStorage.getItem('saqel_student_jwt'); @@ -1061,7 +1073,7 @@ class StudentPortal } initWebSocket(token); await checkStudentSession(token); - await loadStudentLessonPlayback(1); + await loadStudentLessonsList(); } // Video player time listener for Socratic checkpoint @@ -1084,6 +1096,55 @@ class StudentPortal } }); + async function loadStudentLessonsList() { + const token = localStorage.getItem('saqel_student_jwt'); + if (!token) return; + try { + const res = await fetch('/api/student/lessons', { + headers: { 'Authorization': 'Bearer ' + token } + }); + const data = await res.json(); + if (res.ok && data.data && data.data.length > 0) { + const lessons = data.data; + const countLabel = document.getElementById('lessons_count_label'); + if (countLabel) countLabel.textContent = `${lessons.length} حصص متاحة`; + + const carousel = document.getElementById('student_lessons_carousel'); + if (carousel) { + carousel.innerHTML = lessons.map((les, idx) => ` + + `).join(''); + } + + if (!activeLessonId && lessons[0]) { + selectStudentLesson(lessons[0].id); + } + } + } catch (e) { + console.error('Load lessons list notice:', e); + } + } + + function selectStudentLesson(lessonId) { + activeLessonId = lessonId; + const btns = document.querySelectorAll('#student_lessons_carousel button'); + btns.forEach(b => { + b.style.background = 'rgba(255,255,255,0.06)'; + b.style.color = 'var(--text-secondary)'; + b.style.borderColor = 'var(--border)'; + }); + const activeBtn = document.getElementById(`btn_lesson_${lessonId}`); + if (activeBtn) { + activeBtn.style.background = 'linear-gradient(135deg, #0284C7, #0369A1)'; + activeBtn.style.color = '#FFF'; + activeBtn.style.borderColor = '#38BDF8'; + } + loadStudentLessonPlayback(lessonId); + } + function formatTime(secs) { const m = Math.floor(secs / 60).toString().padStart(2, '0'); const s = (secs % 60).toString().padStart(2, '0'); @@ -1114,11 +1175,19 @@ class StudentPortal } function triggerCheckpointDemo() { - const video = document.getElementById('lesson_video_player'); - if (video) { - video.currentTime = 14; - triggeredCheckpoints.clear(); - video.play(); + if (activeLessonCheckpoints.length > 0) { + renderSocraticModal(activeLessonCheckpoints[0]); + } else { + renderSocraticModal({ + question_text: "إذا كان f(x) = sin(3x)، فما هي مشتقة الاقتران f'(x)؟", + options: [ + { text: "3 cos(3x) (مشتقة الزاوية ضرب مشتقة الاقتران)", is_correct: true }, + { text: "cos(3x)", is_correct: false }, + { text: "-3 cos(3x)", is_correct: false }, + { text: "-cos(3x)", is_correct: false } + ], + rewind_on_fail_seconds: 45 + }); } } @@ -1168,19 +1237,30 @@ class StudentPortal } } - async function loadStudentLessonPlayback(lessonId = 1) { + async function loadStudentLessonPlayback(lessonId = 0) { const token = localStorage.getItem('saqel_student_jwt'); if (!token) return; try { - const res = await fetch(`/api/lessons/${lessonId}/playback`, { + const targetUrl = lessonId > 0 ? `/api/lessons/${lessonId}/playback` : '/api/lessons/0/playback'; + const res = await fetch(targetUrl, { headers: { 'Authorization': 'Bearer ' + token } }); const data = await res.json(); if (res.ok && data.status === 'success') { + const les = data.data.lesson; const pb = data.data.playback; const checkpoints = data.data.checkpoints || []; const chapters = data.data.chapters || []; activeLessonCheckpoints = checkpoints; + triggeredCheckpoints.clear(); + + // Update Titles + if (les) { + const titleEl = document.getElementById('current_lesson_title'); + const subtitleEl = document.getElementById('current_lesson_subtitle'); + if (titleEl) titleEl.textContent = `الدرس: ${les.title}`; + if (subtitleEl) subtitleEl.textContent = `الأستاذ حمزة الغويري • Cloudflare R2 + HLS Adaptive Stream`; + } // Update Checkpoint Status badge const badge = document.getElementById('checkpoint_status_badge'); @@ -1190,16 +1270,28 @@ class StudentPortal // Render AI Timeline Chapters const chaptersList = document.getElementById('ai_timeline_chapters_list'); - if (chaptersList && chapters.length > 0) { - chaptersList.innerHTML = chapters.map((ch, idx) => ` -
-
- المحطة ${idx + 1}: ${escapeHtml(ch.title)} - ${formatTime(ch.start_seconds)} + if (chaptersList) { + if (chapters.length > 0) { + chaptersList.innerHTML = chapters.map((ch, idx) => ` +
+
+ المحطة ${idx + 1}: ${escapeHtml(ch.title)} + ${formatTime(ch.start_seconds)} +
+
${escapeHtml(ch.summary || '')}
-
${escapeHtml(ch.summary || '')}
-
- `).join(''); + `).join(''); + } else { + chaptersList.innerHTML = ` +
+
+ المحطة 1: الشرح الكامل والتطبيقات + 00:00 +
+
استعراض المفاهيم وتطبيقات المنهاج الوزاري.
+
+ `; + } } const video = document.getElementById('lesson_video_player'); diff --git a/backend/public/index.php b/backend/public/index.php index 790d1f4..8d831ab 100644 --- a/backend/public/index.php +++ b/backend/public/index.php @@ -73,15 +73,13 @@ $router->get('/api/teacher/courses', [\App\Controllers\TeacherController: $router->post('/api/teacher/courses', [\App\Controllers\TeacherController::class, 'addCourse'], [\App\Middlewares\AuthMiddleware::class]); $router->post('/api/teacher/lessons', [\App\Controllers\TeacherController::class, 'addLesson'], [\App\Middlewares\AuthMiddleware::class]); -// Dual Video Storage & Bunny.net Stream Routes (API-Driven) +// Dual Video Storage & Cloudflare R2 / HLS Stream Routes (API-Driven) $router->post('/api/teacher/videos/upload-direct', [\App\Controllers\VideoController::class, 'uploadDirect'], [\App\Middlewares\AuthMiddleware::class]); -$router->post('/api/teacher/videos/bunny-create', [\App\Controllers\VideoController::class, 'createBunnyVideo'],[\App\Middlewares\AuthMiddleware::class]); -$router->post('/api/teacher/videos/bunny-link', [\App\Controllers\VideoController::class, 'linkBunnyLesson'], [\App\Middlewares\AuthMiddleware::class]); $router->post('/api/teacher/lessons/checkpoints', [\App\Controllers\VideoController::class, 'saveCheckpoint'], [\App\Middlewares\AuthMiddleware::class]); +$router->get('/api/student/lessons', [\App\Controllers\VideoController::class, 'getStudentLessons'], [\App\Middlewares\AuthMiddleware::class]); $router->get('/api/videos/stream/{uuid}', [\App\Controllers\VideoController::class, 'streamLocalVideo']); $router->get('/api/videos/hls/{uuid}/{file}', [\App\Controllers\VideoController::class, 'streamHls']); $router->get('/api/lessons/{id}/playback', [\App\Controllers\VideoController::class, 'getPlaybackData'], [\App\Middlewares\AuthMiddleware::class]); -$router->post('/api/webhooks/bunny', [\App\Controllers\VideoController::class, 'handleBunnyWebhook']); // Student & Teacher Chat Routes (API-Driven, Authenticated) $router->get('/api/chat/conversations', [\App\Controllers\ChatController::class, 'getConversations'], [\App\Middlewares\AuthMiddleware::class]);