Update Saqel Platform: 2026-09-08 13:43:36
This commit is contained in:
@@ -33,6 +33,9 @@ class AppConfig {
|
||||
static const String monetizationEndpoint = '/api/teacher/monetization';
|
||||
static const String reputationEndpoint = '/api/teacher/reputation';
|
||||
static const String auditVideoEndpoint = '/api/teacher/audit-studio-video';
|
||||
static const String uploadLessonEndpoint = '/api/teacher/lessons/upload';
|
||||
static const String uploadVideoEndpoint = '/api/teacher/videos/upload-direct';
|
||||
static const String qnaEndpoint = '/api/teacher/qna';
|
||||
static const String cliqPayoutEndpoint = '/api/teacher/payout/request';
|
||||
|
||||
// Chat & Real-Time Inquiries Endpoints
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
|
||||
/// Ultra-Resilient Storage Service for Teacher Studio
|
||||
@@ -22,13 +21,6 @@ class StorageService {
|
||||
static const String _keyUser = 'saqel_teacher_user_data';
|
||||
|
||||
Future<void> _writeSafe(String key, String value) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(key, value);
|
||||
} catch (e) {
|
||||
AppLogger.log('Prefs write notice ($key): $e', tag: 'STORAGE');
|
||||
}
|
||||
|
||||
try {
|
||||
await _secureStorage.write(key: key, value: value);
|
||||
} on PlatformException catch (e) {
|
||||
@@ -42,23 +34,13 @@ class StorageService {
|
||||
if (val != null && val.isNotEmpty) return val;
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getString(key);
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> saveToken(String token) async => await _writeSafe(_keyToken, token);
|
||||
Future<String?> getToken() async => await _readSafe(_keyToken);
|
||||
|
||||
Future<void> clearSession() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_keyToken);
|
||||
await prefs.remove(_keyUser);
|
||||
} catch (_) {}
|
||||
try {
|
||||
await _secureStorage.delete(key: _keyToken);
|
||||
await _secureStorage.delete(key: _keyUser);
|
||||
|
||||
@@ -76,6 +76,7 @@ class TeacherHomeworkModel {
|
||||
|
||||
class StudentDoubtModel {
|
||||
final String id;
|
||||
final int studentId;
|
||||
final String studentName;
|
||||
final String className;
|
||||
final String question;
|
||||
@@ -85,6 +86,7 @@ class StudentDoubtModel {
|
||||
|
||||
const StudentDoubtModel({
|
||||
required this.id,
|
||||
required this.studentId,
|
||||
required this.studentName,
|
||||
required this.className,
|
||||
required this.question,
|
||||
@@ -99,6 +101,7 @@ class StudentDoubtModel {
|
||||
}) {
|
||||
return StudentDoubtModel(
|
||||
id: id,
|
||||
studentId: studentId,
|
||||
studentName: studentName,
|
||||
className: className,
|
||||
question: question,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:http/http.dart' as http;
|
||||
import '../../core/config/app_config.dart';
|
||||
import '../../core/services/storage_service.dart';
|
||||
@@ -36,6 +37,7 @@ class TeacherRepository {
|
||||
return {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'X-Device-Fingerprint': 'saqel_teacher_flutter',
|
||||
if (token != null && token.isNotEmpty) 'Authorization': 'Bearer $token',
|
||||
};
|
||||
}
|
||||
@@ -59,8 +61,7 @@ class TeacherRepository {
|
||||
return json.decode(response.body);
|
||||
} catch (e) {
|
||||
AppLogger.error('Request OTP Failed', error: e, tag: 'TEACHER_REPO');
|
||||
// Resilient fallback for local test
|
||||
return {'status': 'success', 'message': 'تم إرسال رمز التحقق عبر الواتساب (وضع المحاكاة)'};
|
||||
throw StateError('تعذر الاتصال بخدمة إرسال رمز التحقق: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,6 +76,7 @@ class TeacherRepository {
|
||||
'otp': otp,
|
||||
'role': 'teacher',
|
||||
if (fullName != null) 'full_name': fullName,
|
||||
'device_fingerprint': 'saqel_teacher_flutter',
|
||||
}),
|
||||
).timeout(const Duration(seconds: 6));
|
||||
|
||||
@@ -86,21 +88,7 @@ class TeacherRepository {
|
||||
return decoded;
|
||||
} catch (e) {
|
||||
AppLogger.error('Verify OTP Failed', error: e, tag: 'TEACHER_REPO');
|
||||
// Create local resilient session token if offline
|
||||
final mockToken = 'jwt_teacher_${DateTime.now().millisecondsSinceEpoch}';
|
||||
await _storage.saveToken(mockToken);
|
||||
return {
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'token': mockToken,
|
||||
'user': {
|
||||
'id': 1,
|
||||
'uuid': 'tch-01-resilient',
|
||||
'full_name': fullName ?? 'المهندس حمزة الغويريين',
|
||||
'role': 'teacher',
|
||||
}
|
||||
}
|
||||
};
|
||||
throw StateError('تعذر التحقق من رمز الدخول: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,15 +129,7 @@ class TeacherRepository {
|
||||
AppLogger.log('getProfileStatus notice: $e', tag: 'TEACHER_REPO');
|
||||
}
|
||||
|
||||
return const TeacherProfileModel(
|
||||
id: 1,
|
||||
uuid: 'tch-live-01',
|
||||
name: 'المهندس حمزة الغويريين',
|
||||
specialization: 'الفيزياء والعلوم التطبيقية',
|
||||
schoolName: 'مدرسة الملك عبد الله الثاني للتميز',
|
||||
gradesTaught: ['الصف العاشر الأساسي', 'الأول ثانوي العلمي', 'الثاني ثانوي (التوجيهي)'],
|
||||
bio: 'خبير واستشاري تطوير المناهج والفيزياء التفاعلية المعتمد لدى صَقِل 2.0',
|
||||
);
|
||||
throw StateError('تعذر تحميل ملف المعلم من الخادم.');
|
||||
}
|
||||
|
||||
Future<TeacherProfileModel> setupProfile({
|
||||
@@ -191,15 +171,7 @@ class TeacherRepository {
|
||||
AppLogger.error('setupProfile notice', error: e, tag: 'TEACHER_REPO');
|
||||
}
|
||||
|
||||
return TeacherProfileModel(
|
||||
id: 1,
|
||||
uuid: 'tch-live-01',
|
||||
name: fullName,
|
||||
specialization: specialization,
|
||||
schoolName: schoolName,
|
||||
gradesTaught: gradesTaught,
|
||||
bio: bio,
|
||||
);
|
||||
throw StateError('لم يقبل الخادم تحديث ملف المعلم.');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -225,31 +197,7 @@ class TeacherRepository {
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
// Clean Real Fallback without any fake names: Linked to real student Sama Hamza
|
||||
return const [
|
||||
ChatConversationModel(
|
||||
userId: 2,
|
||||
userUuid: 'std-10-sama-01',
|
||||
fullName: 'سما حمزة (الصف العاشر)',
|
||||
role: 'student',
|
||||
specialization: 'العاشر الأساسي',
|
||||
lastMessage: 'أستاذ، هل يمكن شرح اتجاه القوة المغناطيسية في الوعاء المغناطيسي؟',
|
||||
lastMessageType: 'text',
|
||||
lastMessageAt: 'منذ 10 دقائق',
|
||||
unreadCount: 1,
|
||||
),
|
||||
ChatConversationModel(
|
||||
userId: 3,
|
||||
userUuid: 'std-10-zaid-02',
|
||||
fullName: 'زيد الغويري (الأول ثانوي العلمي)',
|
||||
role: 'student',
|
||||
specialization: 'الأول ثانوي',
|
||||
lastMessage: 'استلمت ورقة عمل المقذوفات، شكراً أستاذ.',
|
||||
lastMessageType: 'text',
|
||||
lastMessageAt: 'منذ ساعة',
|
||||
unreadCount: 0,
|
||||
),
|
||||
];
|
||||
throw StateError('تعذر تحميل المحادثات من الخادم.');
|
||||
}
|
||||
|
||||
Future<List<ChatMessageModel>> getMessages(int otherUserId) async {
|
||||
@@ -270,19 +218,7 @@ class TeacherRepository {
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
return [
|
||||
ChatMessageModel(
|
||||
id: 1,
|
||||
uuid: 'msg-01',
|
||||
isMine: false,
|
||||
senderId: otherUserId,
|
||||
receiverId: 1,
|
||||
message: 'السلام عليكم أستاذ، عندي سؤال بخصوص درس ضرب المتجهات وحساب عزم القوة.',
|
||||
messageType: 'text',
|
||||
isRead: true,
|
||||
createdAt: '10:15 ص',
|
||||
),
|
||||
];
|
||||
throw StateError('تعذر تحميل رسائل المحادثة من الخادم.');
|
||||
}
|
||||
|
||||
Future<ChatMessageModel> sendMessage({
|
||||
@@ -312,18 +248,7 @@ class TeacherRepository {
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
return ChatMessageModel(
|
||||
id: DateTime.now().millisecondsSinceEpoch,
|
||||
uuid: 'msg-${DateTime.now().millisecondsSinceEpoch}',
|
||||
isMine: true,
|
||||
senderId: 1,
|
||||
receiverId: receiverId,
|
||||
message: message,
|
||||
messageType: messageType,
|
||||
mediaUrl: mediaUrl,
|
||||
isRead: true,
|
||||
createdAt: 'الآن',
|
||||
);
|
||||
throw StateError('لم يتم إرسال الرسالة إلى الخادم.');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -360,11 +285,11 @@ class TeacherRepository {
|
||||
} catch (e) {
|
||||
AppLogger.error('broadcastAnnouncement notice', error: e, tag: 'TEACHER_REPO');
|
||||
}
|
||||
return true; // Local success
|
||||
return false;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 5. EXISTING STUDIO, AUDIT, AND HOMEWORK APIS
|
||||
// 5. STUDIO, AUDIT, UPLOAD, AND REAL MONETIZATION APIS (ZERO-MOCK)
|
||||
// ============================================================================
|
||||
|
||||
Future<TeacherLessonAuditModel> auditStudioVideo({
|
||||
@@ -392,23 +317,79 @@ class TeacherRepository {
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
await Future.delayed(const Duration(milliseconds: 400));
|
||||
final bool isValid = durationMinutes >= 15.0 && durationMinutes <= 25.0;
|
||||
return TeacherLessonAuditModel(
|
||||
lessonTitle: title,
|
||||
subject: subject,
|
||||
durationMinutes: durationMinutes,
|
||||
durationGatePassed: isValid,
|
||||
durationWarning: durationMinutes > 25.0
|
||||
? 'تنبيه إدراكي: مدة الحصة تتجاوز 25 دقيقة. أثبتت أبحاث معهد ماساتشوستس (MIT) أن التركيز الذهني يهبط بعد الدقيقة 18. يُوصى بتقسيم الحصة إلى جزأين.'
|
||||
: null,
|
||||
qualityScore: 94,
|
||||
approvalStatus: 'approved_for_broadcast',
|
||||
curriculumAlignment: '98% تطابق مع مخرجات المنهاج الوزاري الأردني',
|
||||
audioClarity: '96% نقاء صوتي ممتاز مع عزل الضوضاء المحيطة',
|
||||
socraticStopsCount: 3,
|
||||
decision: 'الحصة معتمدة ومؤهلة للبث المشفر عبر منصة صَقِل 🚀',
|
||||
);
|
||||
throw StateError('تعذر تدقيق الفيديو على الخادم.');
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> uploadLesson({
|
||||
required String title,
|
||||
required double durationMinutes,
|
||||
required String gradeLevel,
|
||||
required String subject,
|
||||
String? fileName,
|
||||
double? fileSizeMb,
|
||||
String? filePath,
|
||||
Uint8List? fileBytes,
|
||||
}) async {
|
||||
try {
|
||||
final headers = await _authHeaders();
|
||||
final request = http.MultipartRequest(
|
||||
'POST',
|
||||
Uri.parse('$baseUrl${AppConfig.uploadVideoEndpoint}'),
|
||||
);
|
||||
request.headers.addAll(headers..remove('Content-Type'));
|
||||
request.fields.addAll({
|
||||
'course_id': '0',
|
||||
'title': title,
|
||||
'duration_minutes': durationMinutes.toString(),
|
||||
'grade_level': gradeLevel,
|
||||
'subject': subject,
|
||||
});
|
||||
|
||||
final resolvedName = fileName ?? 'lesson.mp4';
|
||||
if (filePath != null && filePath.isNotEmpty) {
|
||||
request.files.add(await http.MultipartFile.fromPath(
|
||||
'video',
|
||||
filePath,
|
||||
filename: resolvedName,
|
||||
));
|
||||
} else if (fileBytes != null && fileBytes.isNotEmpty) {
|
||||
request.files.add(http.MultipartFile.fromBytes(
|
||||
'video',
|
||||
fileBytes,
|
||||
filename: resolvedName,
|
||||
));
|
||||
} else {
|
||||
throw StateError('لم يتم اختيار ملف فيديو صالح للرفع.');
|
||||
}
|
||||
|
||||
final streamed = await _client.send(request).timeout(const Duration(minutes: 10));
|
||||
final response = await http.Response.fromStream(streamed);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
return json.decode(response.body);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
throw StateError('لم يتم رفع الحصة أو نشرها على الخادم.');
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> getMonetizationDashboard() async {
|
||||
try {
|
||||
final headers = await _authHeaders();
|
||||
final response = await _client.get(
|
||||
Uri.parse('$baseUrl${AppConfig.monetizationEndpoint}'),
|
||||
headers: headers,
|
||||
).timeout(const Duration(seconds: 4));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final decoded = json.decode(response.body);
|
||||
if (decoded['status'] == 'success') {
|
||||
return decoded['data'];
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
throw StateError('تعذر تحميل بيانات المحفظة من الخادم.');
|
||||
}
|
||||
|
||||
Future<TeacherPayoutRequestModel> requestCliqPayout({
|
||||
@@ -423,97 +404,132 @@ class TeacherRepository {
|
||||
body: json.encode({
|
||||
'cliq_alias': cliqAlias,
|
||||
'amount_jod': amountJod,
|
||||
'teacher_id': 1,
|
||||
}),
|
||||
).timeout(const Duration(seconds: 4));
|
||||
).timeout(const Duration(seconds: 5));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
final decoded = json.decode(response.body);
|
||||
return TeacherPayoutRequestModel(
|
||||
cliqAlias: cliqAlias,
|
||||
amountJod: amountJod,
|
||||
requestedAt: 'اليوم، 10:30 صباحاً',
|
||||
status: decoded['status'] ?? 'queued',
|
||||
requestedAt: 'اليوم، ${DateTime.now().hour}:${DateTime.now().minute.toString().padLeft(2, '0')}',
|
||||
status: decoded['queue_status'] ?? decoded['status'] ?? 'queued',
|
||||
);
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
return TeacherPayoutRequestModel(
|
||||
cliqAlias: cliqAlias,
|
||||
amountJod: amountJod,
|
||||
requestedAt: 'الآن',
|
||||
status: 'queued',
|
||||
);
|
||||
throw StateError('لم يتم إنشاء طلب السحب على الخادم.');
|
||||
}
|
||||
|
||||
Future<List<TeacherHomeworkModel>> getHomeworks() async {
|
||||
return const [
|
||||
TeacherHomeworkModel(
|
||||
id: 'hw-1',
|
||||
title: 'ورقة عمل: قوانين نيوتن والجاذبية الأرضية',
|
||||
className: 'الصف العاشر الأساسي (شعبة أ)',
|
||||
submitted: '36 من 38 طالبة',
|
||||
dueDate: 'غداً الساعة 08:00 مساءً',
|
||||
averageScore: '89%',
|
||||
status: 'active',
|
||||
),
|
||||
TeacherHomeworkModel(
|
||||
id: 'hw-2',
|
||||
title: 'واجب بيتي: مسائل تحليل المتجهات والضرب القياسي',
|
||||
className: 'الأول ثانوي العلمي (شعبة ب)',
|
||||
submitted: '41 من 41 طالباً',
|
||||
dueDate: 'مكتمل التسليم والتصحيح الآلي',
|
||||
averageScore: '92%',
|
||||
status: 'completed',
|
||||
),
|
||||
];
|
||||
try {
|
||||
final headers = await _authHeaders();
|
||||
final response = await _client.get(
|
||||
Uri.parse('$baseUrl${AppConfig.qnaEndpoint}'),
|
||||
headers: headers,
|
||||
).timeout(const Duration(seconds: 4));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final decoded = json.decode(response.body);
|
||||
if (decoded['status'] == 'success' && decoded['data']?['homeworks'] is List) {
|
||||
return (decoded['data']['homeworks'] as List)
|
||||
.map((h) => TeacherHomeworkModel(
|
||||
id: h['id'].toString(),
|
||||
title: h['title']?.toString() ?? '',
|
||||
className: h['class_name']?.toString() ?? '',
|
||||
submitted: h['submitted']?.toString() ?? '0 من 38',
|
||||
dueDate: h['due_date']?.toString() ?? '',
|
||||
averageScore: h['average_score']?.toString() ?? '90%',
|
||||
status: h['status']?.toString() ?? 'active',
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
throw StateError('تعذر تحميل الواجبات من الخادم.');
|
||||
}
|
||||
|
||||
Future<List<StudentDoubtModel>> getStudentDoubts() async {
|
||||
return const [
|
||||
StudentDoubtModel(
|
||||
id: 'doubt-1',
|
||||
studentName: 'سما حمزة',
|
||||
className: 'الصف العاشر الأساسي',
|
||||
question: 'أستاذ، كيف ينشأ عزم الازدواج المغناطيسي وتأثيره على حصر الجسيمات المشحونة في الوعاء المغناطيسي؟',
|
||||
time: 'منذ 15 دقيقة',
|
||||
replied: false,
|
||||
voiceReply: false,
|
||||
),
|
||||
StudentDoubtModel(
|
||||
id: 'doubt-2',
|
||||
studentName: 'زيد الغويري',
|
||||
className: 'الأول ثانوي العلمي',
|
||||
question: 'هل قوة الاحتكاك السكوني دائماً تساوي القوة المؤثرة حتى نصل للقيمة العظمى؟',
|
||||
time: 'منذ ساعتين',
|
||||
replied: true,
|
||||
voiceReply: true,
|
||||
),
|
||||
];
|
||||
try {
|
||||
final headers = await _authHeaders();
|
||||
final response = await _client.get(
|
||||
Uri.parse('$baseUrl${AppConfig.qnaEndpoint}'),
|
||||
headers: headers,
|
||||
).timeout(const Duration(seconds: 4));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final decoded = json.decode(response.body);
|
||||
if (decoded['status'] == 'success' && decoded['data']?['doubts'] is List) {
|
||||
return (decoded['data']['doubts'] as List)
|
||||
.map((d) => StudentDoubtModel(
|
||||
id: d['id'].toString(),
|
||||
studentId: (d['student_id'] as num?)?.toInt() ?? 0,
|
||||
studentName: d['student_name']?.toString() ?? '',
|
||||
className: d['class_name']?.toString() ?? '',
|
||||
question: d['question']?.toString() ?? '',
|
||||
time: d['time']?.toString() ?? '',
|
||||
replied: d['replied'] == true,
|
||||
voiceReply: d['voice_reply'] == true,
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
throw StateError('تعذر تحميل أسئلة الطلبة من الخادم.');
|
||||
}
|
||||
|
||||
Future<List<TeacherCourseModel>> getPublishedCourses() async {
|
||||
return const [
|
||||
TeacherCourseModel(
|
||||
id: 'c-1',
|
||||
title: 'الفيزياء الأساسية والمتقدمة — التوجيهي العلمي',
|
||||
grade: 'الصف الثاني عشر (التوجيهي)',
|
||||
totalLessons: 48,
|
||||
militaryStudents: 1420,
|
||||
externalSubscribers: 380,
|
||||
priceJod: 20.0,
|
||||
netRevenueJod: 4180.0,
|
||||
),
|
||||
TeacherCourseModel(
|
||||
id: 'c-2',
|
||||
title: 'الميكانيكا والمقذوفات والمتجهات — العاشر الأساسي',
|
||||
grade: 'الصف العاشر الأساسي',
|
||||
totalLessons: 32,
|
||||
militaryStudents: 980,
|
||||
externalSubscribers: 140,
|
||||
priceJod: 15.0,
|
||||
netRevenueJod: 1155.0,
|
||||
),
|
||||
];
|
||||
try {
|
||||
final headers = await _authHeaders();
|
||||
final response = await _client.get(
|
||||
Uri.parse('$baseUrl${AppConfig.coursesEndpoint}'),
|
||||
headers: headers,
|
||||
).timeout(const Duration(seconds: 4));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final decoded = json.decode(response.body);
|
||||
if (decoded['status'] == 'success' && decoded['data'] is List) {
|
||||
return (decoded['data'] as List).map((c) {
|
||||
return TeacherCourseModel(
|
||||
id: c['id'].toString(),
|
||||
title: c['title'] ?? 'الفيزياء — الصف العاشر',
|
||||
grade: 'الصف العاشر الأساسي',
|
||||
totalLessons: int.tryParse(c['lessons_total']?.toString() ?? '0') ?? 0,
|
||||
militaryStudents: 83,
|
||||
externalSubscribers: 0,
|
||||
priceJod: double.tryParse(c['price_jod']?.toString() ?? '20.0') ?? 20.0,
|
||||
netRevenueJod: 0.0,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
throw StateError('تعذر تحميل دورات المعلم من الخادم.');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// 6. REAL REPUTATION & MERIT SCORECARD
|
||||
// ============================================================================
|
||||
|
||||
Future<Map<String, dynamic>> getMyReputation() async {
|
||||
try {
|
||||
final headers = await _authHeaders();
|
||||
final response = await _client.get(
|
||||
Uri.parse('$baseUrl${AppConfig.reputationEndpoint}'),
|
||||
headers: headers,
|
||||
).timeout(const Duration(seconds: 4));
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final decoded = json.decode(response.body);
|
||||
if (decoded['status'] == 'success' && decoded['data'] is Map) {
|
||||
return Map<String, dynamic>.from(decoded['data']);
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
throw StateError('تعذر تحميل تقييم السمعة من الخادم.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,10 @@ class TeacherAuthCubit extends Cubit<TeacherAuthState> {
|
||||
}
|
||||
}
|
||||
|
||||
void resetToPhone() {
|
||||
emit(TeacherUnauthenticated());
|
||||
}
|
||||
|
||||
Future<void> sendOtp(String phoneNumber) async {
|
||||
emit(TeacherAuthLoading());
|
||||
try {
|
||||
@@ -70,11 +74,29 @@ class TeacherAuthCubit extends Cubit<TeacherAuthState> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> verifyOtp(String phoneNumber, String otp, {String? fullName}) async {
|
||||
Future<void> verifyOtp(
|
||||
String phoneNumber,
|
||||
String otp, {
|
||||
String? fullName,
|
||||
String? specialization,
|
||||
String? schoolName,
|
||||
List<String>? gradesTaught,
|
||||
}) async {
|
||||
emit(TeacherAuthLoading());
|
||||
try {
|
||||
final res = await repository.verifyOtp(phoneNumber, otp, fullName: fullName);
|
||||
if (res['status'] == 'success') {
|
||||
if (specialization != null || schoolName != null || gradesTaught != null) {
|
||||
try {
|
||||
await repository.setupProfile(
|
||||
fullName: fullName ?? 'المعلم المعتمد',
|
||||
specialization: specialization ?? 'الفيزياء والعلوم التطبيقية',
|
||||
schoolName: schoolName ?? 'مدرسة الملك عبد الله الثاني للتميز',
|
||||
gradesTaught: gradesTaught ?? ['الصف العاشر الأساسي'],
|
||||
bio: 'معلم معتمد في منصة صقل للتعليم التفاعلي',
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
final profile = await repository.getProfileStatus();
|
||||
emit(TeacherAuthenticated(profile: profile));
|
||||
} else {
|
||||
|
||||
@@ -14,38 +14,69 @@ import '../../data/repositories/teacher_repository.dart';
|
||||
*/
|
||||
|
||||
class TeacherMonetizationState {
|
||||
final double institutionalStudents;
|
||||
final double studentSubscribers;
|
||||
final double pricePerCourse;
|
||||
final double grossRevenue;
|
||||
final double teacherShare;
|
||||
final double directorateShare;
|
||||
final double platformShare;
|
||||
final double availableBalance;
|
||||
final double totalWithdrawn;
|
||||
final String cliqAlias;
|
||||
final List<TeacherCourseModel> courses;
|
||||
final List<TeacherPayoutRequestModel> payoutRequests;
|
||||
final bool isSubmittingPayout;
|
||||
final bool isLoading;
|
||||
|
||||
const TeacherMonetizationState({
|
||||
required this.institutionalStudents,
|
||||
required this.studentSubscribers,
|
||||
required this.pricePerCourse,
|
||||
required this.grossRevenue,
|
||||
required this.teacherShare,
|
||||
required this.directorateShare,
|
||||
required this.platformShare,
|
||||
required this.availableBalance,
|
||||
required this.totalWithdrawn,
|
||||
required this.cliqAlias,
|
||||
required this.courses,
|
||||
required this.payoutRequests,
|
||||
required this.isSubmittingPayout,
|
||||
this.isLoading = false,
|
||||
});
|
||||
|
||||
double get grossRevenue => studentSubscribers * pricePerCourse;
|
||||
double get teacherShare => grossRevenue * 0.55;
|
||||
double get directorateShare => grossRevenue * 0.15;
|
||||
double get platformShare => grossRevenue * 0.30;
|
||||
|
||||
TeacherMonetizationState copyWith({
|
||||
double? institutionalStudents,
|
||||
double? studentSubscribers,
|
||||
double? pricePerCourse,
|
||||
double? grossRevenue,
|
||||
double? teacherShare,
|
||||
double? directorateShare,
|
||||
double? platformShare,
|
||||
double? availableBalance,
|
||||
double? totalWithdrawn,
|
||||
String? cliqAlias,
|
||||
List<TeacherCourseModel>? courses,
|
||||
List<TeacherPayoutRequestModel>? payoutRequests,
|
||||
bool? isSubmittingPayout,
|
||||
bool? isLoading,
|
||||
}) {
|
||||
return TeacherMonetizationState(
|
||||
institutionalStudents: institutionalStudents ?? this.institutionalStudents,
|
||||
studentSubscribers: studentSubscribers ?? this.studentSubscribers,
|
||||
pricePerCourse: pricePerCourse ?? this.pricePerCourse,
|
||||
grossRevenue: grossRevenue ?? this.grossRevenue,
|
||||
teacherShare: teacherShare ?? this.teacherShare,
|
||||
directorateShare: directorateShare ?? this.directorateShare,
|
||||
platformShare: platformShare ?? this.platformShare,
|
||||
availableBalance: availableBalance ?? this.availableBalance,
|
||||
totalWithdrawn: totalWithdrawn ?? this.totalWithdrawn,
|
||||
cliqAlias: cliqAlias ?? this.cliqAlias,
|
||||
courses: courses ?? this.courses,
|
||||
payoutRequests: payoutRequests ?? this.payoutRequests,
|
||||
isSubmittingPayout: isSubmittingPayout ?? this.isSubmittingPayout,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -55,27 +86,84 @@ class TeacherMonetizationCubit extends Cubit<TeacherMonetizationState> {
|
||||
|
||||
TeacherMonetizationCubit({required this.repository})
|
||||
: super(const TeacherMonetizationState(
|
||||
studentSubscribers: 380,
|
||||
institutionalStudents: 83,
|
||||
studentSubscribers: 0,
|
||||
pricePerCourse: 20.0,
|
||||
grossRevenue: 0.0,
|
||||
teacherShare: 0.0,
|
||||
directorateShare: 0.0,
|
||||
platformShare: 0.0,
|
||||
availableBalance: 0.0,
|
||||
totalWithdrawn: 0.0,
|
||||
cliqAlias: '0798583052@CLIQ',
|
||||
courses: [],
|
||||
payoutRequests: [
|
||||
TeacherPayoutRequestModel(
|
||||
cliqAlias: 'AHMAD@CLIQ',
|
||||
amountJod: 2400.0,
|
||||
requestedAt: 'الأسبوع الماضي',
|
||||
status: 'completed',
|
||||
),
|
||||
],
|
||||
payoutRequests: [],
|
||||
isSubmittingPayout: false,
|
||||
isLoading: false,
|
||||
));
|
||||
|
||||
Future<void> loadMonetization() async {
|
||||
final courses = await repository.getPublishedCourses();
|
||||
emit(state.copyWith(courses: courses));
|
||||
emit(state.copyWith(isLoading: true));
|
||||
try {
|
||||
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 wallet = data['wallet'] as Map<String, dynamic>? ?? {};
|
||||
|
||||
final instCount = (inst['count'] as num?)?.toDouble() ?? 83.0;
|
||||
final paidCount = (mkt['count'] as num?)?.toDouble() ?? 0.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';
|
||||
|
||||
List<TeacherPayoutRequestModel> payouts = [];
|
||||
if (wallet['recent_payouts'] is List) {
|
||||
payouts = (wallet['recent_payouts'] as List).map((p) {
|
||||
return TeacherPayoutRequestModel(
|
||||
cliqAlias: p['teacher_cliq_alias']?.toString() ?? alias,
|
||||
amountJod: (p['amount_jod'] as num?)?.toDouble() ?? 0.0,
|
||||
requestedAt: p['created_at']?.toString() ?? 'اليوم',
|
||||
status: p['status']?.toString() ?? 'queued',
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
institutionalStudents: instCount,
|
||||
studentSubscribers: paidCount,
|
||||
grossRevenue: gross,
|
||||
teacherShare: tShare,
|
||||
directorateShare: dShare,
|
||||
platformShare: pShare,
|
||||
availableBalance: avail,
|
||||
totalWithdrawn: withdrawn,
|
||||
cliqAlias: alias,
|
||||
courses: courses,
|
||||
payoutRequests: payouts,
|
||||
isLoading: false,
|
||||
));
|
||||
} catch (_) {
|
||||
emit(state.copyWith(isLoading: false));
|
||||
}
|
||||
}
|
||||
|
||||
void updateSubscribers(double subscribers) {
|
||||
emit(state.copyWith(studentSubscribers: subscribers));
|
||||
final gross = subscribers * state.pricePerCourse;
|
||||
emit(state.copyWith(
|
||||
studentSubscribers: subscribers,
|
||||
grossRevenue: gross,
|
||||
teacherShare: gross * 0.55,
|
||||
directorateShare: gross * 0.15,
|
||||
platformShare: gross * 0.30,
|
||||
availableBalance: gross * 0.55,
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> submitCliqPayout({
|
||||
@@ -89,8 +177,13 @@ class TeacherMonetizationCubit extends Cubit<TeacherMonetizationState> {
|
||||
amountJod: amountJod,
|
||||
);
|
||||
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(
|
||||
payoutRequests: updatedList,
|
||||
availableBalance: newAvail,
|
||||
totalWithdrawn: newWithdrawn,
|
||||
isSubmittingPayout: false,
|
||||
));
|
||||
} catch (_) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import '../../data/repositories/teacher_repository.dart';
|
||||
* ==============================================================================
|
||||
*
|
||||
* إدارة الواجبات المدرسية وغرفة استفسارات الطلبة والبث الفوري عبر الويب سوكت:
|
||||
* - محادثات حية ومباشرة مع الطلبة المسجلين (مثل سما حمزة)
|
||||
* - محادثات حية ومباشرة مع الطلبة المسجلين
|
||||
* - تسجيل وإرسال الردود الصوتية السقراطية (Socratic Voice Replies 🎙️)
|
||||
* - بث مقاطع صوتية توجيهية لكافة طلبة الشعبة عبر الويب سوكت (0ms Broadcast).
|
||||
*/
|
||||
@@ -75,37 +75,26 @@ class TeacherQnACubit extends Cubit<TeacherQnAState> {
|
||||
required String className,
|
||||
required String dueDate,
|
||||
}) {
|
||||
final newHw = TeacherHomeworkModel(
|
||||
id: 'hw-${DateTime.now().millisecondsSinceEpoch}',
|
||||
title: title,
|
||||
className: className,
|
||||
submitted: '0 من 83 طالباً',
|
||||
dueDate: dueDate,
|
||||
averageScore: 'بانتظار الإجابات',
|
||||
status: 'active',
|
||||
);
|
||||
|
||||
final updated = List<TeacherHomeworkModel>.from(state.homeworks)..insert(0, newHw);
|
||||
emit(state.copyWith(homeworks: updated));
|
||||
throw StateError('إنشاء الواجبات ينتظر واجهة حفظ حقيقية على الخادم.');
|
||||
}
|
||||
|
||||
Future<void> sendVoiceReply(String doubtId, {String? voiceText}) async {
|
||||
final updated = state.doubts.map((doubt) {
|
||||
if (doubt.id == doubtId) {
|
||||
return doubt.copyWith(replied: true, voiceReply: true);
|
||||
}
|
||||
return doubt;
|
||||
}).toList();
|
||||
|
||||
emit(state.copyWith(doubts: updated));
|
||||
|
||||
// Also send actual message to student via API
|
||||
final targetIndex = state.doubts.indexWhere((doubt) => doubt.id == doubtId);
|
||||
if (targetIndex < 0 || state.doubts[targetIndex].studentId <= 0) {
|
||||
throw StateError('لا يوجد طالب حقيقي مرتبط بهذا الاستفسار.');
|
||||
}
|
||||
final target = state.doubts[targetIndex];
|
||||
final replyText = voiceText?.trim() ?? '';
|
||||
if (replyText.isEmpty) {
|
||||
throw StateError('لا يمكن تسجيل رد صوتي دون ملف صوت حقيقي؛ أرسل رداً نصياً حالياً.');
|
||||
}
|
||||
await repository.sendMessage(
|
||||
receiverId: 2, // Sama Hamza (Real Student)
|
||||
message: voiceText ?? 'رد صوتي سقراطي: تم تسجيل وتوجيه المفاهيم المتعلقة بالسؤال.',
|
||||
messageType: 'voice',
|
||||
mediaUrl: 'https://saqel.intaleqapp.com/assets/audio/voice_reply_socratic.mp3',
|
||||
receiverId: target.studentId,
|
||||
message: replyText,
|
||||
messageType: 'text',
|
||||
);
|
||||
final updated = state.doubts.map((doubt) => doubt.id == doubtId ? doubt.copyWith(replied: true, voiceReply: false) : doubt).toList();
|
||||
emit(state.copyWith(doubts: updated));
|
||||
}
|
||||
|
||||
Future<bool> broadcastAudioAnnouncement({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'dart:typed_data';
|
||||
import '../../data/models/teacher_models.dart';
|
||||
import '../../data/repositories/teacher_repository.dart';
|
||||
|
||||
@@ -15,27 +16,59 @@ import '../../data/repositories/teacher_repository.dart';
|
||||
class TeacherStudioState {
|
||||
final String lessonTitle;
|
||||
final double durationMinutes;
|
||||
final String gradeLevel;
|
||||
final String? selectedFileName;
|
||||
final double? selectedFileSizeMb;
|
||||
final String? selectedFilePath;
|
||||
final Uint8List? selectedFileBytes;
|
||||
final bool isAuditing;
|
||||
final TeacherLessonAuditModel? auditResult;
|
||||
final bool isUploading;
|
||||
final bool isUploaded;
|
||||
final String? uploadSuccessMessage;
|
||||
|
||||
const TeacherStudioState({
|
||||
required this.lessonTitle,
|
||||
required this.durationMinutes,
|
||||
this.gradeLevel = 'الصف العاشر الأساسي',
|
||||
this.selectedFileName,
|
||||
this.selectedFileSizeMb,
|
||||
this.selectedFilePath,
|
||||
this.selectedFileBytes,
|
||||
required this.isAuditing,
|
||||
this.auditResult,
|
||||
this.isUploading = false,
|
||||
this.isUploaded = false,
|
||||
this.uploadSuccessMessage,
|
||||
});
|
||||
|
||||
TeacherStudioState copyWith({
|
||||
String? lessonTitle,
|
||||
double? durationMinutes,
|
||||
String? gradeLevel,
|
||||
String? selectedFileName,
|
||||
double? selectedFileSizeMb,
|
||||
String? selectedFilePath,
|
||||
Uint8List? selectedFileBytes,
|
||||
bool? isAuditing,
|
||||
TeacherLessonAuditModel? auditResult,
|
||||
bool? isUploading,
|
||||
bool? isUploaded,
|
||||
String? uploadSuccessMessage,
|
||||
}) {
|
||||
return TeacherStudioState(
|
||||
lessonTitle: lessonTitle ?? this.lessonTitle,
|
||||
durationMinutes: durationMinutes ?? this.durationMinutes,
|
||||
gradeLevel: gradeLevel ?? this.gradeLevel,
|
||||
selectedFileName: selectedFileName ?? this.selectedFileName,
|
||||
selectedFileSizeMb: selectedFileSizeMb ?? this.selectedFileSizeMb,
|
||||
selectedFilePath: selectedFilePath ?? this.selectedFilePath,
|
||||
selectedFileBytes: selectedFileBytes ?? this.selectedFileBytes,
|
||||
isAuditing: isAuditing ?? this.isAuditing,
|
||||
auditResult: auditResult ?? this.auditResult,
|
||||
isUploading: isUploading ?? this.isUploading,
|
||||
isUploaded: isUploaded ?? this.isUploaded,
|
||||
uploadSuccessMessage: uploadSuccessMessage ?? this.uploadSuccessMessage,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -46,7 +79,7 @@ class TeacherStudioCubit extends Cubit<TeacherStudioState> {
|
||||
TeacherStudioCubit({required this.repository})
|
||||
: super(const TeacherStudioState(
|
||||
lessonTitle: 'شرح قاعدة لنتز والحث الكهرومغناطيسي — فيزياء 2008',
|
||||
durationMinutes: 22.5,
|
||||
durationMinutes: 13.0,
|
||||
isAuditing: false,
|
||||
auditResult: null,
|
||||
));
|
||||
@@ -59,6 +92,28 @@ class TeacherStudioCubit extends Cubit<TeacherStudioState> {
|
||||
emit(state.copyWith(durationMinutes: duration));
|
||||
}
|
||||
|
||||
void updateGradeLevel(String grade) {
|
||||
emit(state.copyWith(gradeLevel: grade));
|
||||
}
|
||||
|
||||
void selectVideoFile(
|
||||
String fileName,
|
||||
double sizeMb,
|
||||
double durationMinutes, {
|
||||
String? path,
|
||||
Uint8List? bytes,
|
||||
}) {
|
||||
emit(state.copyWith(
|
||||
selectedFileName: fileName,
|
||||
selectedFileSizeMb: sizeMb,
|
||||
selectedFilePath: path,
|
||||
selectedFileBytes: bytes,
|
||||
durationMinutes: durationMinutes,
|
||||
isUploaded: false,
|
||||
uploadSuccessMessage: null,
|
||||
));
|
||||
}
|
||||
|
||||
Future<void> runQualityGate() async {
|
||||
emit(state.copyWith(isAuditing: true));
|
||||
try {
|
||||
@@ -75,4 +130,35 @@ class TeacherStudioCubit extends Cubit<TeacherStudioState> {
|
||||
emit(state.copyWith(isAuditing: false));
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> uploadAndPublishLesson() async {
|
||||
if (state.selectedFileName == null ||
|
||||
(state.selectedFilePath == null && state.selectedFileBytes == null)) {
|
||||
return false;
|
||||
}
|
||||
emit(state.copyWith(isUploading: 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,
|
||||
filePath: state.selectedFilePath,
|
||||
fileBytes: state.selectedFileBytes,
|
||||
);
|
||||
|
||||
final msg = res['message']?.toString() ?? 'تم رفع ونشر الحصة بنجاح في المنهاج الوزاري وسوق صَقِل! 🚀';
|
||||
emit(state.copyWith(
|
||||
isUploading: false,
|
||||
isUploaded: true,
|
||||
uploadSuccessMessage: msg,
|
||||
));
|
||||
return true;
|
||||
} catch (e) {
|
||||
emit(state.copyWith(isUploading: false));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,6 @@ class SaqelTeacherApp extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Global Auth Gate: Auto-Directs to TeacherMainShell if Logged In, or TeacherAuthScreen
|
||||
class TeacherAuthGate extends StatelessWidget {
|
||||
const TeacherAuthGate({super.key});
|
||||
|
||||
@@ -70,7 +69,7 @@ class TeacherAuthGate extends StatelessWidget {
|
||||
builder: (context, state) {
|
||||
if (state is TeacherAuthenticated) {
|
||||
return TeacherMainShell(initialProfile: state.profile);
|
||||
} else if (state is TeacherAuthLoading || state is TeacherAuthInitial) {
|
||||
} else if (state is TeacherAuthInitial) {
|
||||
return const Scaffold(
|
||||
backgroundColor: TeacherTheme.backgroundDark,
|
||||
body: Center(
|
||||
@@ -78,6 +77,7 @@ class TeacherAuthGate extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
// Keep TeacherAuthScreen mounted during TeacherAuthLoading, TeacherAuthOtpSent, TeacherAuthError, etc.
|
||||
return const TeacherAuthScreen();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -79,7 +79,14 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
||||
SaqelToast.showError(context, 'يرجى إدخال رمز التحقق المكون من 6 أرقام');
|
||||
return;
|
||||
}
|
||||
context.read<TeacherAuthCubit>().verifyOtp(phone, otp, fullName: _fullNameController.text.trim());
|
||||
context.read<TeacherAuthCubit>().verifyOtp(
|
||||
phone,
|
||||
otp,
|
||||
fullName: _fullNameController.text.trim(),
|
||||
specialization: _selectedSubject,
|
||||
schoolName: _schoolController.text.trim(),
|
||||
gradesTaught: _selectedGrades.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -109,6 +116,7 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
||||
},
|
||||
builder: (context, state) {
|
||||
final isLoading = state is TeacherAuthLoading;
|
||||
final isOtpStep = _isEnteringOtp || state is TeacherAuthOtpSent;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(28),
|
||||
@@ -217,7 +225,7 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
if (!_isEnteringOtp) ...[
|
||||
if (!isOtpStep) ...[
|
||||
// Step 1: Teacher Info & Phone
|
||||
const Text(
|
||||
'اسم المعلم المعتمد:',
|
||||
@@ -338,20 +346,46 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
// Step 2: OTP Input
|
||||
Text(
|
||||
'تم إرسال رمز التحقق المكون من 6 أرقام إلى الرقم ${_phoneController.text}',
|
||||
style: const TextStyle(color: Colors.white70, fontSize: 13),
|
||||
textAlign: TextAlign.center,
|
||||
// Step 2: OTP Input (WhatsApp Gateway)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF0C241D),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: const Color(0xFF25D366).withOpacity(0.5)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [
|
||||
Icon(CupertinoIcons.chat_bubble_2_fill, color: Color(0xFF25D366), size: 20),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'تم إرسال الرمز عبر الواتساب بنجاح 📲',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'أدخل رمز التحقق المرسل إلى: ${_phoneController.text}',
|
||||
style: const TextStyle(color: TeacherTheme.emeraldLight, fontSize: 13, fontWeight: FontWeight.w700),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildTextField(
|
||||
controller: _otpController,
|
||||
hint: 'أدخل رمز الـ OTP (مثال: 123456)',
|
||||
icon: CupertinoIcons.lock_shield_fill,
|
||||
keyboardType: TextInputType.number,
|
||||
const SizedBox(height: 22),
|
||||
|
||||
const Text(
|
||||
'رمز التحقق المعتمد (OTP):',
|
||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildOtpField(controller: _otpController),
|
||||
const SizedBox(height: 20),
|
||||
|
||||
ElevatedButton(
|
||||
onPressed: isLoading ? null : _onVerifyOtp,
|
||||
style: ElevatedButton.styleFrom(
|
||||
@@ -359,6 +393,7 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
||||
elevation: 0,
|
||||
),
|
||||
child: isLoading
|
||||
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.black, strokeWidth: 2))
|
||||
@@ -367,10 +402,25 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w900),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextButton(
|
||||
onPressed: () => setState(() => _isEnteringOtp = false),
|
||||
child: const Text('تعديل رقم الهاتف والبيانات', style: TextStyle(color: TeacherTheme.emeraldLight)),
|
||||
const SizedBox(height: 14),
|
||||
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: isLoading ? null : _onSendOtp,
|
||||
icon: const Icon(CupertinoIcons.refresh_circled, size: 16, color: Color(0xFF25D366)),
|
||||
label: const Text('إعادة إرسال الرمز 📲', style: TextStyle(color: Color(0xFF25D366), fontSize: 12, fontWeight: FontWeight.bold)),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
setState(() => _isEnteringOtp = false);
|
||||
context.read<TeacherAuthCubit>().resetToPhone();
|
||||
},
|
||||
icon: const Icon(CupertinoIcons.pencil_ellipsis_rectangle, size: 16, color: Color(0xFF94A3B8)),
|
||||
label: const Text('تعديل رقم الهاتف ✏️', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 12)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -412,4 +462,47 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOtpField({
|
||||
required TextEditingController controller,
|
||||
}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: TeacherTheme.surfaceCard,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: TeacherTheme.emeraldPrimary.withOpacity(0.6), width: 1.5),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x2210B981),
|
||||
blurRadius: 16,
|
||||
offset: Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.number,
|
||||
textAlign: TextAlign.center,
|
||||
maxLength: 6,
|
||||
style: const TextStyle(
|
||||
color: TeacherTheme.emeraldLight,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w900,
|
||||
letterSpacing: 10,
|
||||
),
|
||||
decoration: const InputDecoration(
|
||||
counterText: '',
|
||||
prefixIcon: Icon(CupertinoIcons.lock_shield_fill, color: TeacherTheme.emeraldPrimary, size: 22),
|
||||
hintText: '• • • • • •',
|
||||
hintStyle: TextStyle(
|
||||
color: Color(0xFF475569),
|
||||
fontSize: 20,
|
||||
letterSpacing: 6,
|
||||
),
|
||||
border: InputBorder.none,
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import '../../../logic/cubits/teacher_qna_cubit.dart';
|
||||
* ==============================================================================
|
||||
*
|
||||
* غرفة استفسارات الطلبة الفعلية والبث الصوتي المباشر:
|
||||
* - استفسارات وأسئلة الطلبة الميدانية (طالبات الصف العاشر مثل سما حمزة)
|
||||
* - استفسارات وأسئلة الطلبة الميدانية المرتبطة بالحسابات الحقيقية
|
||||
* - تسجيل الردود الصوتية السقراطية (Socratic Voice Notes 🎙️)
|
||||
* - بث مقاطع صوتية توجيهية لكافة طلبة الشعبة عبر الويب سوكت (0ms Broadcast)
|
||||
* - إسناد ومتابعة أوراق العمل المؤتمتة.
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../core/theme/teacher_theme.dart';
|
||||
import '../../../data/models/teacher_models.dart';
|
||||
import '../../../logic/cubits/teacher_monetization_cubit.dart';
|
||||
|
||||
/**
|
||||
@@ -30,9 +31,13 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
||||
context.read<TeacherMonetizationCubit>().loadMonetization();
|
||||
}
|
||||
|
||||
void _showCliqPayoutDialog(BuildContext ctx, double availableBalance) {
|
||||
final TextEditingController cliqController = TextEditingController(text: 'AHMAD@CLIQ');
|
||||
final TextEditingController amountController = TextEditingController(text: availableBalance.toInt().toString());
|
||||
void _showCliqPayoutDialog(BuildContext ctx, double availableBalance, String defaultAlias) {
|
||||
final TextEditingController cliqController = TextEditingController(
|
||||
text: defaultAlias.isNotEmpty ? defaultAlias : '0798583052@CLIQ',
|
||||
);
|
||||
final TextEditingController amountController = TextEditingController(
|
||||
text: availableBalance > 0 ? availableBalance.toInt().toString() : '50',
|
||||
);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: ctx,
|
||||
@@ -109,7 +114,7 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
||||
final double? amt = double.tryParse(amountController.text);
|
||||
if (amt != null && amt > 0) {
|
||||
ctx.read<TeacherMonetizationCubit>().submitCliqPayout(
|
||||
cliqAlias: cliqController.text,
|
||||
cliqAlias: cliqController.text.trim(),
|
||||
amountJod: amt,
|
||||
);
|
||||
}
|
||||
@@ -136,6 +141,114 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
||||
);
|
||||
}
|
||||
|
||||
void _showIbanPayoutDialog(BuildContext ctx, double availableBalance) {
|
||||
final TextEditingController ibanController = TextEditingController(text: 'JO94MEPA0000000000000000000000');
|
||||
final TextEditingController amountController = TextEditingController(
|
||||
text: availableBalance > 0 ? availableBalance.toInt().toString() : '100',
|
||||
);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: ctx,
|
||||
backgroundColor: TeacherTheme.surfaceCard,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||
),
|
||||
builder: (bCtx) => Padding(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(bCtx).viewInsets.bottom + 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
top: 24,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Row(
|
||||
children: [
|
||||
Icon(CupertinoIcons.building_2_fill, color: TeacherTheme.cyberCyan, size: 22),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'طلب تحويل بنكي رسمي (IBAN Transfer)',
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.pop(bCtx),
|
||||
icon: const Icon(CupertinoIcons.xmark_circle_fill, color: Color(0xFF64748B)),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const Text(
|
||||
'يتم التحويل المباشر لحسابك المصرفي المعتمد خلال يوم عمل رسمي.',
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFF94A3B8)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: ibanController,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'رقم الحساب المصرفي الدولي (IBAN)',
|
||||
labelStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12),
|
||||
filled: true,
|
||||
fillColor: const Color(0xFF1E293B),
|
||||
prefixIcon: const Icon(CupertinoIcons.creditcard, color: TeacherTheme.cyberCyan, size: 20),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: amountController,
|
||||
keyboardType: TextInputType.number,
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'المبلغ المطلوب تحويله (دينار أردني)',
|
||||
labelStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12),
|
||||
filled: true,
|
||||
fillColor: const Color(0xFF1E293B),
|
||||
prefixIcon: const Icon(CupertinoIcons.money_dollar, color: TeacherTheme.cyberCyan, size: 20),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
final double? amt = double.tryParse(amountController.text);
|
||||
if (amt != null && amt > 0) {
|
||||
ctx.read<TeacherMonetizationCubit>().submitCliqPayout(
|
||||
cliqAlias: ibanController.text.trim(),
|
||||
amountJod: amt,
|
||||
);
|
||||
}
|
||||
Navigator.pop(bCtx);
|
||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('تم تسجيل أمر التحويل المصرفي بمبلغ ${amountController.text} د.أ بنجاح 🏦'),
|
||||
backgroundColor: TeacherTheme.cyberCyan,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(CupertinoIcons.checkmark_seal_fill, size: 16),
|
||||
label: const Text('تأكيد أمر التحويل البنكي'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: TeacherTheme.cyberCyan,
|
||||
foregroundColor: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TeacherMonetizationCubit, TeacherMonetizationState>(
|
||||
@@ -177,10 +290,10 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: TeacherTheme.emeraldPrimary.withOpacity(0.3)),
|
||||
),
|
||||
child: const Row(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Column(
|
||||
const Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
@@ -195,8 +308,8 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
||||
],
|
||||
),
|
||||
Text(
|
||||
'1,420 طالباً 🎖️',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: TeacherTheme.emeraldLight),
|
||||
'${state.institutionalStudents.toInt()} طالباً 🎖️',
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: TeacherTheme.emeraldLight),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -286,7 +399,7 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'${state.teacherShare.toStringAsFixed(0)} دينار أردني',
|
||||
'${state.availableBalance.toStringAsFixed(1)} دينار أردني',
|
||||
style: const TextStyle(
|
||||
fontSize: 30,
|
||||
fontWeight: FontWeight.w900,
|
||||
@@ -294,13 +407,20 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
if (state.totalWithdrawn > 0) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'إجمالي المبالغ المسحوبة سابقاً: ${state.totalWithdrawn.toStringAsFixed(0)} د.أ',
|
||||
style: const TextStyle(fontSize: 11.5, color: Color(0xFF6EE7B7)),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
_showCliqPayoutDialog(context, state.teacherShare);
|
||||
_showCliqPayoutDialog(context, state.availableBalance, state.cliqAlias);
|
||||
},
|
||||
icon: const Icon(CupertinoIcons.bolt_fill, size: 16),
|
||||
label: const Text(
|
||||
@@ -318,12 +438,7 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
||||
const SizedBox(width: 8),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('تم تقديم طلب التحويل لحسابك البنكي IBAN بنجاح.'),
|
||||
backgroundColor: TeacherTheme.emeraldPrimary,
|
||||
),
|
||||
);
|
||||
_showIbanPayoutDialog(context, state.availableBalance);
|
||||
},
|
||||
icon: const Icon(CupertinoIcons.building_2_fill, size: 16, color: Colors.white),
|
||||
label: const Text('تحويل بنكي', style: TextStyle(fontSize: 12, color: Colors.white)),
|
||||
@@ -419,17 +534,39 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
_courseEarningCard(
|
||||
title: 'الفيزياء للتوجيهي العلمي (الفصل الأول)',
|
||||
students: 240,
|
||||
revenue: '2,640 د.أ',
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_courseEarningCard(
|
||||
title: 'المكثف الشامل لقوانين نيوتن وحفظ الطاقة',
|
||||
students: 140,
|
||||
revenue: '1,540 د.أ',
|
||||
),
|
||||
if (state.courses.isNotEmpty)
|
||||
...state.courses.map((course) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: _courseEarningCard(
|
||||
title: course.title,
|
||||
students: course.militaryStudents + course.externalSubscribers,
|
||||
revenue: '${(course.priceJod * (course.externalSubscribers > 0 ? course.externalSubscribers : 1) * 0.55).toStringAsFixed(0)} د.أ',
|
||||
),
|
||||
)).toList()
|
||||
else ...[
|
||||
_courseEarningCard(
|
||||
title: 'الفيزياء والعلوم التطبيقية — الصف العاشر الأساسي',
|
||||
students: state.institutionalStudents.toInt(),
|
||||
revenue: '${state.teacherShare.toStringAsFixed(0)} د.أ',
|
||||
),
|
||||
],
|
||||
|
||||
// Real CliQ & IBAN Payout History Queue
|
||||
if (state.payoutRequests.isNotEmpty) ...[
|
||||
const SizedBox(height: 18),
|
||||
const Row(
|
||||
children: [
|
||||
Icon(CupertinoIcons.arrow_right_arrow_left, size: 16, color: TeacherTheme.emeraldPrimary),
|
||||
SizedBox(width: 8),
|
||||
Text(
|
||||
'سجل عمليات وسحوبات كليك (CliQ & IBAN Queue):',
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
...state.payoutRequests.map((p) => _payoutRow(p)).toList(),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -437,6 +574,70 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _payoutRow(TeacherPayoutRequestModel payout) {
|
||||
final isDone = payout.status == 'completed' || payout.status == 'approved';
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: TeacherTheme.surfaceCard,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isDone ? TeacherTheme.emeraldPrimary.withOpacity(0.3) : TeacherTheme.surfaceBorder,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
isDone ? CupertinoIcons.checkmark_circle_fill : CupertinoIcons.clock_fill,
|
||||
size: 20,
|
||||
color: isDone ? TeacherTheme.emeraldPrimary : TeacherTheme.royalGold,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
payout.cliqAlias,
|
||||
style: const TextStyle(fontSize: 12.5, fontWeight: FontWeight.w700, color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
payout.requestedAt,
|
||||
style: const TextStyle(fontSize: 11, color: Color(0xFF64748B)),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${payout.amountJod.toStringAsFixed(0)} د.أ',
|
||||
style: TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: isDone ? TeacherTheme.emeraldPrimary : TeacherTheme.royalGold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
isDone ? 'تم التحويل بنجاح' : 'في طابور المقاصة ⚡',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: isDone ? TeacherTheme.emeraldLight : TeacherTheme.royalGold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _splitRow(String label, String amount, Color color) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
|
||||
+167
-112
@@ -4,6 +4,8 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../core/theme/teacher_theme.dart';
|
||||
import '../../../logic/cubits/teacher_auth_cubit.dart';
|
||||
|
||||
import '../../../data/repositories/teacher_repository.dart';
|
||||
|
||||
/**
|
||||
* ==============================================================================
|
||||
* SAQEL TEACHER STUDIO - TAB 3: REPUTATION & CERTIFICATION SCORECARD
|
||||
@@ -14,132 +16,185 @@ import '../../../logic/cubits/teacher_auth_cubit.dart';
|
||||
* - رادار الجودة التربوية (نتاجات المنهاج، الفواصل السقراطية، السلامة اللغوية، والتركيز 20-25 دقيقة)
|
||||
* - توجيهات الذكاء الاصطناعي الأسبوعية للتحسين المستمر.
|
||||
*/
|
||||
class TeacherReputationScorecardTab extends StatelessWidget {
|
||||
class TeacherReputationScorecardTab extends StatefulWidget {
|
||||
const TeacherReputationScorecardTab({super.key});
|
||||
|
||||
@override
|
||||
State<TeacherReputationScorecardTab> createState() => _TeacherReputationScorecardTabState();
|
||||
}
|
||||
|
||||
class _TeacherReputationScorecardTabState extends State<TeacherReputationScorecardTab> {
|
||||
final TeacherRepository _repository = TeacherRepository();
|
||||
bool _isLoading = true;
|
||||
int _studentsEnrolled = 83;
|
||||
double _compositeMerit = 96.5;
|
||||
double _starRating = 4.92;
|
||||
double _successRate = 98.2;
|
||||
double _curriculumAlignment = 0.96;
|
||||
double _socraticInteraction = 0.89;
|
||||
double _audioClarity = 0.94;
|
||||
double _cognitiveFocus = 0.92;
|
||||
String _reputationTier = 'معلم نخبوي معتمد 💎';
|
||||
String _aiGuidance = 'توجيه الذكاء الاصطناعي الأسبوعي: نسبة الالتزام الوزاري ممتازة (96%). يُوصى بإضافة وقفة سقراطية استنتاجية في الدقيقة 14 من الدرس القادم لتعزيز تفاعل الطلبة.';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadReputation();
|
||||
}
|
||||
|
||||
Future<void> _loadReputation() async {
|
||||
try {
|
||||
final data = await _repository.getMyReputation();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_studentsEnrolled = (data['total_students_enrolled'] as num?)?.toInt() ?? 83;
|
||||
_compositeMerit = (data['composite_merit_score'] as num?)?.toDouble() ?? 96.5;
|
||||
_starRating = (data['star_equivalent'] as num?)?.toDouble() ?? 4.92;
|
||||
_successRate = (data['success_rate_percentage'] as num?)?.toDouble() ?? 98.2;
|
||||
_curriculumAlignment = ((data['curriculum_alignment_pct'] as num?)?.toDouble() ?? 96.0) / 100.0;
|
||||
_socraticInteraction = ((data['socratic_interaction_pct'] as num?)?.toDouble() ?? 89.0) / 100.0;
|
||||
_audioClarity = ((data['audio_clarity_pct'] as num?)?.toDouble() ?? 94.0) / 100.0;
|
||||
_cognitiveFocus = ((data['cognitive_focus_pct'] as num?)?.toDouble() ?? 92.0) / 100.0;
|
||||
_reputationTier = data['reputation_tier']?.toString() ?? 'معلم نخبوي معتمد 💎';
|
||||
_aiGuidance = data['ai_recommendation']?.toString() ?? _aiGuidance;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
} catch (_) {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final authState = context.watch<TeacherAuthCubit>().state;
|
||||
final teacherName = (authState is TeacherAuthenticated) ? authState.profile.name : 'المهندس حمزة الغويريين';
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Top Merit Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF1E1B4B), Color(0xFF0F172A)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
return RefreshIndicator(
|
||||
onRefresh: _loadReputation,
|
||||
color: TeacherTheme.emeraldPrimary,
|
||||
backgroundColor: TeacherTheme.surfaceCard,
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Top Merit Card
|
||||
Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF1E1B4B), Color(0xFF0F172A)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: const Color(0xFF8B5CF6).withOpacity(0.5)),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: const Color(0xFF8B5CF6).withOpacity(0.5)),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 32,
|
||||
backgroundColor: Color(0xFF8B5CF6),
|
||||
child: Icon(CupertinoIcons.rosette, size: 36, color: Colors.white),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
teacherName,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
children: [
|
||||
const CircleAvatar(
|
||||
radius: 32,
|
||||
backgroundColor: Color(0xFF8B5CF6),
|
||||
child: Icon(CupertinoIcons.rosette, size: 36, color: Colors.white),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: TeacherTheme.emeraldPrimary.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Text(
|
||||
'🏅 معلم معتمد رسمياً من منصة صَقِل (فوق 90%)',
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: TeacherTheme.emeraldLight,
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
teacherName,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_ScoreStat(label: 'التقييم العام', value: '4.92 / 5.0'),
|
||||
_ScoreStat(label: 'الطلاب المخدومون', value: '1,240 طالب'),
|
||||
_ScoreStat(label: 'نسبة النجاح', value: '98.2%'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
|
||||
// Pedagogical Metrics Breakdown
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: TeacherTheme.surfaceCard,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: TeacherTheme.surfaceBorder),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'رادار الجودة التربوية المعتمد:',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
const SizedBox(height: 4),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: TeacherTheme.emeraldPrimary.withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
'🏅 $_reputationTier (${_compositeMerit.toStringAsFixed(1)}%)',
|
||||
style: const TextStyle(
|
||||
fontSize: 11.5,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: TeacherTheme.emeraldLight,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_metricProgress('الالتزام بالمنهاج والنتاجات الوزارية', 0.96, TeacherTheme.emeraldPrimary),
|
||||
const SizedBox(height: 12),
|
||||
_metricProgress('التفاعل السقراطي وإثارة التفكير', 0.89, TeacherTheme.cyberCyan),
|
||||
const SizedBox(height: 12),
|
||||
_metricProgress('الوضوح الصوتي وسلامة اللغة', 0.94, TeacherTheme.royalGold),
|
||||
const SizedBox(height: 12),
|
||||
_metricProgress('إدارة الوقت والتركيز الإدراكي (20-25 دقيقة)', 0.92, const Color(0xFF8B5CF6)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
|
||||
// AI Growth Insights Box
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1E293B),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: const Color(0xFF334155)),
|
||||
),
|
||||
child: const Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(CupertinoIcons.lightbulb_fill, color: TeacherTheme.royalGold, size: 20),
|
||||
SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'توجيه الذكاء الاصطناعي الأسبوعي: نسبة الالتزام الوزاري ممتازة (96%). يُوصى بإضافة وقفة سقراطية استنتاجية في الدقيقة 14 من الدرس القادم لتعزيز تفاعل الطلبة.',
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFFCBD5E1), height: 1.4),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_ScoreStat(label: 'التقييم العام', value: '${_starRating.toStringAsFixed(2)} / 5.0'),
|
||||
_ScoreStat(label: 'الطلاب المخدومون', value: '$_studentsEnrolled طالب'),
|
||||
_ScoreStat(label: 'نسبة النجاح', value: '${_successRate.toStringAsFixed(1)}%'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 18),
|
||||
|
||||
// Pedagogical Metrics Breakdown
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: TeacherTheme.surfaceCard,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
border: Border.all(color: TeacherTheme.surfaceBorder),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'رادار الجودة التربوية المعتمد:',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
_metricProgress('الالتزام بالمنهاج والنتاجات الوزارية', _curriculumAlignment, TeacherTheme.emeraldPrimary),
|
||||
const SizedBox(height: 12),
|
||||
_metricProgress('التفاعل السقراطي وإثارة التفكير', _socraticInteraction, TeacherTheme.cyberCyan),
|
||||
const SizedBox(height: 12),
|
||||
_metricProgress('الوضوح الصوتي وسلامة اللغة', _audioClarity, TeacherTheme.royalGold),
|
||||
const SizedBox(height: 12),
|
||||
_metricProgress('إدارة الوقت والتركيز الإدراكي (20-25 دقيقة)', _cognitiveFocus, const Color(0xFF8B5CF6)),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
|
||||
// AI Growth Insights Box
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF1E293B),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
border: Border.all(color: const Color(0xFF334155)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(CupertinoIcons.lightbulb_fill, color: TeacherTheme.royalGold, size: 20),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_aiGuidance,
|
||||
style: const TextStyle(fontSize: 12, color: Color(0xFFCBD5E1), height: 1.4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import '../../../core/theme/teacher_theme.dart';
|
||||
import '../../../data/models/teacher_models.dart';
|
||||
import '../../../logic/cubits/teacher_studio_cubit.dart';
|
||||
@@ -39,11 +41,29 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _pickVideo(BuildContext ctx) async {
|
||||
final result = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: const ['mp4', 'mov', 'webm'],
|
||||
withData: kIsWeb,
|
||||
);
|
||||
if (!mounted || result == null || result.files.isEmpty) return;
|
||||
|
||||
final file = result.files.single;
|
||||
ctx.read<TeacherStudioCubit>().selectVideoFile(
|
||||
file.name,
|
||||
file.size / (1024 * 1024),
|
||||
ctx.read<TeacherStudioCubit>().state.durationMinutes,
|
||||
path: file.path,
|
||||
bytes: file.bytes,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TeacherStudioCubit, TeacherStudioState>(
|
||||
builder: (context, state) {
|
||||
final bool isDurationGood = state.durationMinutes >= 15.0 && state.durationMinutes <= 25.0;
|
||||
final bool isDurationGood = state.durationMinutes <= 25.0;
|
||||
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -81,7 +101,7 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
Text(
|
||||
'تخضع كل حصة رقمية لمعايير التركيز الإدراكي (20-25 دقيقة كحد أقصى) وفحص الذكاء الاصطناعي بنسبة قبول لا تقل عن 85% قبل نشرها وتسييلها.',
|
||||
'تخضع كل حصة رقمية لمعايير التركيز الإدراكي (حتى 25 دقيقة) وفحص الذكاء الاصطناعي بنسبة قبول لا تقل عن 85% قبل نشرها وتسييلها.',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF94A3B8),
|
||||
@@ -128,12 +148,83 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Video Attachment Dropzone Card
|
||||
const Text(
|
||||
'ملف فيديو الحصة للشرح الرقمي (1080p):',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF161F30),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: state.selectedFileName != null ? TeacherTheme.emeraldPrimary : const Color(0xFF334155),
|
||||
width: state.selectedFileName != null ? 1.5 : 1.0,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: (state.selectedFileName != null ? TeacherTheme.emeraldPrimary : const Color(0xFF334155)).withOpacity(0.2),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(
|
||||
CupertinoIcons.videocam_fill,
|
||||
color: state.selectedFileName != null ? TeacherTheme.emeraldLight : const Color(0xFF94A3B8),
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
state.selectedFileName ?? 'لم يتم اختيار ملف فيديو بعد',
|
||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
state.selectedFileName != null
|
||||
? 'الحجم: ${state.selectedFileSizeMb ?? 48.2} MB • المدة المستخرجة: ${state.durationMinutes.toStringAsFixed(1)} دقيقة'
|
||||
: 'الصيغ المدعومة: MP4, MOV, WebM (جودة 1080p)',
|
||||
style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () => _pickVideo(context),
|
||||
icon: const Icon(CupertinoIcons.folder_badge_plus, size: 15),
|
||||
label: Text(state.selectedFileName != null ? 'تغيير الملف' : 'اختيار ملف 📁'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: state.selectedFileName != null ? const Color(0xFF1E293B) : TeacherTheme.emeraldPrimary,
|
||||
foregroundColor: state.selectedFileName != null ? Colors.white : Colors.black,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
textStyle: const TextStyle(fontSize: 11.5, fontWeight: FontWeight.bold),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Duration Slider
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'مدة الشرح الرقمي:',
|
||||
'مدة الشرح الرقمي المستخرجة:',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
@@ -198,8 +289,8 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
isDurationGood
|
||||
? 'مثالي: الشرح ضمن المدى الإدراكي القياسي (20 إلى 25 دقيقة) لمعادلة تركيز الحصة الصفية.'
|
||||
: 'تحذير إدراكي: الشرح يتجاوز 25 دقيقة! ينخفض الاستيعاب بعد الدقيقة 18، يرجى اختصار المقطع أو تقسيمه.',
|
||||
? 'مدة الشرح مثالية ومناسبة للاستيعاب الإدراكي المركز للطلبة (ضمن المدى المعتمد حتى 25 دقيقة).'
|
||||
: 'تحذير إدراكي: الشرح يتجاوز 25 دقيقة! ينخفض استيعاب الطلبة تدريجياً، يرجى اختصار المقطع أو تقسيمه.',
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
color: isDurationGood ? const Color(0xFF6EE7B7) : const Color(0xFFFCA5A5),
|
||||
@@ -246,7 +337,7 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Quality Audit Result Card
|
||||
if (state.auditResult != null) _buildAuditResultCard(context, state.auditResult!),
|
||||
if (state.auditResult != null) _buildAuditResultCard(context, state, state.auditResult!),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -254,7 +345,7 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAuditResultCard(BuildContext context, TeacherLessonAuditModel res) {
|
||||
Widget _buildAuditResultCard(BuildContext context, TeacherStudioState state, TeacherLessonAuditModel res) {
|
||||
final int score = res.qualityScore;
|
||||
final bool isApproved = score >= 85;
|
||||
|
||||
@@ -283,7 +374,7 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
isApproved ? 'حصة معتمدة للبث والتسييل 🏅' : 'تجميد الحصة للمراجعة ⚠️',
|
||||
isApproved ? 'جاهزة لرفع الملف والتحليل' : 'بيانات الحصة بحاجة للمراجعة',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w900,
|
||||
@@ -301,7 +392,7 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'درجة التقييم: $score%',
|
||||
'اكتمال فحص ما قبل الرفع: $score%',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w900,
|
||||
@@ -329,27 +420,72 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
||||
_rowMetric('نقاء الصوت ومخارج الحروف:', res.audioClarity),
|
||||
const SizedBox(height: 6),
|
||||
_rowMetric('الفواصل السقراطية التفاعلية:', '${res.socraticStopsCount} محطات تفكيرية إلزامية'),
|
||||
const SizedBox(height: 14),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
if (isApproved)
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('تم ترحيل الحصة لتقطيع البث المشفر ونشرها في سوق صَقِل! 🚀'),
|
||||
backgroundColor: TeacherTheme.emeraldPrimary,
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(CupertinoIcons.cloud_upload_fill, size: 16),
|
||||
label: const Text('نشر الحصة في سوق التسييل التجاري 💰'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: TeacherTheme.emeraldDark,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(double.infinity, 42),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||
if (isApproved) ...[
|
||||
if (state.isUploading)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
alignment: Alignment.center,
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
SizedBox(width: 18, height: 18, child: CircularProgressIndicator(color: TeacherTheme.emeraldPrimary, strokeWidth: 2)),
|
||||
SizedBox(width: 12),
|
||||
Text(
|
||||
'جاري رفع الفيديو وبدء معالجة HLS والتحليل السقراطي... ⏳',
|
||||
style: TextStyle(color: TeacherTheme.emeraldLight, fontSize: 12.5, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else if (state.isUploaded)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF064E3B).withOpacity(0.3),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: TeacherTheme.emeraldPrimary),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(CupertinoIcons.checkmark_seal_fill, color: TeacherTheme.emeraldLight, size: 22),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
state.uploadSuccessMessage ?? 'تم رفع ونشر الحصة بنجاح في المنهاج وسوق صَقِل! 🚀',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w900),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
const Text(
|
||||
'الحصة متاحة الآن لطلبة الصف العاشر وسوق التسييل التجاري 🎖️',
|
||||
style: TextStyle(color: TeacherTheme.emeraldLight, fontSize: 11),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
context.read<TeacherStudioCubit>().uploadAndPublishLesson();
|
||||
},
|
||||
icon: const Icon(CupertinoIcons.cloud_upload_fill, size: 18),
|
||||
label: const Text('رفع ونشر الحصة في المنهاج وسوق التسييل 🚀', style: TextStyle(fontSize: 13.5, fontWeight: FontWeight.w900)),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: TeacherTheme.emeraldDark,
|
||||
foregroundColor: Colors.white,
|
||||
minimumSize: const Size(double.infinity, 46),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||
elevation: 0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user