fix(curriculum): robustly normalize student grade across app and backend to display subjects

This commit is contained in:
Hamza-Ayed
2026-09-09 01:28:31 +03:00
parent a8e895d5da
commit a4d6e34a40
4 changed files with 55 additions and 24 deletions
@@ -31,9 +31,23 @@ class GradeLevelModel {
GradeLevelModel(key: 'tawjihi_2008', name: 'الثاني ثانوي — توجيهي 2008', number: 12),
];
static String normalizeGradeKey(String raw) {
final clean = raw.toLowerCase().trim();
if (clean.contains('عاشر') || clean.contains('grade_10') || clean.contains('grade 10') || clean.contains('grade10') || clean == '10') return 'grade_10';
if (clean.contains('تاسع') || clean.contains('grade_9') || clean.contains('grade 9') || clean.contains('grade9') || clean == '9') return 'grade_9';
if (clean.contains('ثامن') || clean.contains('grade_8') || clean.contains('grade 8') || clean.contains('grade8') || clean == '8') return 'grade_8';
if (clean.contains('سابع') || clean.contains('grade_7') || clean.contains('grade 7') || clean.contains('grade7') || clean == '7') return 'grade_7';
if (clean.contains('سادس') || clean.contains('grade_6') || clean.contains('grade 6') || clean.contains('grade6') || clean == '6') return 'grade_6';
if (clean.contains('خامس') || clean.contains('grade_5') || clean.contains('grade 5') || clean.contains('grade5') || clean == '5') return 'grade_5';
if (clean.contains('حادي') || clean.contains('أول ثانوي') || clean.contains('grade_11') || clean.contains('grade 11') || clean == '11') return 'grade_11';
if (clean.contains('ثاني عشر') || clean.contains('توجيهي') || clean.contains('tawjihi') || clean.contains('grade_12') || clean.contains('grade 12') || clean == '12') return 'tawjihi_2008';
return raw;
}
static String getGradeName(String key) {
final norm = normalizeGradeKey(key);
final match = allK12Grades.firstWhere(
(g) => g.key == key || g.key == key.toLowerCase(),
(g) => g.key == norm || g.key == key || g.key == key.toLowerCase(),
orElse: () => GradeLevelModel(key: key, name: key, number: 0),
);
return match.name;
@@ -62,25 +76,20 @@ class CurriculumRepository {
final List<GradeLevelModel> available = [];
for (var grade in GradeLevelModel.allK12Grades) {
if (tree.containsKey(grade.key) || tree.containsKey(grade.key.replaceAll('grade_', ''))) {
final norm = GradeLevelModel.normalizeGradeKey(grade.key);
final shortKey = norm.replaceAll('grade_', '');
if (tree.containsKey(grade.key) || tree.containsKey(norm) || tree.containsKey(shortKey)) {
available.add(grade);
}
}
// If server has custom keys, include them as well
tree.forEach((key, val) {
if (!available.any((g) => g.key == key)) {
final name = (val is Map && val['name'] != null) ? val['name'].toString() : GradeLevelModel.getGradeName(key);
available.add(GradeLevelModel(key: key, name: name, number: 0));
}
});
return available;
return available.isNotEmpty ? available : GradeLevelModel.allK12Grades;
}
/// Fetch subjects for a specific grade level dynamically from Live Backend
Future<List<SubjectModel>> getSubjects({String gradeLevel = 'grade_10', String stream = 'scientific'}) async {
AppLogger.log('Fetching live curriculum tree from /api/curriculum/tree (Grade: $gradeLevel)...', tag: 'CURRICULUM_REPO');
final normGrade = GradeLevelModel.normalizeGradeKey(gradeLevel);
AppLogger.log('Fetching live curriculum tree from /api/curriculum/tree (Grade: $gradeLevel -> $normGrade)...', tag: 'CURRICULUM_REPO');
final tree = await getRawCurriculumTree();
if (tree.isEmpty) {
@@ -90,19 +99,37 @@ class CurriculumRepository {
final List<SubjectModel> liveSubjects = [];
// Match exact grade key, or fallback variants (e.g. grade_10 vs 10 vs tawjihi_2008)
// Match canonical key, exact key, or fuzzy iterate tree
Map<String, dynamic>? gradeData;
if (tree.containsKey(gradeLevel) && tree[gradeLevel] is Map) {
if (tree.containsKey(normGrade) && tree[normGrade] is Map) {
gradeData = Map<String, dynamic>.from(tree[normGrade]);
} else if (tree.containsKey(gradeLevel) && tree[gradeLevel] is Map) {
gradeData = Map<String, dynamic>.from(tree[gradeLevel]);
} else {
final shortKey = gradeLevel.replaceAll('grade_', '');
final shortKey = normGrade.replaceAll('grade_', '');
if (tree.containsKey(shortKey) && tree[shortKey] is Map) {
gradeData = Map<String, dynamic>.from(tree[shortKey]);
} else {
for (var entry in tree.entries) {
if (GradeLevelModel.normalizeGradeKey(entry.key) == normGrade) {
if (entry.value is Map) {
gradeData = Map<String, dynamic>.from(entry.value);
break;
}
}
if (entry.value is Map && entry.value['name'] != null) {
final name = entry.value['name'].toString();
if (GradeLevelModel.normalizeGradeKey(name) == normGrade || name.contains(normGrade)) {
gradeData = Map<String, dynamic>.from(entry.value);
break;
}
}
}
}
}
if (gradeData == null || gradeData['subjects'] == null || gradeData['subjects'] is! Map) {
AppLogger.log('No subjects found in grade $gradeLevel on server.', tag: 'CURRICULUM_REPO');
AppLogger.log('No subjects found in grade $gradeLevel ($normGrade) on server.', tag: 'CURRICULUM_REPO');
return [];
}
@@ -61,16 +61,17 @@ class CurriculumCubit extends Cubit<CurriculumState> {
super(CurriculumInitial());
Future<void> fetchSubjects({String gradeLevel = 'grade_10', String stream = 'scientific'}) async {
AppLogger.log('CurriculumCubit: Loading subjects for Grade: $gradeLevel ($stream)...', tag: 'CURRICULUM_CUBIT');
final normGrade = GradeLevelModel.normalizeGradeKey(gradeLevel);
AppLogger.log('CurriculumCubit: Loading subjects for Grade: $gradeLevel ($normGrade - $stream)...', tag: 'CURRICULUM_CUBIT');
emit(CurriculumLoading());
try {
final grades = await _repo.getAvailableGrades();
final subjects = await _repo.getSubjects(gradeLevel: gradeLevel, stream: stream);
final subjects = await _repo.getSubjects(gradeLevel: normGrade, stream: stream);
emit(CurriculumLoaded(
subjects: subjects,
filteredSubjects: subjects,
availableGrades: grades,
selectedGrade: gradeLevel,
selectedGrade: normGrade,
selectedStream: stream,
));
} catch (e) {
@@ -23,7 +23,7 @@ class _SubjectsGridScreenState extends State<SubjectsGridScreen> {
@override
void initState() {
super.initState();
final grade = widget.initialGrade ?? 'grade_10';
final grade = GradeLevelModel.normalizeGradeKey(widget.initialGrade ?? 'grade_10');
context.read<CurriculumCubit>().fetchSubjects(gradeLevel: grade);
}
+7 -4
View File
@@ -514,9 +514,11 @@ 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';
$rawGrade = (string)($user['grade_level'] ?? 'grade_10');
$normGrade = \App\Services\StudentAccessControlService::normalizeGrade($rawGrade);
if ($rawGrade === 'tawjihi_2008' || $rawGrade !== $normGrade) {
Database::query("UPDATE students SET grade_level = ?, updated_at = NOW() WHERE id = ?", [$normGrade, $userId]);
$user['grade_level'] = $normGrade;
}
$isCompleted = !empty($user['full_name']) && $user['full_name'] !== 'طالب جديد' && $user['full_name'] !== 'الطالب المتميز' && !empty($user['grade_level']);
$userData = [
@@ -669,7 +671,8 @@ class AuthController
$identityToken = $body['identity_token'] ?? '';
$fullName = trim((string)($body['full_name'] ?? ''));
$gradeLevel = trim((string)($body['grade_level'] ?? 'grade_10'));
$rawGrade = trim((string)($body['grade_level'] ?? 'grade_10'));
$gradeLevel = \App\Services\StudentAccessControlService::normalizeGrade($rawGrade);
$stream = trim((string)($body['stream'] ?? 'scientific'));
$nationalId = trim((string)($body['national_id'] ?? ''));
$deviceFingerprint = trim((string)($request->getHeader('x-device-fingerprint', '')));