Update Saqel Platform: 2026-09-08 18:19:28

This commit is contained in:
Hamza-Ayed
2026-09-08 18:19:28 +03:00
parent 75451ebc3f
commit e5cbd89323
22 changed files with 1537 additions and 595 deletions
@@ -7,6 +7,7 @@ import '../../data/repositories/teacher_repository.dart';
abstract class TeacherAuthState {}
class TeacherAuthInitial extends TeacherAuthState {}
class TeacherAuthLoading extends TeacherAuthState {}
class TeacherAuthOtpSent extends TeacherAuthState {
@@ -42,13 +43,16 @@ class TeacherAuthCubit extends Cubit<TeacherAuthState> {
try {
final token = await _storage.getToken();
if (token == null || token.isEmpty) {
AppLogger.log('No teacher token -> Unauthenticated', tag: 'TEACHER_AUTH');
AppLogger.log('No teacher token -> Unauthenticated',
tag: 'TEACHER_AUTH');
emit(TeacherUnauthenticated());
return;
}
final profile = await repository.getProfileStatus();
AppLogger.log('Teacher authenticated: ${profile.name} (${profile.specialization})', tag: 'TEACHER_AUTH');
AppLogger.log(
'Teacher authenticated: ${profile.name} (${profile.specialization})',
tag: 'TEACHER_AUTH');
emit(TeacherAuthenticated(profile: profile));
} catch (e) {
AppLogger.error('Session check failed', error: e, tag: 'TEACHER_AUTH');
@@ -67,7 +71,8 @@ class TeacherAuthCubit extends Cubit<TeacherAuthState> {
if (res['status'] == 'success') {
emit(TeacherAuthOtpSent(phoneNumber: phoneNumber));
} else {
emit(TeacherAuthError(res['message']?.toString() ?? 'فشل إرسال رمز التحقق'));
emit(TeacherAuthError(
res['message']?.toString() ?? 'فشل إرسال رمز التحقق'));
}
} catch (e) {
emit(TeacherAuthError(e.toString()));
@@ -84,9 +89,13 @@ class TeacherAuthCubit extends Cubit<TeacherAuthState> {
}) async {
emit(TeacherAuthLoading());
try {
final res = await repository.verifyOtp(phoneNumber, otp, fullName: fullName);
final res =
await repository.verifyOtp(phoneNumber, otp, fullName: fullName);
if (res['status'] == 'success') {
if ((fullName ?? '').trim().isNotEmpty && (specialization ?? '').trim().isNotEmpty && (schoolName ?? '').trim().isNotEmpty && (gradesTaught?.isNotEmpty ?? false)) {
if ((fullName ?? '').trim().isNotEmpty &&
(specialization ?? '').trim().isNotEmpty &&
(schoolName ?? '').trim().isNotEmpty &&
(gradesTaught?.isNotEmpty ?? false)) {
try {
await repository.setupProfile(
fullName: fullName!.trim(),
@@ -100,7 +109,8 @@ class TeacherAuthCubit extends Cubit<TeacherAuthState> {
final profile = await repository.getProfileStatus();
emit(TeacherAuthenticated(profile: profile));
} else {
emit(TeacherAuthError(res['message']?.toString() ?? 'رمز التحقق غير صحيح'));
emit(TeacherAuthError(
res['message']?.toString() ?? 'رمز التحقق غير صحيح'));
}
} catch (e) {
emit(TeacherAuthError(e.toString()));
@@ -63,7 +63,8 @@ class TeacherMonetizationState {
bool? isLoading,
}) {
return TeacherMonetizationState(
institutionalStudents: institutionalStudents ?? this.institutionalStudents,
institutionalStudents:
institutionalStudents ?? this.institutionalStudents,
studentSubscribers: studentSubscribers ?? this.studentSubscribers,
pricePerCourse: pricePerCourse ?? this.pricePerCourse,
grossRevenue: grossRevenue ?? this.grossRevenue,
@@ -86,7 +87,7 @@ class TeacherMonetizationCubit extends Cubit<TeacherMonetizationState> {
TeacherMonetizationCubit({required this.repository})
: super(const TeacherMonetizationState(
institutionalStudents: 83,
institutionalStudents: 0,
studentSubscribers: 0,
pricePerCourse: 20.0,
grossRevenue: 0.0,
@@ -95,7 +96,7 @@ class TeacherMonetizationCubit extends Cubit<TeacherMonetizationState> {
platformShare: 0.0,
availableBalance: 0.0,
totalWithdrawn: 0.0,
cliqAlias: '0798583052@CLIQ',
cliqAlias: '',
courses: [],
payoutRequests: [],
isSubmittingPayout: false,
@@ -108,20 +109,26 @@ class TeacherMonetizationCubit extends Cubit<TeacherMonetizationState> {
final data = await repository.getMonetizationDashboard();
final courses = await repository.getPublishedCourses();
final audience = data['audience_breakdown'] as Map<String, dynamic>? ?? {};
final inst = audience['institutional_students'] as Map<String, dynamic>? ?? {};
final mkt = audience['marketplace_students'] as Map<String, dynamic>? ?? {};
final audience =
data['audience_breakdown'] as Map<String, dynamic>? ?? {};
final inst =
audience['institutional_students'] as Map<String, dynamic>? ?? {};
final mkt =
audience['marketplace_students'] as Map<String, dynamic>? ?? {};
final wallet = data['wallet'] as Map<String, dynamic>? ?? {};
final instCount = (inst['count'] as num?)?.toDouble() ?? 83.0;
final instCount = (inst['count'] as num?)?.toDouble() ?? 0.0;
final paidCount = (mkt['count'] as num?)?.toDouble() ?? 0.0;
final gross = (data['gross_revenue_jod'] as num?)?.toDouble() ?? (paidCount * 20.0);
final gross =
(data['gross_revenue_jod'] as num?)?.toDouble() ?? (paidCount * 20.0);
final tShare = gross * 0.55;
final dShare = gross * 0.15;
final pShare = gross * 0.30;
final avail = (wallet['available_balance_jod'] as num?)?.toDouble() ?? tShare;
final withdrawn = (wallet['total_withdrawn_jod'] as num?)?.toDouble() ?? 0.0;
final alias = wallet['cliq_payout_alias']?.toString() ?? '0798583052@CLIQ';
final avail =
(wallet['available_balance_jod'] as num?)?.toDouble() ?? tShare;
final withdrawn =
(wallet['total_withdrawn_jod'] as num?)?.toDouble() ?? 0.0;
final alias = wallet['cliq_payout_alias']?.toString() ?? '';
List<TeacherPayoutRequestModel> payouts = [];
if (wallet['recent_payouts'] is List) {
@@ -176,8 +183,11 @@ class TeacherMonetizationCubit extends Cubit<TeacherMonetizationState> {
cliqAlias: cliqAlias,
amountJod: amountJod,
);
final updatedList = List<TeacherPayoutRequestModel>.from(state.payoutRequests)..insert(0, req);
final newAvail = (state.availableBalance - amountJod).clamp(0.0, double.infinity);
final updatedList =
List<TeacherPayoutRequestModel>.from(state.payoutRequests)
..insert(0, req);
final newAvail =
(state.availableBalance - amountJod).clamp(0.0, double.infinity);
final newWithdrawn = state.totalWithdrawn + amountJod;
emit(state.copyWith(
@@ -58,16 +58,21 @@ class TeacherQnACubit extends Cubit<TeacherQnAState> {
Future<void> loadQnA() async {
emit(state.copyWith(isLoading: true));
final hwList = await repository.getHomeworks();
final doubtsList = await repository.getStudentDoubts();
final conversationsList = await repository.getConversations();
emit(state.copyWith(
homeworks: hwList,
doubts: doubtsList,
conversations: conversationsList,
isLoading: false,
));
try {
final results = await Future.wait([
repository.getHomeworks(),
repository.getStudentDoubts(),
repository.getConversations(),
]);
emit(state.copyWith(
homeworks: results[0] as List<TeacherHomeworkModel>,
doubts: results[1] as List<StudentDoubtModel>,
conversations: results[2] as List<ChatConversationModel>,
isLoading: false,
));
} catch (_) {
emit(state.copyWith(isLoading: false));
}
}
void dispatchNewHomework({
@@ -86,14 +91,19 @@ class TeacherQnACubit extends Cubit<TeacherQnAState> {
final target = state.doubts[targetIndex];
final replyText = voiceText?.trim() ?? '';
if (replyText.isEmpty) {
throw StateError('لا يمكن تسجيل رد صوتي دون ملف صوت حقيقي؛ أرسل رداً نصياً حالياً.');
throw StateError(
'لا يمكن تسجيل رد صوتي دون ملف صوت حقيقي؛ أرسل رداً نصياً حالياً.');
}
await repository.sendMessage(
receiverId: target.studentId,
message: replyText,
messageType: 'text',
);
final updated = state.doubts.map((doubt) => doubt.id == doubtId ? doubt.copyWith(replied: true, voiceReply: false) : doubt).toList();
final updated = state.doubts
.map((doubt) => doubt.id == doubtId
? doubt.copyWith(replied: true, voiceReply: false)
: doubt)
.toList();
emit(state.copyWith(doubts: updated));
}
@@ -106,8 +116,8 @@ class TeacherQnACubit extends Cubit<TeacherQnAState> {
final success = await repository.broadcastAnnouncement(
title: title,
message: explanationText,
messageType: 'voice',
mediaUrl: 'https://saqel.intaleqapp.com/assets/audio/teacher_broadcast.mp3',
messageType: 'text',
mediaUrl: null,
gradeLevel: gradeLevel,
);
emit(state.copyWith(isBroadcasting: false));
@@ -17,6 +17,14 @@ class TeacherStudioState {
final String lessonTitle;
final double durationMinutes;
final String gradeLevel;
final Map<String, dynamic> curriculumTree;
final String gradeKey;
final String subjectKey;
final String semesterKey;
final String unitKey;
final String lessonKey;
final bool isCurriculumLoading;
final String? errorMessage;
final String? selectedFileName;
final double? selectedFileSizeMb;
final String? selectedFilePath;
@@ -30,7 +38,15 @@ class TeacherStudioState {
const TeacherStudioState({
required this.lessonTitle,
required this.durationMinutes,
this.gradeLevel = 'الصف العاشر الأساسي',
this.gradeLevel = '',
this.curriculumTree = const {},
this.gradeKey = '',
this.subjectKey = '',
this.semesterKey = '',
this.unitKey = '',
this.lessonKey = '',
this.isCurriculumLoading = false,
this.errorMessage,
this.selectedFileName,
this.selectedFileSizeMb,
this.selectedFilePath,
@@ -46,6 +62,15 @@ class TeacherStudioState {
String? lessonTitle,
double? durationMinutes,
String? gradeLevel,
Map<String, dynamic>? curriculumTree,
String? gradeKey,
String? subjectKey,
String? semesterKey,
String? unitKey,
String? lessonKey,
bool? isCurriculumLoading,
String? errorMessage,
bool clearError = false,
String? selectedFileName,
double? selectedFileSizeMb,
String? selectedFilePath,
@@ -60,6 +85,14 @@ class TeacherStudioState {
lessonTitle: lessonTitle ?? this.lessonTitle,
durationMinutes: durationMinutes ?? this.durationMinutes,
gradeLevel: gradeLevel ?? this.gradeLevel,
curriculumTree: curriculumTree ?? this.curriculumTree,
gradeKey: gradeKey ?? this.gradeKey,
subjectKey: subjectKey ?? this.subjectKey,
semesterKey: semesterKey ?? this.semesterKey,
unitKey: unitKey ?? this.unitKey,
lessonKey: lessonKey ?? this.lessonKey,
isCurriculumLoading: isCurriculumLoading ?? this.isCurriculumLoading,
errorMessage: clearError ? null : errorMessage ?? this.errorMessage,
selectedFileName: selectedFileName ?? this.selectedFileName,
selectedFileSizeMb: selectedFileSizeMb ?? this.selectedFileSizeMb,
selectedFilePath: selectedFilePath ?? this.selectedFilePath,
@@ -78,8 +111,8 @@ class TeacherStudioCubit extends Cubit<TeacherStudioState> {
TeacherStudioCubit({required this.repository})
: super(const TeacherStudioState(
lessonTitle: 'شرح قاعدة لنتز والحث الكهرومغناطيسي — فيزياء 2008',
durationMinutes: 13.0,
lessonTitle: '',
durationMinutes: 20.0,
isAuditing: false,
auditResult: null,
));
@@ -96,6 +129,137 @@ class TeacherStudioCubit extends Cubit<TeacherStudioState> {
emit(state.copyWith(gradeLevel: grade));
}
Map<String, dynamic> _map(dynamic value) =>
value is Map ? Map<String, dynamic>.from(value) : {};
Map<String, dynamic> get _subjects =>
_map(state.curriculumTree[state.gradeKey]?['subjects']);
Map<String, dynamic> get _semesters =>
_map(_subjects[state.subjectKey]?['semesters']);
Map<String, dynamic> get _units =>
_map(_semesters[state.semesterKey]?['units']);
Future<void> loadCurriculum() async {
if (state.curriculumTree.isNotEmpty || state.isCurriculumLoading) return;
emit(state.copyWith(isCurriculumLoading: true, clearError: true));
try {
final results = await Future.wait(
[repository.getCurriculumTree(), repository.getProfileStatus()]);
final rawTree = Map<String, dynamic>.from(results[0] as Map);
final profile = results[1] as TeacherProfileModel;
final allowedGrades = profile.gradesTaught
.map(_normalized)
.where((e) => e.isNotEmpty)
.toList();
final specialization = _normalized(profile.specialization);
final tree = <String, dynamic>{};
for (final gradeEntry in rawTree.entries) {
final gradeNode = _map(gradeEntry.value);
final gradeName =
_normalized(gradeNode['name']?.toString() ?? gradeEntry.key);
if (allowedGrades.isNotEmpty &&
!allowedGrades
.any((g) => gradeName.contains(g) || g.contains(gradeName)))
continue;
final allSubjects = _map(gradeNode['subjects']);
if (specialization.isNotEmpty) {
final matched = Map<String, dynamic>.fromEntries(
allSubjects.entries.where((entry) {
final subjectName =
_normalized('${entry.key} ${_map(entry.value)['name'] ?? ''}');
return subjectName.contains(specialization) ||
specialization.contains(subjectName) ||
specialization.split(' ').any(
(word) => word.length > 3 && subjectName.contains(word));
}));
if (matched.isNotEmpty) gradeNode['subjects'] = matched;
}
tree[gradeEntry.key] = gradeNode;
}
final key = tree.keys.isNotEmpty ? tree.keys.first : '';
emit(state.copyWith(curriculumTree: tree, isCurriculumLoading: false));
selectGrade(key);
} catch (e) {
emit(state.copyWith(
isCurriculumLoading: false, errorMessage: e.toString()));
}
}
String _normalized(String value) => value
.toLowerCase()
.replaceAll(RegExp(r'[^\u0600-\u06ffa-z0-9]+'), ' ')
.trim();
void selectGrade(String key) {
final node = state.curriculumTree[key];
final name = node is Map ? node['name']?.toString() ?? key : key;
emit(state.copyWith(
gradeKey: key,
gradeLevel: name,
subjectKey: '',
semesterKey: '',
unitKey: '',
lessonKey: '',
clearError: true));
final values = _map(state.curriculumTree[key]?['subjects']);
selectSubject(values.keys.isNotEmpty ? values.keys.first : '');
}
void selectSubject(String key) {
emit(state.copyWith(
subjectKey: key,
semesterKey: '',
unitKey: '',
lessonKey: '',
clearError: true));
final values = _map(_subjects[key]?['semesters']);
selectSemester(values.keys.isNotEmpty ? values.keys.first : '');
}
void selectSemester(String key) {
emit(state.copyWith(
semesterKey: key, unitKey: '', lessonKey: '', clearError: true));
final values = _map(_semesters[key]?['units']);
selectUnit(values.keys.isNotEmpty ? values.keys.first : '');
}
void selectUnit(String key) {
emit(state.copyWith(unitKey: key, lessonKey: '', clearError: true));
final lessons = _units[key]?['lessons'];
final id = lessons is List && lessons.isNotEmpty && lessons.first is Map
? (lessons.first as Map)['id']?.toString() ?? ''
: '';
selectLesson(id);
}
void selectLesson(String key) {
String title = '';
final lessons = _units[state.unitKey]?['lessons'];
if (lessons is List)
for (final item in lessons) {
if (item is Map && item['id']?.toString() == key)
title = item['title']?.toString() ?? '';
}
emit(state.copyWith(lessonKey: key, lessonTitle: title, clearError: true));
}
String get selectedSubjectName {
final node = _subjects[state.subjectKey];
final name = node is Map
? node['name']?.toString() ?? state.subjectKey
: state.subjectKey;
return name.replaceFirst(RegExp(r'\s*\([^)]*\)\s*$'), '').trim();
}
String get curriculumKey => [
state.gradeKey,
state.subjectKey,
state.semesterKey,
state.unitKey,
state.lessonKey
].where((e) => e.isNotEmpty).join('/');
void reportError(String message) =>
emit(state.copyWith(errorMessage: message));
void selectVideoFile(
String fileName,
double sizeMb,
@@ -115,20 +279,18 @@ class TeacherStudioCubit extends Cubit<TeacherStudioState> {
}
Future<void> runQualityGate() async {
emit(state.copyWith(isAuditing: true));
try {
final result = await repository.auditStudioVideo(
title: state.lessonTitle,
durationMinutes: state.durationMinutes,
subject: 'الفيزياء',
);
if (state.selectedFileName == null ||
(state.selectedFilePath == null && state.selectedFileBytes == null)) {
emit(state.copyWith(
isAuditing: false,
auditResult: result,
));
} catch (_) {
emit(state.copyWith(isAuditing: false));
errorMessage: 'اختر ملف فيديو فعلياً قبل بدء فحص الجودة.'));
return;
}
if (curriculumKey.isEmpty || state.lessonTitle.trim().isEmpty) {
emit(state.copyWith(
errorMessage: 'اختر الصف والمبحث والفصل والوحدة والدرس أولاً.'));
return;
}
await uploadAndPublishLesson();
}
Future<bool> uploadAndPublishLesson() async {
@@ -136,28 +298,45 @@ class TeacherStudioCubit extends Cubit<TeacherStudioState> {
(state.selectedFilePath == null && state.selectedFileBytes == null)) {
return false;
}
emit(state.copyWith(isUploading: true));
emit(state.copyWith(isUploading: true, isAuditing: true, clearError: true));
try {
final res = await repository.uploadLesson(
title: state.lessonTitle,
durationMinutes: state.durationMinutes,
gradeLevel: state.gradeLevel,
subject: 'الفيزياء والعلوم التطبيقية',
fileName: state.selectedFileName ?? 'lentz_law_physics_2008.mp4',
fileSizeMb: state.selectedFileSizeMb ?? 48.2,
subject: selectedSubjectName,
curriculumKey: curriculumKey,
fileName: state.selectedFileName!,
fileSizeMb: state.selectedFileSizeMb,
filePath: state.selectedFilePath,
fileBytes: state.selectedFileBytes,
);
final msg = res['message']?.toString() ?? 'تم رفع ونشر الحصة بنجاح في المنهاج الوزاري وسوق صَقِل! 🚀';
final data = res['data'] is Map
? Map<String, dynamic>.from(res['data'] as Map)
: <String, dynamic>{};
final report = data['preflight_report'] is Map
? Map<String, dynamic>.from(data['preflight_report'] as Map)
: <String, dynamic>{};
final result = TeacherLessonAuditModel.fromJson({
...report,
'lesson_title': state.lessonTitle,
'subject': selectedSubjectName
});
final msg = res['message']?.toString() ?? 'تم اعتماد الحصة ورفعها بنجاح.';
emit(state.copyWith(
isAuditing: false,
isUploading: false,
isUploaded: true,
auditResult: result,
uploadSuccessMessage: msg,
));
return true;
} catch (e) {
emit(state.copyWith(isUploading: false));
emit(state.copyWith(
isAuditing: false,
isUploading: false,
errorMessage: e.toString().replaceFirst('Bad state: ', '')));
return false;
}
}