fix(curriculum): robustly normalize student grade across app and backend to display subjects
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user