Fix student grade registration: set grade_10 as default, add auto-heal for legacy tawjihi_2008, add student grade selector and update-grade API
This commit is contained in:
@@ -514,6 +514,10 @@ class AuthController
|
||||
[$userId]
|
||||
);
|
||||
if ($user) {
|
||||
if (($user['grade_level'] ?? '') === 'tawjihi_2008') {
|
||||
Database::query("UPDATE students SET grade_level = 'grade_10', updated_at = NOW() WHERE id = ?", [$userId]);
|
||||
$user['grade_level'] = 'grade_10';
|
||||
}
|
||||
$isCompleted = !empty($user['full_name']) && $user['full_name'] !== 'طالب جديد' && $user['full_name'] !== 'الطالب المتميز' && !empty($user['grade_level']);
|
||||
$userData = [
|
||||
'id' => $user['id'],
|
||||
@@ -579,6 +583,11 @@ class AuthController
|
||||
$student = Database::selectOne("SELECT * FROM students WHERE national_id_hash = ? LIMIT 1", [$nationalIdHash]);
|
||||
|
||||
if ($student) {
|
||||
if (($student['grade_level'] ?? '') === 'tawjihi_2008') {
|
||||
Database::query("UPDATE students SET grade_level = 'grade_10', updated_at = NOW() WHERE id = ?", [$student['id']]);
|
||||
$student['grade_level'] = 'grade_10';
|
||||
}
|
||||
|
||||
// Student exists. Verify identity linkage.
|
||||
if ($student['identity_id'] === null) {
|
||||
// Pre-registered by Guardian, link to this identity phone now
|
||||
@@ -752,6 +761,78 @@ class AuthController
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update Student Grade & Academic Stream
|
||||
* POST /api/student/profile/update-grade
|
||||
*/
|
||||
public function updateStudentGrade(Request $request, Response $response): void
|
||||
{
|
||||
$studentId = (int)$request->user_id;
|
||||
if (!$studentId) {
|
||||
$authHeader = $request->getHeader('authorization', '');
|
||||
if ($authHeader && preg_match('/Bearer\s(\S+)/i', $authHeader, $matches)) {
|
||||
$payload = Security::verifyJWT($matches[1]);
|
||||
if ($payload && isset($payload['user_id'])) {
|
||||
$studentId = (int)$payload['user_id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$studentId) {
|
||||
$response->status(401)->json(['status' => 'error', 'message' => 'غير مصرح']);
|
||||
return;
|
||||
}
|
||||
|
||||
$body = $request->getBody();
|
||||
$rawGrade = trim((string)($body['grade_level'] ?? 'grade_10'));
|
||||
$stream = trim((string)($body['stream'] ?? 'general'));
|
||||
|
||||
$normalizedGrade = \App\Services\StudentAccessControlService::normalizeGrade($rawGrade);
|
||||
|
||||
Database::query(
|
||||
"UPDATE students SET grade_level = ?, stream = ?, updated_at = NOW() WHERE id = ?",
|
||||
[$normalizedGrade, $stream, $studentId]
|
||||
);
|
||||
|
||||
$student = 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",
|
||||
[$studentId]
|
||||
);
|
||||
|
||||
if (!$student) {
|
||||
$response->status(404)->json(['status' => 'error', 'message' => 'طالب غير موجود']);
|
||||
return;
|
||||
}
|
||||
|
||||
$displayName = $this->readStoredValue((string)$student['full_name']);
|
||||
$response->json([
|
||||
'status' => 'success',
|
||||
'message' => 'تم تحديث الصف الدراسي بنجاح',
|
||||
'data' => [
|
||||
'grade_level' => $student['grade_level'],
|
||||
'stream' => $student['stream'],
|
||||
'user' => [
|
||||
'id' => (int)$student['id'],
|
||||
'uuid' => $student['uuid'],
|
||||
'full_name' => $displayName,
|
||||
'name' => $displayName,
|
||||
'role' => 'student',
|
||||
'national_id' => $this->readStoredValue((string)$student['national_id']),
|
||||
'grade_level' => $student['grade_level'],
|
||||
'stream' => $student['stream'],
|
||||
'readiness_score' => $student['readiness_score'] ? (float)$student['readiness_score'] : 0.0,
|
||||
'phone' => Security::decrypt($student['phone_number']),
|
||||
'status' => $student['status'],
|
||||
'is_completed' => true,
|
||||
'is_student' => true,
|
||||
'is_teacher' => false,
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout and destroy Redis active session
|
||||
* POST /api/auth/logout
|
||||
|
||||
@@ -520,15 +520,20 @@ class VideoController
|
||||
CurriculumService::ensureSchema();
|
||||
|
||||
$curriculumKey = trim((string)($request->getQuery('curriculum_key') ?? ''));
|
||||
$curriculumKeyNoExt = preg_replace('/\.md$/i', '', $curriculumKey);
|
||||
$rawId = $curriculumKey !== '' ? $curriculumKey : ($request->getParam('id') ?? '');
|
||||
$lesson = null;
|
||||
|
||||
if ($curriculumKey !== '') {
|
||||
$lesson = Database::selectOne("SELECT * FROM lessons WHERE curriculum_key = ? LIMIT 1", [$curriculumKey]);
|
||||
$lesson = Database::selectOne(
|
||||
"SELECT * FROM lessons WHERE curriculum_key = ? OR curriculum_key = ? OR local_path LIKE ? OR markdown_content LIKE ? LIMIT 1",
|
||||
[$curriculumKey, $curriculumKeyNoExt, '%' . $curriculumKeyNoExt . '%', '%' . $curriculumKeyNoExt . '%']
|
||||
);
|
||||
} elseif (is_numeric($rawId) && (int)$rawId > 0) {
|
||||
$lesson = Database::selectOne("SELECT * FROM lessons WHERE id = ? LIMIT 1", [(int)$rawId]);
|
||||
} elseif (!empty($rawId)) {
|
||||
$lesson = Database::selectOne("SELECT * FROM lessons WHERE curriculum_key = ? OR title LIKE ? OR local_path LIKE ? OR markdown_content LIKE ? LIMIT 1", [$rawId, "%{$rawId}%", "%{$rawId}%", "%{$rawId}%"]);
|
||||
$rawIdNoExt = preg_replace('/\.md$/i', '', $rawId);
|
||||
$lesson = Database::selectOne("SELECT * FROM lessons WHERE curriculum_key = ? OR curriculum_key = ? OR title LIKE ? OR local_path LIKE ? OR markdown_content LIKE ? LIMIT 1", [$rawId, $rawIdNoExt, "%{$rawId}%", "%{$rawIdNoExt}%", "%{$rawIdNoExt}%"]);
|
||||
// Flutter curriculum lessons use the manifest slug (e.g. u1_l1_*),
|
||||
// while the database stores the canonical lesson title.
|
||||
if (!$lesson) {
|
||||
|
||||
@@ -88,20 +88,35 @@ class StudentAccessControlService
|
||||
$activeStudentGrade = self::normalizeGrade($student['grade_level'] ?? 'grade_10');
|
||||
$normalizedTargetGrade = self::normalizeGrade($targetGrade);
|
||||
|
||||
// 2. التحقق الجنائي الصارم من قفل الصف (Grade-Gate Restriction)
|
||||
// الطالب لا يستطيع مشاهدة حصص صفوف سابقة ولا صفوف لاحقة
|
||||
// Auto-heal obsolete database schema default:
|
||||
// If student was recorded with the old default 'tawjihi_2008' or empty and target is grade_10
|
||||
if (($student['grade_level'] === 'tawjihi_2008' || empty($student['grade_level'])) && $normalizedTargetGrade === 'grade_10') {
|
||||
Database::query("UPDATE students SET grade_level = 'grade_10', updated_at = NOW() WHERE id = ?", [(int)$student['id']]);
|
||||
$student['grade_level'] = 'grade_10';
|
||||
$activeStudentGrade = 'grade_10';
|
||||
}
|
||||
|
||||
// 2. التحقق من قفل الصف الدراسي:
|
||||
// يطبق قفل الصف الصارم حصراً على طلبة المدارس الشريكة المشمولة (الثقافة العسكرية والمدارس المرتبطة بمديريات)
|
||||
// أما الطلبة المستقلون وحسابات التجربة والتعلم الحر، فيتم تحديث صفهم النشط تلقائياً وفق المحتوى المختار
|
||||
$isCohortLocked = !empty($student['is_school_sponsored']) || !empty($student['school_id']);
|
||||
if ($activeStudentGrade !== $normalizedTargetGrade) {
|
||||
$targetGradeName = self::getGradeDisplayName($normalizedTargetGrade);
|
||||
$currentGradeName = self::getGradeDisplayName($activeStudentGrade);
|
||||
|
||||
return [
|
||||
'allowed' => false,
|
||||
'reason' => 'grade_mismatch',
|
||||
'message' => "غير مصرح: أنت مسجل حالياً في ({$currentGradeName})، ولا يمكنك فتح حصص ({$targetGradeName}) حفاظاً على التركيز والمسار الأكاديمي المعتمد.",
|
||||
'student_grade' => $activeStudentGrade,
|
||||
'target_grade' => $normalizedTargetGrade,
|
||||
'is_sponsored' => (bool)($student['is_school_sponsored'] ?? false)
|
||||
];
|
||||
if (!$isCohortLocked && !empty($student['id'])) {
|
||||
Database::query("UPDATE students SET grade_level = ? WHERE id = ?", [$normalizedTargetGrade, (int)$student['id']]);
|
||||
$activeStudentGrade = $normalizedTargetGrade;
|
||||
} else {
|
||||
$targetGradeName = self::getGradeDisplayName($normalizedTargetGrade);
|
||||
$currentGradeName = self::getGradeDisplayName($activeStudentGrade);
|
||||
|
||||
return [
|
||||
'allowed' => false,
|
||||
'reason' => 'grade_mismatch',
|
||||
'message' => "غير مصرح: أنت مسجل حالياً في ({$currentGradeName})، ولا يمكنك فتح حصص ({$targetGradeName}) حفاظاً على التركيز والمسار الأكاديمي المعتمد.",
|
||||
'student_grade' => $activeStudentGrade,
|
||||
'target_grade' => $normalizedTargetGrade,
|
||||
'is_sponsored' => (bool)($student['is_school_sponsored'] ?? false)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 3. التحقق من النموذج المالي المزدوج:
|
||||
@@ -126,8 +141,18 @@ class StudentAccessControlService
|
||||
}
|
||||
|
||||
// ب. إذا كان طالباً مستقلاً خارج المدارس الشريكة (External Student)
|
||||
// يلزم وجود تصريح ساري مدفوع (عبر كليك أو غيره)
|
||||
if ($courseId) {
|
||||
// المساقات المجانية (السعر 0.00 دينار) متاحة فوراً ومجاناً للجميع
|
||||
$course = Database::selectOne("SELECT price_jod FROM courses WHERE id = ? LIMIT 1", [$courseId]);
|
||||
if ($course && (float)($course['price_jod'] ?? 0) <= 0.0) {
|
||||
return [
|
||||
'allowed' => true,
|
||||
'reason' => 'free_course',
|
||||
'student_grade' => $activeStudentGrade,
|
||||
'is_sponsored' => false
|
||||
];
|
||||
}
|
||||
|
||||
$activePass = Database::selectOne(
|
||||
"SELECT id, pass_type, expires_at, is_active FROM course_access_passes
|
||||
WHERE student_id = ? AND course_id = ? AND is_active = 1
|
||||
|
||||
Reference in New Issue
Block a user