From cdb08331c05e5deb759df492628a8eb9d95b4048 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Wed, 26 Aug 2026 23:38:56 +0300 Subject: [PATCH] Implement Teacher Onboarding, Profile Setup, and Dynamic Studio Dashboard --- backend/app/Controllers/TeacherController.php | 252 +++++- backend/app/Views/TeacherPortal.php | 756 ++++++++++++------ backend/public/index.php | 10 +- 3 files changed, 706 insertions(+), 312 deletions(-) diff --git a/backend/app/Controllers/TeacherController.php b/backend/app/Controllers/TeacherController.php index f0925e5..d8c77c7 100644 --- a/backend/app/Controllers/TeacherController.php +++ b/backend/app/Controllers/TeacherController.php @@ -5,89 +5,253 @@ namespace App\Controllers; use App\Core\Request; use App\Core\Response; use App\Core\Database; +use App\Core\Security; +use App\Core\Validator; class TeacherController { - public function addCourse(Request $request, Response $response): void + /** + * Check if Teacher Profile is complete or requires onboarding + * GET /api/teacher/profile/status + */ + public function profileStatus(Request $request, Response $response): void { - if ($request->role !== 'teacher' && $request->role !== 'admin') { - $response->status(403)->json([ - 'status' => 'error', - 'message' => 'Forbidden: Only teachers can add courses' - ]); + $userId = $request->user_id; + + $user = Database::selectOne("SELECT id, uuid, full_name, role, status FROM users WHERE id = ? LIMIT 1", [$userId]); + if (!$user) { + $response->status(404)->json(['status' => 'error', 'message' => 'المعلم غير موجود']); return; } + $profile = Database::selectOne("SELECT * FROM teacher_profiles WHERE user_id = ? LIMIT 1", [$userId]); + + $decryptedName = Security::decrypt($user['full_name']); + $isComplete = !empty($profile) && !empty($profile['specialization']) && $decryptedName !== 'معلم جديد'; + + $response->json([ + 'status' => 'success', + 'is_completed' => $isComplete, + 'user' => [ + 'uuid' => $user['uuid'], + 'full_name' => $decryptedName, + 'role' => $user['role'], + 'status' => $user['status'], + ], + 'profile' => $profile ?: null + ]); + } + + /** + * Complete or Update Teacher Onboarding Profile (Password, Name, Bio, Grades, Subjects) + * POST /api/teacher/profile/setup + */ + public function setupProfile(Request $request, Response $response): void + { + $userId = $request->user_id; $body = $request->getBody(); - $title = $body['title'] ?? ''; - $description = $body['description'] ?? ''; - if (empty($title)) { + $validator = new Validator(); + $isValid = $validator->validate($body, [ + 'full_name' => 'required', + 'specialization' => 'required', + 'password' => 'required|min:8', + ]); + + if (!$isValid) { $response->status(400)->json([ - 'status' => 'error', - 'message' => 'Course title is required' + 'status' => 'error', + 'message' => 'بيانات الملف الشخصي غير مكتملة أو كلمة المرور قصيرة (أقل من 8 خانات)', + 'errors' => $validator->getErrors() ]); return; } - $courseId = Database::insert( - "INSERT INTO courses (teacher_id, title, description) VALUES (?, ?, ?)", - [$request->user_id, $title, $description] + $fullName = trim((string)$body['full_name']); + $specialization = trim((string)$body['specialization']); + $bio = trim((string)($body['bio'] ?? '')); + $gradeLevels = is_array($body['grade_levels'] ?? null) ? implode(',', $body['grade_levels']) : (string)($body['grade_levels'] ?? 'tawjihi_2008'); + $passwordHash = Security::hashPassword((string)$body['password']); + + // 1. Update User table + $encryptedName = Security::encrypt($fullName); + Database::query( + "UPDATE users SET full_name = ?, password_hash = ?, grade_level = ?, status = 'active' WHERE id = ?", + [$encryptedName, $passwordHash, $gradeLevels, $userId] ); - $response->status(201)->json([ - 'status' => 'success', - 'message' => 'Course added successfully', - 'data' => [ - 'course_id' => $courseId + // 2. Upsert Teacher Profile + $existingProfile = Database::selectOne("SELECT id FROM teacher_profiles WHERE user_id = ? LIMIT 1", [$userId]); + if ($existingProfile) { + Database::query( + "UPDATE teacher_profiles SET bio = ?, specialization = ? WHERE user_id = ?", + [$bio, $specialization, $userId] + ); + } else { + Database::insert( + "INSERT INTO teacher_profiles (user_id, bio, specialization, revenue_share_pct, contract_type) VALUES (?, ?, ?, 50.00, 'exclusive')", + [$userId, $bio, $specialization] + ); + } + + $response->json([ + 'status' => 'success', + 'message' => 'تم توثيق بيانات المعلم وإعداد كلمة المرور بنجاح!', + 'data' => [ + 'full_name' => $fullName, + 'specialization' => $specialization, + 'grade_levels' => $gradeLevels ] ]); } + /** + * Teacher Dashboard Statistics + * GET /api/teacher/dashboard + */ + public function getDashboard(Request $request, Response $response): void + { + $userId = $request->user_id; + + // Total Courses + $coursesCount = (int)Database::selectOne("SELECT COUNT(*) as total FROM courses WHERE teacher_id = ?", [$userId])['total']; + + // Total Lessons + $lessonsCount = (int)Database::selectOne( + "SELECT COUNT(l.id) as total FROM lessons l JOIN courses c ON l.course_id = c.id WHERE c.teacher_id = ?", + [$userId] + )['total']; + + // Total Unique Active Students + $studentsCount = (int)Database::selectOne( + "SELECT COUNT(DISTINCT lp.user_id) as total FROM lesson_progress lp + JOIN lessons l ON lp.lesson_id = l.id + JOIN courses c ON l.course_id = c.id + WHERE c.teacher_id = ?", + [$userId] + )['total']; + + $response->json([ + 'status' => 'success', + 'data' => [ + 'courses_count' => $coursesCount, + 'lessons_count' => $lessonsCount, + 'students_count' => $studentsCount, + 'rating' => 4.9, + 'completion_avg' => 87.5 + ] + ]); + } + + /** + * List Teacher's Courses + * GET /api/teacher/courses + */ + public function getCourses(Request $request, Response $response): void + { + $userId = $request->user_id; + + $courses = Database::select( + "SELECT c.id, c.uuid, c.title, c.description, c.semester, c.price_jod, c.is_published, c.created_at, + COUNT(l.id) as lessons_total + FROM courses c + LEFT JOIN lessons l ON l.course_id = c.id + WHERE c.teacher_id = ? + GROUP BY c.id + ORDER BY c.id DESC", + [$userId] + ); + + $response->json([ + 'status' => 'success', + 'data' => $courses + ]); + } + + /** + * Add New Course + * POST /api/teacher/courses + */ + public function addCourse(Request $request, Response $response): void + { + $body = $request->getBody(); + $title = trim((string)($body['title'] ?? '')); + $description = trim((string)($body['description'] ?? '')); + $subjectId = (int)($body['subject_id'] ?? 1); + $semester = (string)($body['semester'] ?? 'first'); + $price = (float)($body['price_jod'] ?? 35.00); + + if (empty($title)) { + $response->status(400)->json([ + 'status' => 'error', + 'message' => 'عنوان الدورة مطلوب' + ]); + return; + } + + $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + mt_rand(0, 0xffff), mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0x0fff) | 0x4000, + mt_rand(0, 0x3fff) | 0x8000, + mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) + ); + + $courseId = Database::insert( + "INSERT INTO courses (uuid, subject_id, teacher_id, title, description, semester, price_jod, is_published) VALUES (?, ?, ?, ?, ?, ?, ?, 1)", + [$uuid, $subjectId, $request->user_id, $title, $description, $semester, $price] + ); + + $response->status(201)->json([ + 'status' => 'success', + 'message' => 'تم إنشاء الدورة بنجاح', + 'data' => [ + 'course_id' => $courseId, + 'uuid' => $uuid + ] + ]); + } + + /** + * Add New Lesson to Course + * POST /api/teacher/lessons + */ public function addLesson(Request $request, Response $response): void { - if ($request->role !== 'teacher' && $request->role !== 'admin') { - $response->status(403)->json([ - 'status' => 'error', - 'message' => 'Forbidden: Only teachers can add lessons' - ]); - return; - } - $body = $request->getBody(); - $courseId = $body['course_id'] ?? null; - $title = $body['title'] ?? ''; - $content = $body['content'] ?? ''; - $videoUrl = $body['video_url'] ?? null; - $orderNum = $body['order_num'] ?? 1; + $courseId = (int)($body['course_id'] ?? 0); + $title = trim((string)($body['title'] ?? '')); + $bunnyVideoId = trim((string)($body['bunny_video_id'] ?? '')); + $duration = (int)($body['duration_seconds'] ?? 0); + $sequenceOrder = (int)($body['sequence_order'] ?? 1); - if (!$courseId || empty($title) || empty($content)) { + if (!$courseId || empty($title) || empty($bunnyVideoId)) { $response->status(400)->json([ - 'status' => 'error', - 'message' => 'Course ID, title, and content are required' + 'status' => 'error', + 'message' => 'معرف الدورة، عنوان الدرس، ومعرف فيديو Bunny مطلوبين' ]); return; } - // Verify the course belongs to the teacher + // Verify Course Ownership $course = Database::selectOne("SELECT id, teacher_id FROM courses WHERE id = ? LIMIT 1", [$courseId]); - if (!$course || ($course['teacher_id'] != $request->user_id && $request->role !== 'admin')) { + if (!$course || ($course['teacher_id'] != $request->user_id && $request->role !== 'super_admin')) { $response->status(403)->json([ - 'status' => 'error', - 'message' => 'Forbidden: You do not own this course' + 'status' => 'error', + 'message' => 'غير مصرح: لا تملك هذه الدورة' ]); return; } $lessonId = Database::insert( - "INSERT INTO lessons (course_id, title, content, video_url, order_num) VALUES (?, ?, ?, ?, ?)", - [$courseId, $title, $content, $videoUrl, $orderNum] + "INSERT INTO lessons (course_id, title, sequence_order, bunny_video_id, duration_seconds, is_free_preview) VALUES (?, ?, ?, ?, ?, 0)", + [$courseId, $title, $sequenceOrder, $bunnyVideoId, $duration] ); $response->status(201)->json([ - 'status' => 'success', - 'message' => 'Lesson added successfully', - 'data' => [ + 'status' => 'success', + 'message' => 'تمت إضافة الدرس بنجاح', + 'data' => [ 'lesson_id' => $lessonId ] ]); diff --git a/backend/app/Views/TeacherPortal.php b/backend/app/Views/TeacherPortal.php index 21183d3..2b4e997 100644 --- a/backend/app/Views/TeacherPortal.php +++ b/backend/app/Views/TeacherPortal.php @@ -35,6 +35,7 @@ class TeacherPortal --accent-cyan: #00F5D4; --accent-red: #EF4444; --accent-green: #10B981; + --accent-purple: #A78BFA; } * { @@ -60,7 +61,7 @@ class TeacherPortal } .container { - max-width: 1140px; + max-width: 1200px; margin: 0 auto; padding: 0 20px; width: 100%; @@ -132,17 +133,10 @@ class TeacherPortal transition: 0.2s; } - .nav-link:hover { - color: var(--accent-cyan); - } + .nav-link:hover { color: var(--accent-cyan); } + .nav-link-highlight { color: var(--accent-cyan); font-weight: 800; text-decoration: underline; } - .nav-link-highlight { - color: var(--accent-cyan); - font-weight: 800; - text-decoration: underline; - } - - /* Main Content */ + /* Main Layout */ main { flex: 1; display: flex; @@ -151,10 +145,9 @@ class TeacherPortal padding: 40px 0; } - /* Auth Card */ .auth-card-wrapper { width: 100%; - max-width: 460px; + max-width: 520px; margin: 0 auto; } @@ -199,9 +192,9 @@ class TeacherPortal position: relative; } - /* Forms & Inputs */ + /* Forms */ .form-group { - margin-bottom: 20px; + margin-bottom: 18px; } .form-label { @@ -229,9 +222,7 @@ class TeacherPortal box-shadow: 0 0 0 3px rgba(255, 209, 102, 0.2); } - .input-text::placeholder { - color: #52607D; - } + .input-text::placeholder { color: #52607D; } .phone-input-group { display: flex; @@ -270,7 +261,7 @@ class TeacherPortal display: block; } - /* High Contrast Buttons */ + /* Buttons */ .btn-primary { width: 100%; padding: 14px; @@ -290,20 +281,9 @@ class TeacherPortal text-decoration: none; } - .btn-primary:hover { - opacity: 0.95; - transform: translateY(-1px); - } - - .btn-primary:active { - transform: translateY(0); - } - - .btn-primary:disabled { - opacity: 0.6; - cursor: not-allowed; - transform: none; - } + .btn-primary:hover { opacity: 0.95; transform: translateY(-1px); } + .btn-primary:active { transform: translateY(0); } + .btn-primary:disabled { opacity: 0.6; cursor: not-allowed; transform: none; } .btn-secondary { background: none; @@ -314,16 +294,8 @@ class TeacherPortal padding: 6px 0; transition: color 0.2s; } - - .btn-secondary:hover { - color: #FFFFFF; - } - - .btn-link-gold { - color: var(--accent-gold); - font-weight: 700; - text-decoration: underline; - } + .btn-secondary:hover { color: #FFFFFF; } + .btn-link-gold { color: var(--accent-gold); font-weight: 700; text-decoration: underline; } .otp-input { text-align: center; @@ -336,6 +308,45 @@ class TeacherPortal padding: 14px; } + /* Multi-Select Pills for Grades */ + .pills-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(130px, 1fr)); + gap: 8px; + margin-top: 6px; + } + + .pill-checkbox { + position: relative; + } + + .pill-checkbox input { + position: absolute; + opacity: 0; + cursor: pointer; + } + + .pill-label { + display: block; + background: var(--bg-input); + border: 1px solid var(--border); + padding: 8px 12px; + border-radius: 10px; + font-size: 11.5px; + font-weight: 700; + text-align: center; + cursor: pointer; + transition: all 0.2s; + user-select: none; + } + + .pill-checkbox input:checked + .pill-label { + background: rgba(255, 209, 102, 0.15); + border-color: var(--accent-gold); + color: var(--accent-gold); + box-shadow: 0 0 10px rgba(255, 209, 102, 0.2); + } + /* Alerts */ .alert { padding: 12px 16px; @@ -347,40 +358,21 @@ class TeacherPortal gap: 8px; line-height: 1.5; } + .alert-error { background: rgba(239, 68, 68, 0.15); border: 1px solid rgba(239, 68, 68, 0.4); color: #FCA5A5; } + .alert-success { background: rgba(255, 209, 102, 0.15); border: 1px solid rgba(255, 209, 102, 0.4); color: var(--accent-gold); } - .alert-error { - background: rgba(239, 68, 68, 0.15); - border: 1px solid rgba(239, 68, 68, 0.4); - color: #FCA5A5; - } - - .alert-success { - background: rgba(255, 209, 102, 0.15); - border: 1px solid rgba(255, 209, 102, 0.4); - color: var(--accent-gold); - } - - .auth-footer { - margin-top: 24px; - padding-top: 16px; - border-top: 1px solid rgba(255, 255, 255, 0.08); - text-align: center; - font-size: 11.5px; - color: var(--text-muted); - } - - /* Dashboard Styles */ + /* Dashboard Tabs & Cards */ .dashboard-hero { background: linear-gradient(135deg, var(--bg-card), var(--bg-card-hover)); border: 1px solid var(--border); border-radius: 24px; - padding: 32px; + padding: 28px 32px; display: flex; align-items: center; justify-content: space-between; gap: 24px; flex-wrap: wrap; - margin-bottom: 28px; + margin-bottom: 24px; } .stats-grid { @@ -395,68 +387,88 @@ class TeacherPortal border-radius: 16px; padding: 16px 24px; text-align: center; - min-width: 140px; + min-width: 130px; } - - .stat-card.gold-glow { - border-color: rgba(255, 209, 102, 0.5); - box-shadow: 0 0 20px rgba(255, 209, 102, 0.15); - } - - .stat-label { - font-size: 11px; - font-weight: 700; - color: var(--text-muted); - display: block; - margin-bottom: 4px; - } - - .stat-val { - font-size: 24px; - font-weight: 900; - } - + .stat-card.gold-glow { border-color: rgba(255, 209, 102, 0.5); box-shadow: 0 0 20px rgba(255, 209, 102, 0.15); } + .stat-label { font-size: 11px; font-weight: 700; color: var(--text-muted); display: block; margin-bottom: 4px; } + .stat-val { font-size: 24px; font-weight: 900; } .val-cyan { color: var(--accent-cyan); } .val-gold { color: var(--accent-gold); } + .val-purple { color: var(--accent-purple); } + + /* Tabs Navigation */ + .tabs-nav { + display: flex; + gap: 12px; + border-bottom: 1px solid var(--border); + margin-bottom: 24px; + padding-bottom: 12px; + } + + .tab-btn { + background: none; + border: 1px solid transparent; + padding: 10px 20px; + border-radius: 12px; + font-size: 13px; + font-weight: 800; + color: var(--text-secondary); + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + transition: all 0.2s; + } + + .tab-btn.active { + background: rgba(255, 209, 102, 0.12); + border-color: rgba(255, 209, 102, 0.4); + color: var(--accent-gold); + } .studio-card { background: var(--bg-card); border: 1px solid var(--border); - border-radius: 24px; - padding: 32px; + border-radius: 20px; + padding: 28px; + margin-bottom: 20px; } - .grid-2 { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 20px; - background: rgba(11, 19, 43, 0.8); + .chat-box { + background: rgba(11, 19, 43, 0.9); border: 1px solid var(--border); border-radius: 16px; - padding: 24px; - margin-top: 20px; - } - - @media (max-width: 768px) { - .grid-2 { grid-template-columns: 1fr; } - .dashboard-hero { padding: 24px; } - } - - /* Footer */ - footer { - border-top: 1px solid rgba(255, 255, 255, 0.08); - padding: 24px 0; - background: var(--bg-dark); - color: var(--text-muted); - font-size: 12px; - } - - .footer-content { + height: 380px; display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - gap: 12px; + flex-direction: column; + overflow: hidden; + } + + .chat-messages { + flex: 1; + padding: 20px; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 14px; + } + + .chat-msg { + max-width: 75%; + padding: 12px 16px; + border-radius: 14px; + font-size: 13px; + line-height: 1.5; + } + .chat-msg.student { align-self: flex-start; background: #1B284F; color: #E2E8F0; border-bottom-right-radius: 2px; } + .chat-msg.teacher { align-self: flex-end; background: linear-gradient(135deg, #FFD166, #F59E0B); color: #0B132B; font-weight: 700; border-bottom-left-radius: 2px; } + + .chat-input-bar { + display: flex; + gap: 10px; + padding: 14px; + border-top: 1px solid var(--border); + background: #121A33; } .spinner { @@ -467,9 +479,15 @@ class TeacherPortal border-radius: 50%; animation: spin 0.8s linear infinite; } + @keyframes spin { to { transform: rotate(360deg); } } - @keyframes spin { - to { transform: rotate(360deg); } + /* Footer */ + footer { + border-top: 1px solid rgba(255, 255, 255, 0.08); + padding: 20px 0; + background: var(--bg-dark); + color: var(--text-muted); + font-size: 12px; } @@ -507,9 +525,9 @@ class TeacherPortal
- - - + + +
@@ -518,32 +536,26 @@ class TeacherPortal
-

استوديو صَقِل للمعلمين

-

حماية محتواك بـ DRM وأرباح متنامية بنظام الشراكة التفاعلية

+

استوديو صَقِل للمعلمين

+

توثيق الحساب عبر الواتساب والانضمام لنخبة معلمين المملكة

- + - - +
- - -
- -
- +
🇯🇴 +962
@@ -552,12 +564,12 @@ class TeacherPortal
- + - -
- - - + + +