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:
@@ -20,7 +20,9 @@ class ErrorNotebookSummary {
|
||||
});
|
||||
|
||||
factory ErrorNotebookSummary.fromJson(Map<String, dynamic> json) {
|
||||
final rawBySubject = json['by_subject'] as Map<String, dynamic>? ?? {};
|
||||
final rawBySubject = json['by_subject'] is Map
|
||||
? Map<String, dynamic>.from(json['by_subject'] as Map)
|
||||
: <String, dynamic>{};
|
||||
final bySub = <String, int>{};
|
||||
rawBySubject.forEach((k, v) {
|
||||
bySub[k] = (v is num) ? v.toInt() : 0;
|
||||
|
||||
@@ -119,6 +119,27 @@ class AuthRepository {
|
||||
throw ApiException('فشل استكمال فتح الملف الأكاديمي');
|
||||
}
|
||||
|
||||
Future<UserModel> updateStudentGrade(String gradeLevel, {String stream = 'general'}) async {
|
||||
final res = await _api.post(
|
||||
'/api/student/profile/update-grade',
|
||||
body: {
|
||||
'grade_level': gradeLevel,
|
||||
'stream': stream,
|
||||
},
|
||||
requiresAuth: true,
|
||||
);
|
||||
|
||||
if (res is Map<String, dynamic> && res['data'] != null) {
|
||||
final data = res['data'];
|
||||
if (data['user'] != null) {
|
||||
final user = UserModel.fromJson(Map<String, dynamic>.from(data['user']));
|
||||
await _storage.saveUser(user);
|
||||
return user;
|
||||
}
|
||||
}
|
||||
throw ApiException('فشل تحديث الصف الدراسي');
|
||||
}
|
||||
|
||||
Future<UserModel?> getMe() async {
|
||||
try {
|
||||
final res = await _api.get(AppConfig.meEndpoint);
|
||||
|
||||
@@ -169,6 +169,22 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> updateGrade(String gradeLevel, {String stream = 'general'}) async {
|
||||
final currentState = state;
|
||||
if (currentState is! Authenticated) return;
|
||||
AppLogger.log('Updating student grade to $gradeLevel ($stream)...', tag: 'AUTH_CUBIT');
|
||||
emit(AuthLoading());
|
||||
try {
|
||||
final updatedUser = await _authRepo.updateStudentGrade(gradeLevel, stream: stream);
|
||||
AppLogger.log('Grade updated successfully -> ${updatedUser.gradeLevel}', tag: 'AUTH_CUBIT');
|
||||
emit(Authenticated(user: updatedUser, activeRole: 'student'));
|
||||
} catch (e) {
|
||||
AppLogger.error('Update Grade Exception', error: e, tag: 'AUTH_CUBIT');
|
||||
emit(AuthError(e.toString()));
|
||||
emit(currentState);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> switchRole(String newRole) async {
|
||||
final currentState = state;
|
||||
if (currentState is Authenticated) {
|
||||
|
||||
@@ -93,7 +93,8 @@ class VideoPlaybackCubit extends Cubit<VideoPlaybackState> {
|
||||
final storageKey = _getLessonStorageKey(lesson);
|
||||
|
||||
try {
|
||||
var playback = await _repo.getLessonPlayback(lesson.markdownFilePath ?? lesson.id, subjectId: subject?.id);
|
||||
final cleanKey = (lesson.markdownFilePath ?? lesson.id).replaceAll(RegExp(r'\.md$'), '');
|
||||
var playback = await _repo.getLessonPlayback(cleanKey, subjectId: subject?.id);
|
||||
if (playback.videoUrl.isEmpty) {
|
||||
throw StateError('لم يربط الخادم فيديو R2 بهذا الدرس بعد.');
|
||||
}
|
||||
|
||||
@@ -207,16 +207,16 @@ class _SubjectsGridScreenState extends State<SubjectsGridScreen> {
|
||||
child: const Icon(CupertinoIcons.lock_shield_fill, color: AppColors.saqelCyan, size: 18),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'الصف العاشر الأساسي — المسار الأكاديمي المعتمد',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 13.5),
|
||||
'${GradeLevelModel.getGradeName(state.selectedGrade)} — المسار الأكاديمي المعتمد',
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 13.5),
|
||||
),
|
||||
SizedBox(height: 2),
|
||||
Text(
|
||||
const SizedBox(height: 2),
|
||||
const Text(
|
||||
'خطة دراسية موحدة ومطابقة للمنهاج الوزاري الأردني المطور',
|
||||
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 11),
|
||||
),
|
||||
|
||||
@@ -13,6 +13,7 @@ import '../../widgets/luxury_widgets.dart';
|
||||
import '../curriculum/subjects_grid_screen.dart';
|
||||
import '../notebook/smart_error_notebook_screen.dart';
|
||||
import '../vocational/vocational_training_screen.dart';
|
||||
import '../../../data/repositories/curriculum_repository.dart';
|
||||
|
||||
class UnifiedHomeScreen extends StatefulWidget {
|
||||
final UserModel user;
|
||||
@@ -38,6 +39,14 @@ class _UnifiedHomeScreenState extends State<UnifiedHomeScreen> {
|
||||
_refreshData();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant UnifiedHomeScreen oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.user.gradeLevel != widget.user.gradeLevel || oldWidget.user.stream != widget.user.stream) {
|
||||
_refreshData();
|
||||
}
|
||||
}
|
||||
|
||||
void _refreshData() {
|
||||
if (_currentRole == 'student') {
|
||||
context.read<StudentCubit>().fetchLessons(
|
||||
@@ -57,6 +66,157 @@ class _UnifiedHomeScreenState extends State<UnifiedHomeScreen> {
|
||||
_refreshData();
|
||||
}
|
||||
|
||||
void _showGradeSelectionModal(BuildContext context) {
|
||||
String tempGrade = widget.user.gradeLevel ?? 'grade_10';
|
||||
if (tempGrade == 'tawjihi_2008') tempGrade = 'grade_10';
|
||||
String tempStream = widget.user.stream ?? 'scientific';
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
backgroundColor: Colors.transparent,
|
||||
isScrollControlled: true,
|
||||
builder: (ctx) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setModalState) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.darkSurface,
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(28)),
|
||||
border: Border(top: BorderSide(color: AppColors.darkCardBorder, width: 1.5)),
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 44,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white24,
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: const Icon(CupertinoIcons.book_fill, color: AppColors.saqelCyan, size: 24),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'تحديد وتعديل الصف الدراسي 🎓',
|
||||
style: TextStyle(color: Colors.white, fontSize: 17, fontWeight: FontWeight.w800),
|
||||
),
|
||||
SizedBox(height: 3),
|
||||
Text(
|
||||
'اختر صفك لفتح المناهج الوزارية والشروحات المعتمدة',
|
||||
style: TextStyle(color: AppColors.textSecondaryDark, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
...[
|
||||
('grade_10', 'الصف العاشر الأساسي (Grade 10)', 'يشمل الرياضيات المطور، الفيزياء، والإنجليزية'),
|
||||
('grade_11', 'الصف الحادي عشر (الأول ثانوي)', 'المسار الأكاديمي للفرعين العلمي والأدبي'),
|
||||
('grade_12', 'الصف الثاني عشر (التوجيهي الوزاري)', 'منهاج الثانوية العامة التوجيهي الوزاري'),
|
||||
].map((g) {
|
||||
final isSelected = tempGrade == g.$1;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
setModalState(() {
|
||||
tempGrade = g.$1;
|
||||
if (tempGrade == 'grade_10') tempStream = 'general';
|
||||
});
|
||||
},
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.saqelCyan.withAlpha(25) : AppColors.darkBackground,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppColors.saqelCyan : AppColors.darkCardBorder,
|
||||
width: isSelected ? 1.5 : 1.0,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
isSelected ? CupertinoIcons.checkmark_circle_fill : CupertinoIcons.circle,
|
||||
color: isSelected ? AppColors.saqelCyan : AppColors.textMutedDark,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
g.$2,
|
||||
style: TextStyle(
|
||||
color: isSelected ? Colors.white : AppColors.textSecondaryDark,
|
||||
fontWeight: isSelected ? FontWeight.w800 : FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
g.$3,
|
||||
style: const TextStyle(color: AppColors.textMutedDark, fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 16),
|
||||
LuxuryButton(
|
||||
text: 'حفظ الصف وتحديث المسار 🚀',
|
||||
onPressed: () async {
|
||||
Navigator.of(ctx).pop();
|
||||
await context.read<AuthCubit>().updateGrade(tempGrade, stream: tempStream);
|
||||
if (mounted) {
|
||||
_refreshData();
|
||||
SaqelToast.showSuccess(
|
||||
context,
|
||||
'تم ضبط صفك بنجاح: ${GradeLevelModel.getGradeName(tempGrade)}',
|
||||
title: 'تحديث المسار الدراسي ✨',
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isStudent = _currentRole == 'student';
|
||||
@@ -129,10 +289,39 @@ class _UnifiedHomeScreenState extends State<UnifiedHomeScreen> {
|
||||
widget.user.name,
|
||||
style: AppTypography.titleMedium.copyWith(color: Colors.white),
|
||||
),
|
||||
Text(
|
||||
isStudent ? AppStrings.studentPortal : AppStrings.guardianPortal,
|
||||
style: AppTypography.labelSmall.copyWith(color: AppColors.textMutedDark),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
if (isStudent)
|
||||
GestureDetector(
|
||||
onTap: () => _showGradeSelectionModal(context),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(80), width: 0.8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
GradeLevelModel.getGradeName(widget.user.gradeLevel ?? 'grade_10'),
|
||||
style: const TextStyle(
|
||||
color: AppColors.saqelCyan,
|
||||
fontSize: 10.5,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(CupertinoIcons.chevron_down, color: AppColors.saqelCyan, size: 10),
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
AppStrings.guardianPortal,
|
||||
style: AppTypography.labelSmall.copyWith(color: AppColors.textMutedDark),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
@@ -466,18 +655,29 @@ class _UnifiedHomeScreenState extends State<UnifiedHomeScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
),
|
||||
child: Text(
|
||||
widget.user.gradeLevel ?? 'توجيهي 2008',
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.saqelCyan,
|
||||
GestureDetector(
|
||||
onTap: () => _showGradeSelectionModal(context),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.saqelCyan.withAlpha(30),
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: AppColors.saqelCyan.withAlpha(80), width: 0.8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
GradeLevelModel.getGradeName(widget.user.gradeLevel ?? 'grade_10'),
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: AppColors.saqelCyan,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const Icon(CupertinoIcons.pencil, color: AppColors.saqelCyan, size: 11),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user