From abe862f04db60fa88c3617d0fd8364b019587018 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 28 Aug 2026 18:21:58 +0300 Subject: [PATCH] Update Saqel Platform: 2026-08-28 18:21:58 --- backend/app/Controllers/AuthController.php | 170 ++++++++++- backend/app/Controllers/TeacherController.php | 40 +-- backend/app/Views/CurriculumStudio.php | 268 ++++++++++++------ backend/app/Views/StudentPortal.php | 154 ++++++++-- backend/app/Views/TeacherPortal.php | 16 +- backend/public/index.php | 2 + 6 files changed, 486 insertions(+), 164 deletions(-) diff --git a/backend/app/Controllers/AuthController.php b/backend/app/Controllers/AuthController.php index 177e27a..04412f3 100644 --- a/backend/app/Controllers/AuthController.php +++ b/backend/app/Controllers/AuthController.php @@ -359,37 +359,183 @@ class AuthController ]); } - /** + /** * Get Current Authenticated User Data * GET /api/auth/me */ public function me(Request $request, Response $response): void { - $userId = $request->user_id; + $userId = (int)$request->user_id; + $role = $request->role ?? 'student'; - $user = Database::selectOne( - "SELECT uuid, full_name, phone_number, role, grade_level, stream, status, created_at FROM users WHERE id = ? LIMIT 1", - [$userId] - ); + $userData = null; - if (!$user) { + if ($role === 'teacher') { + $user = Database::selectOne( + "SELECT t.id, t.uuid, t.full_name, t.specialization, t.bio, t.school_id, ai.phone_number, ai.status, t.created_at + FROM teachers t + JOIN auth_identities ai ON t.identity_id = ai.id + WHERE t.id = ? LIMIT 1", + [$userId] + ); + if ($user) { + $isCompleted = !empty($user['full_name']) && $user['full_name'] !== 'معلم جديد' && !empty($user['specialization']) && $user['specialization'] !== 'بانتظار تحديد التخصص'; + $userData = [ + 'id' => $user['id'], + 'uuid' => $user['uuid'], + 'full_name' => $user['full_name'], + 'name' => $user['full_name'], + 'role' => 'teacher', + 'specialization' => $user['specialization'], + 'bio' => $user['bio'], + 'phone' => Security::decrypt($user['phone_number']), + 'status' => $user['status'], + 'is_completed' => $isCompleted, + 'is_teacher' => true, + 'is_student' => false, + ]; + } + } elseif ($role === 'guardian') { + $user = Database::selectOne( + "SELECT g.id, g.uuid, g.full_name, g.national_id, ai.phone_number, ai.status, g.created_at + FROM guardians g + JOIN auth_identities ai ON g.identity_id = ai.id + WHERE g.id = ? LIMIT 1", + [$userId] + ); + if ($user) { + $userData = [ + 'id' => $user['id'], + 'uuid' => $user['uuid'], + 'full_name' => $user['full_name'], + 'name' => $user['full_name'], + 'role' => 'guardian', + 'phone' => Security::decrypt($user['phone_number']), + 'status' => $user['status'], + 'is_completed' => true, + 'is_teacher' => false, + 'is_student' => false, + ]; + } + } else { + // Student + $user = Database::selectOne( + "SELECT s.id, s.uuid, s.full_name, s.national_id, s.grade_level, s.stream, s.readiness_score, s.school_id, ai.phone_number, ai.status, s.created_at + FROM students s + JOIN auth_identities ai ON s.identity_id = ai.id + WHERE s.id = ? LIMIT 1", + [$userId] + ); + if ($user) { + $isCompleted = !empty($user['full_name']) && $user['full_name'] !== 'طالب جديد' && $user['full_name'] !== 'الطالب المتميز' && !empty($user['grade_level']); + $userData = [ + 'id' => $user['id'], + 'uuid' => $user['uuid'], + 'full_name' => $user['full_name'], + 'name' => $user['full_name'], + 'role' => 'student', + 'national_id' => $user['national_id'], + 'grade_level' => $user['grade_level'], + 'stream' => $user['stream'], + 'readiness_score' => $user['readiness_score'], + 'phone' => Security::decrypt($user['phone_number']), + 'status' => $user['status'], + 'is_completed' => $isCompleted, + 'is_teacher' => false, + 'is_student' => true, + ]; + } + } + + if (!$userData) { $response->status(404)->json([ 'status' => 'error', - 'message' => 'المستخدم غير موجود' + 'message' => 'المستخدم غير موجود أو تم إعادة تهيئة قاعدة البيانات' ]); return; } - $user['full_name'] = Security::decrypt($user['full_name']); - $user['phone_number'] = Security::decrypt($user['phone_number']); - $response->json([ 'status' => 'success', - 'data' => $user + 'data' => $userData ]); } /** + * Check if Student Profile is complete + * GET /api/student/profile/status + */ + public function studentProfileStatus(Request $request, Response $response): void + { + $studentId = (int)$request->user_id; + $student = Database::selectOne( + "SELECT s.*, ai.phone_number FROM students s JOIN auth_identities ai ON s.identity_id = ai.id WHERE s.id = ? LIMIT 1", + [$studentId] + ); + + if (!$student) { + $response->status(401)->json(['status' => 'error', 'message' => 'طالب غير مسجل']); + return; + } + + $isCompleted = !empty($student['full_name']) && $student['full_name'] !== 'طالب جديد' && $student['full_name'] !== 'الطالب المتميز' && !empty($student['grade_level']); + + $response->json([ + 'status' => 'success', + 'is_completed' => $isCompleted, + 'user' => [ + 'id' => $student['id'], + 'uuid' => $student['uuid'], + 'full_name' => $student['full_name'], + 'national_id' => $student['national_id'], + 'grade_level' => $student['grade_level'], + 'stream' => $student['stream'], + 'role' => 'student' + ] + ]); + } + + /** + * Setup Student Profile + * POST /api/student/profile/setup + */ + public function studentProfileSetup(Request $request, Response $response): void + { + $studentId = (int)$request->user_id; + $body = $request->getBody(); + + $fullName = trim((string)($body['full_name'] ?? '')); + $gradeLevel = trim((string)($body['grade_level'] ?? 'grade_10')); + $stream = trim((string)($body['stream'] ?? 'scientific')); + $nationalId = trim((string)($body['national_id'] ?? '')); + + if (empty($fullName)) { + $response->status(400)->json(['status' => 'error', 'message' => 'الاسم الكامل مطلوب']); + return; + } + + Database::query( + "UPDATE students SET full_name = ?, grade_level = ?, stream = ?, national_id = IF(? != '', ?, national_id), updated_at = NOW() WHERE id = ?", + [$fullName, $gradeLevel, $stream, $nationalId, $nationalId, $studentId] + ); + + $student = Database::selectOne("SELECT * FROM students WHERE id = ? LIMIT 1", [$studentId]); + + $response->json([ + 'status' => 'success', + 'message' => 'تم استكمال ملف الطالب بنجاح! مرحباً بك في منصة صَقِل', + 'user' => [ + 'id' => $student['id'], + 'uuid' => $student['uuid'], + 'full_name' => $student['full_name'], + 'grade_level' => $student['grade_level'], + 'stream' => $student['stream'], + 'role' => 'student' + ] + ]); + } + +/** * Logout and destroy Redis active session * POST /api/auth/logout */ diff --git a/backend/app/Controllers/TeacherController.php b/backend/app/Controllers/TeacherController.php index a4b7b6d..e322dec 100644 --- a/backend/app/Controllers/TeacherController.php +++ b/backend/app/Controllers/TeacherController.php @@ -53,20 +53,19 @@ class TeacherController */ public function setupProfile(Request $request, Response $response): void { - $userId = $request->user_id; + $teacherId = (int)$request->user_id; $body = $request->getBody(); $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' => 'بيانات الملف الشخصي غير مكتملة أو كلمة المرور قصيرة (أقل من 8 خانات)', + 'message' => 'بيانات الملف الشخصي غير مكتملة', 'errors' => $validator->getErrors() ]); return; @@ -75,45 +74,26 @@ class TeacherController $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); + // Update Teachers Table Directly Database::query( - "UPDATE users SET full_name = ?, password_hash = ?, grade_level = ?, status = 'active' WHERE id = ?", - [$encryptedName, $passwordHash, $gradeLevels, $userId] + "UPDATE teachers SET full_name = ?, specialization = ?, bio = ?, updated_at = NOW() WHERE id = ?", + [$fullName, $specialization, $bio, $teacherId] ); - // 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] - ); - } + $teacher = Database::selectOne("SELECT * FROM teachers WHERE id = ? LIMIT 1", [$teacherId]); $response->json([ 'status' => 'success', - 'message' => 'تم توثيق بيانات المعلم وإعداد كلمة المرور بنجاح!', + 'message' => 'تم توثيق بيانات المعلم واعتماد ملفك بنجاح!', 'data' => [ - 'full_name' => $fullName, - 'specialization' => $specialization, - 'grade_levels' => $gradeLevels + 'full_name' => $teacher['full_name'] ?? $fullName, + 'specialization' => $teacher['specialization'] ?? $specialization, + 'bio' => $teacher['bio'] ?? $bio ] ]); } - /** - * Teacher Dashboard Statistics - * GET /api/teacher/dashboard - */ public function getDashboard(Request $request, Response $response): void { $userId = $request->user_id; diff --git a/backend/app/Views/CurriculumStudio.php b/backend/app/Views/CurriculumStudio.php index 5bfe951..fcae40a 100644 --- a/backend/app/Views/CurriculumStudio.php +++ b/backend/app/Views/CurriculumStudio.php @@ -32,7 +32,9 @@ class CurriculumStudio :root { --bg-base: #0B0F19; --bg-card: rgba(22, 27, 34, 0.85); + --bg-hover: rgba(255, 255, 255, 0.05); --border: rgba(255, 255, 255, 0.08); + --border-color: rgba(255, 255, 255, 0.12); --accent-cyan: #00F5D4; --accent-gold: #FFD166; --accent-purple: #7B2CBF; @@ -110,6 +112,12 @@ class CurriculumStudio font-size: 11.5px; font-family: monospace; color: var(--accent-cyan); margin-bottom: 14px; display: flex; align-items: center; justify-content: space-between; } + + @keyframes pulse { + 0% { transform: scale(1); opacity: 1; } + 50% { transform: scale(1.1); opacity: 0.7; } + 100% { transform: scale(1); opacity: 1; } + } @@ -121,7 +129,10 @@ class CurriculumStudio صَقِل Enterprise استوديو تفريغ وفهرسة المناهج الحية 📚 -
+
+ استوديو المعلم 👨‍🏫 بوابة الطالب 🎓
@@ -134,7 +145,7 @@ class CurriculumStudio

رفع وتفريغ كتب المناهج الوزارية (PDF ⟵ Markdown Tree) 📑

-

اختر ملف كتاب المنهاج (PDF) ليقوم الخادم بتفريغه وفهرسة وحداته ودروسه الحقيقية على القرص وتوليد ملفات المارك داون.

+

اختر ملف كتاب المنهاج (PDF) ليقوم الخادم والذكاء الاصطناعي بتفريغ الوحدات والدروس والمصادر كملفات Markdown حية.

@@ -192,14 +203,46 @@ class CurriculumStudio
+ + + - - - - - - - -
+ + + @@ -1939,7 +1983,7 @@ class StudentPortal } } - async function verifyStudentOtp() { + async function verifyStudentOtp() { const phone = document.getElementById('student_phone').value.trim(); const otp = document.getElementById('student_otp').value.trim(); if (!otp || otp.length < 4) { @@ -1954,11 +1998,19 @@ class StudentPortal }); const data = await res.json(); if (res.ok && data.status === 'success') { - localStorage.setItem('saqel_student_jwt', data.data.token); - localStorage.setItem('saqel_student_user', JSON.stringify(data.data.user)); - renderDashboard(data.data.user); - initWebSocket(data.data.token); - showLuxuryToast('أهلاً بك يا بطل! 🚀', 'تم تسجيل الدخول بنجاح.'); + const token = data.data.token; + localStorage.setItem('saqel_student_jwt', token); + + // Check if student needs onboarding + const isNew = data.data.user.is_new || data.data.user.name === 'طالب جديد' || data.data.user.name === 'الطالب المتميز'; + if (isNew) { + switchToStudentOnboarding(); + } else { + localStorage.setItem('saqel_student_user', JSON.stringify(data.data.user)); + renderDashboard(data.data.user); + initWebSocket(token); + showLuxuryToast('أهلاً بك يا بطل! 🚀', 'تم تسجيل الدخول بنجاح.'); + } } else { showError(data.message || 'رمز التحقق غير صحيح أو منتهي الصلاحية'); } @@ -1967,11 +2019,67 @@ class StudentPortal } } + function switchToStudentOnboarding() { + document.getElementById('step_phone_container').style.display = 'none'; + document.getElementById('step_otp_container').style.display = 'none'; + document.getElementById('step_onboarding_container').style.display = 'block'; + document.getElementById('alert_box_error').style.display = 'none'; + document.getElementById('alert_box_success').style.display = 'none'; + } + + async function completeStudentOnboarding() { + const token = localStorage.getItem('saqel_student_jwt'); + const fullName = document.getElementById('onboard_student_name').value.trim(); + const grade = document.getElementById('onboard_student_grade').value; + const stream = document.getElementById('onboard_student_stream').value; + const nationalId = document.getElementById('onboard_student_national_id').value.trim(); + + if (!fullName || fullName.length < 3) { + showError('يرجى إدخال اسم الطالب الكامل (الرباعي)'); + return; + } + + const btn = document.getElementById('btn_complete_onboarding'); + btn.disabled = true; + btn.innerHTML = '⏳ جاري حفظ البيانات...'; + + try { + const res = await fetch('/api/student/profile/setup', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + }, + body: JSON.stringify({ + full_name: fullName, + grade_level: grade, + stream: stream, + national_id: nationalId + }) + }); + const data = await res.json(); + if (res.ok && data.status === 'success') { + const u = data.user; + localStorage.setItem('saqel_student_user', JSON.stringify(u)); + renderDashboard(u); + initWebSocket(token); + showLuxuryToast('تم استكمال الحساب بنجاح! 🚀', `مرحباً بك يا ${fullName}`); + } else { + showError(data.message || 'فشل حفظ الملف الشخصي'); + } + } catch (e) { + showError('تعذر الاتصال بالسيرفر أثناء حفظ البيانات'); + } finally { + btn.disabled = false; + btn.innerHTML = 'إتمام التسجيل وبدء التعلم 🚀'; + } + } + function renderDashboard(user) { document.getElementById('auth_box_view').style.display = 'none'; document.getElementById('dashboard_container').style.display = 'block'; document.getElementById('auth_user_badge').style.display = 'flex'; - const name = user.full_name || 'طالب صَقِل'; + const name = user.full_name || user.name || 'طالب صَقِل'; document.getElementById('header_student_name').textContent = `أهلاً، ${name}`; document.getElementById('dashboard_welcome_title').textContent = `أهلاً بك يا ${name} 🚀`; } @@ -1983,37 +2091,31 @@ class StudentPortal } function switchToPhoneStep() { + document.getElementById('step_onboarding_container').style.display = 'none'; document.getElementById('step_otp_container').style.display = 'none'; document.getElementById('step_phone_container').style.display = 'block'; document.getElementById('alert_box_error').style.display = 'none'; document.getElementById('alert_box_success').style.display = 'none'; } - function showError(msg) { - document.getElementById('alert_box_success').style.display = 'none'; - const errBox = document.getElementById('alert_box_error'); - document.getElementById('alert_error_msg').textContent = msg; - errBox.style.display = 'block'; - } - - function showSuccess(msg) { - document.getElementById('alert_box_error').style.display = 'none'; - const okBox = document.getElementById('alert_box_success'); - document.getElementById('alert_success_msg').textContent = msg; - okBox.style.display = 'block'; - } - async function checkStudentSession(token) { try { const res = await fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } }); + if (res.status === 401) { + handleLogout(); + return; + } const data = await res.json(); if (res.ok && data.data) { - renderDashboard(data.data.user || data.data); - } else { - // Token invalid or database was reset: clear cached session - handleLogout(); + const user = data.data; + if (!user.is_completed) { + switchToStudentOnboarding(); + } else { + localStorage.setItem('saqel_student_user', JSON.stringify(user)); + renderDashboard(user); + } } } catch (e) { console.error('Student session check error:', e); diff --git a/backend/app/Views/TeacherPortal.php b/backend/app/Views/TeacherPortal.php index 3c3eb25..3e20505 100644 --- a/backend/app/Views/TeacherPortal.php +++ b/backend/app/Views/TeacherPortal.php @@ -1219,13 +1219,19 @@ class TeacherPortal const user = data.data.user; const profile = data.data.profile || {}; localStorage.setItem('saqel_teacher_jwt', token); - if (user) { - localStorage.setItem('saqel_teacher_user', JSON.stringify({ ...user, ...profile })); - } document.cookie = "saqel_teacher_jwt=" + token + "; path=/; max-age=2592000; SameSite=Lax"; initWebSocket(token); - renderDashboard(user, profile); - await checkTeacherSession(token); + + const isNewTeacher = user?.is_new || user?.full_name === 'معلم جديد' || user?.name === 'معلم جديد' || user?.name === 'الأستاذ المعتمد'; + if (isNewTeacher) { + switchToOnboardingStep(); + } else { + if (user) { + localStorage.setItem('saqel_teacher_user', JSON.stringify({ ...user, ...profile })); + } + renderDashboard(user, profile); + await checkTeacherSession(token); + } } else { showError(data.message || 'رمز التحقق غير صحيح'); } diff --git a/backend/public/index.php b/backend/public/index.php index 08c62c3..6e1e53b 100644 --- a/backend/public/index.php +++ b/backend/public/index.php @@ -87,6 +87,8 @@ $router->post('/api/auth/otp/request', [\App\Controllers\AuthController::class, $router->post('/api/auth/otp/verify', [\App\Controllers\AuthController::class, 'verifyOtp'], [\App\Middlewares\RateLimitMiddleware::class]); $router->post('/api/auth/logout', [\App\Controllers\AuthController::class, 'logout'], [\App\Middlewares\AuthMiddleware::class]); $router->get('/api/auth/me', [\App\Controllers\AuthController::class, 'me'], [\App\Middlewares\AuthMiddleware::class]); +$router->get('/api/student/profile/status', [\App\Controllers\AuthController::class, 'studentProfileStatus'], [\App\Middlewares\AuthMiddleware::class]); +$router->post('/api/student/profile/setup', [\App\Controllers\AuthController::class, 'studentProfileSetup'], [\App\Middlewares\AuthMiddleware::class]); // Legacy / Direct Auth $router->post('/api/auth/register', [\App\Controllers\AuthController::class, 'register'], [\App\Middlewares\RateLimitMiddleware::class]);