Update Saqel Platform: 2026-09-08 18:19:28
This commit is contained in:
@@ -340,10 +340,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.19"
|
version: "0.12.18"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -356,10 +356,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.17.0"
|
||||||
nested:
|
nested:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -585,10 +585,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.11"
|
version: "0.7.9"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class AppConfig {
|
|||||||
static const String auditVideoEndpoint = '/api/teacher/audit-studio-video';
|
static const String auditVideoEndpoint = '/api/teacher/audit-studio-video';
|
||||||
static const String uploadLessonEndpoint = '/api/teacher/lessons/upload';
|
static const String uploadLessonEndpoint = '/api/teacher/lessons/upload';
|
||||||
static const String uploadVideoEndpoint = '/api/teacher/videos/upload-direct';
|
static const String uploadVideoEndpoint = '/api/teacher/videos/upload-direct';
|
||||||
|
static const String curriculumTreeEndpoint = '/api/curriculum/tree';
|
||||||
static const String qnaEndpoint = '/api/teacher/qna';
|
static const String qnaEndpoint = '/api/teacher/qna';
|
||||||
static const String cliqPayoutEndpoint = '/api/teacher/payout/request';
|
static const String cliqPayoutEndpoint = '/api/teacher/payout/request';
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,8 @@ class StorageService {
|
|||||||
return _volatileStore[key];
|
return _volatileStore[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveToken(String token) async => await _writeSafe(_keyToken, token);
|
Future<void> saveToken(String token) async =>
|
||||||
|
await _writeSafe(_keyToken, token);
|
||||||
Future<String?> getToken() async => await _readSafe(_keyToken);
|
Future<String?> getToken() async => await _readSafe(_keyToken);
|
||||||
|
|
||||||
Future<void> clearSession() async {
|
Future<void> clearSession() async {
|
||||||
|
|||||||
@@ -21,21 +21,25 @@ class AppLogger {
|
|||||||
if (!kDebugMode) return;
|
if (!kDebugMode) return;
|
||||||
|
|
||||||
final buffer = StringBuffer();
|
final buffer = StringBuffer();
|
||||||
buffer.writeln('\n🌐 ─── [HTTP REQUEST (TEACHER)] ──────────────────────────────────');
|
buffer.writeln(
|
||||||
|
'\n🌐 ─── [HTTP REQUEST (TEACHER)] ──────────────────────────────────');
|
||||||
buffer.writeln('➡️ Method: $method');
|
buffer.writeln('➡️ Method: $method');
|
||||||
buffer.writeln('🔗 URL: $uri');
|
buffer.writeln('🔗 URL: $uri');
|
||||||
if (headers != null && headers.isNotEmpty) {
|
if (headers != null && headers.isNotEmpty) {
|
||||||
final safeHeaders = Map<String, String>.from(headers);
|
final safeHeaders = Map<String, String>.from(headers);
|
||||||
if (safeHeaders.containsKey('Authorization')) {
|
if (safeHeaders.containsKey('Authorization')) {
|
||||||
final auth = safeHeaders['Authorization']!;
|
final auth = safeHeaders['Authorization']!;
|
||||||
safeHeaders['Authorization'] = auth.length > 20 ? '${auth.substring(0, 15)}...' : 'Bearer [REDACTED]';
|
safeHeaders['Authorization'] = auth.length > 20
|
||||||
|
? '${auth.substring(0, 15)}...'
|
||||||
|
: 'Bearer [REDACTED]';
|
||||||
}
|
}
|
||||||
buffer.writeln('📋 Headers: ${jsonEncode(safeHeaders)}');
|
buffer.writeln('📋 Headers: ${jsonEncode(safeHeaders)}');
|
||||||
}
|
}
|
||||||
if (body != null) {
|
if (body != null) {
|
||||||
buffer.writeln('📦 Body: ${body is String ? body : jsonEncode(body)}');
|
buffer.writeln('📦 Body: ${body is String ? body : jsonEncode(body)}');
|
||||||
}
|
}
|
||||||
buffer.writeln('───────────────────────────────────────────────────────────────');
|
buffer.writeln(
|
||||||
|
'───────────────────────────────────────────────────────────────');
|
||||||
debugPrint(buffer.toString());
|
debugPrint(buffer.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,24 +56,29 @@ class AppLogger {
|
|||||||
final emoji = isSuccess ? '✅' : '⚠️';
|
final emoji = isSuccess ? '✅' : '⚠️';
|
||||||
final buffer = StringBuffer();
|
final buffer = StringBuffer();
|
||||||
|
|
||||||
buffer.writeln('\n$emoji ─── [HTTP RESPONSE $statusCode (TEACHER)] ──────────────────────');
|
buffer.writeln(
|
||||||
|
'\n$emoji ─── [HTTP RESPONSE $statusCode (TEACHER)] ──────────────────────');
|
||||||
buffer.writeln('⬅️ Method: $method');
|
buffer.writeln('⬅️ Method: $method');
|
||||||
buffer.writeln('🔗 URL: $uri');
|
buffer.writeln('🔗 URL: $uri');
|
||||||
if (duration != null) {
|
if (duration != null) {
|
||||||
buffer.writeln('⏱️ Time: ${duration.inMilliseconds}ms');
|
buffer.writeln('⏱️ Time: ${duration.inMilliseconds}ms');
|
||||||
}
|
}
|
||||||
if (responseBody != null) {
|
if (responseBody != null) {
|
||||||
buffer.writeln('📦 Data: ${responseBody is String ? responseBody : jsonEncode(responseBody)}');
|
buffer.writeln(
|
||||||
|
'📦 Data: ${responseBody is String ? responseBody : jsonEncode(responseBody)}');
|
||||||
}
|
}
|
||||||
buffer.writeln('───────────────────────────────────────────────────────────────');
|
buffer.writeln(
|
||||||
|
'───────────────────────────────────────────────────────────────');
|
||||||
debugPrint(buffer.toString());
|
debugPrint(buffer.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
static void error(String message, {dynamic error, StackTrace? stackTrace, String tag = 'TEACHER_ERROR'}) {
|
static void error(String message,
|
||||||
|
{dynamic error, StackTrace? stackTrace, String tag = 'TEACHER_ERROR'}) {
|
||||||
if (!kDebugMode) return;
|
if (!kDebugMode) return;
|
||||||
|
|
||||||
final buffer = StringBuffer();
|
final buffer = StringBuffer();
|
||||||
buffer.writeln('\n🚨 ─── [ERROR: $tag] ──────────────────────────────────────────');
|
buffer.writeln(
|
||||||
|
'\n🚨 ─── [ERROR: $tag] ──────────────────────────────────────────');
|
||||||
buffer.writeln('❌ Message: $message');
|
buffer.writeln('❌ Message: $message');
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
buffer.writeln('⚠️ Error: $error');
|
buffer.writeln('⚠️ Error: $error');
|
||||||
@@ -77,7 +86,8 @@ class AppLogger {
|
|||||||
if (stackTrace != null) {
|
if (stackTrace != null) {
|
||||||
buffer.writeln('📍 Trace: \n$stackTrace');
|
buffer.writeln('📍 Trace: \n$stackTrace');
|
||||||
}
|
}
|
||||||
buffer.writeln('───────────────────────────────────────────────────────────────');
|
buffer.writeln(
|
||||||
|
'───────────────────────────────────────────────────────────────');
|
||||||
debugPrint(buffer.toString());
|
debugPrint(buffer.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,18 +38,28 @@ class TeacherLessonAuditModel {
|
|||||||
});
|
});
|
||||||
|
|
||||||
factory TeacherLessonAuditModel.fromJson(Map<String, dynamic> json) {
|
factory TeacherLessonAuditModel.fromJson(Map<String, dynamic> json) {
|
||||||
|
final durationSeconds = (json['duration_seconds'] as num?)?.toDouble() ?? 0;
|
||||||
|
final readiness = (json['pedagogical_readiness_score'] as num?)?.toInt();
|
||||||
|
final visual = (json['visual_clarity_score'] as num?)?.toInt();
|
||||||
|
final decisionValue = json['decision']?.toString() ?? 'pending';
|
||||||
return TeacherLessonAuditModel(
|
return TeacherLessonAuditModel(
|
||||||
lessonTitle: json['lesson_title'] ?? '',
|
lessonTitle: json['lesson_title'] ?? '',
|
||||||
subject: json['subject'] ?? 'الفيزياء',
|
subject: json['subject'] ?? '',
|
||||||
durationMinutes: (json['duration_minutes'] as num?)?.toDouble() ?? 22.5,
|
durationMinutes: (json['duration_minutes'] as num?)?.toDouble() ??
|
||||||
durationGatePassed: json['duration_gate_passed'] ?? true,
|
durationSeconds / 60,
|
||||||
|
durationGatePassed: json['duration_gate_passed'] ??
|
||||||
|
(durationSeconds > 0 && durationSeconds <= 1500),
|
||||||
durationWarning: json['duration_warning'],
|
durationWarning: json['duration_warning'],
|
||||||
qualityScore: json['quality_score'] ?? 92,
|
qualityScore: (json['quality_score'] as num?)?.toInt() ?? readiness ?? 0,
|
||||||
approvalStatus: json['approval_status'] ?? 'approved_for_broadcast',
|
approvalStatus: json['approval_status'] ?? decisionValue,
|
||||||
curriculumAlignment: json['curriculum_alignment'] ?? '96% تطابق مع مخرجات المنهاج الوزاري',
|
curriculumAlignment:
|
||||||
audioClarity: json['audio_clarity'] ?? '95% نقاء صوتي ممتاز',
|
json['curriculum_alignment'] ?? 'بانتظار تحليل الملف الفعلي',
|
||||||
socraticStopsCount: json['socratic_stops_count'] ?? 3,
|
audioClarity: json['audio_clarity'] ??
|
||||||
decision: json['decision'] ?? 'الحصة معتمدة ومؤهلة للبث المشفر والعرض للبيع خارج الثقافة العسكرية 🚀',
|
(visual == null
|
||||||
|
? 'تم التحقق من وجود مسار صوت فعلي'
|
||||||
|
: 'وضوح الصورة $visual%، وتم التحقق من وجود الصوت'),
|
||||||
|
socraticStopsCount: (json['socratic_stops_count'] as num?)?.toInt() ?? 0,
|
||||||
|
decision: json['report'] ?? json['reason'] ?? decisionValue,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -179,7 +189,8 @@ class TeacherProfileModel {
|
|||||||
|
|
||||||
List<String> grades = [];
|
List<String> grades = [];
|
||||||
if (profile['grades_taught'] is List) {
|
if (profile['grades_taught'] is List) {
|
||||||
grades = (profile['grades_taught'] as List).map((e) => e.toString()).toList();
|
grades =
|
||||||
|
(profile['grades_taught'] as List).map((e) => e.toString()).toList();
|
||||||
} else if (profile['grades_taught'] is String) {
|
} else if (profile['grades_taught'] is String) {
|
||||||
grades = [profile['grades_taught'].toString()];
|
grades = [profile['grades_taught'].toString()];
|
||||||
}
|
}
|
||||||
@@ -191,7 +202,8 @@ class TeacherProfileModel {
|
|||||||
id: user['id'] is int ? user['id'] : 1,
|
id: user['id'] is int ? user['id'] : 1,
|
||||||
uuid: user['uuid']?.toString() ?? '',
|
uuid: user['uuid']?.toString() ?? '',
|
||||||
name: user['full_name']?.toString() ?? 'حساب معلم غير مكتمل',
|
name: user['full_name']?.toString() ?? 'حساب معلم غير مكتمل',
|
||||||
specialization: profile['specialization']?.toString() ?? 'بانتظار استكمال التخصص',
|
specialization:
|
||||||
|
profile['specialization']?.toString() ?? 'بانتظار استكمال التخصص',
|
||||||
schoolName: profile['school_name']?.toString() ?? '',
|
schoolName: profile['school_name']?.toString() ?? '',
|
||||||
gradesTaught: grades,
|
gradesTaught: grades,
|
||||||
bio: profile['bio']?.toString() ?? '',
|
bio: profile['bio']?.toString() ?? '',
|
||||||
@@ -248,7 +260,9 @@ class ChatConversationModel {
|
|||||||
|
|
||||||
factory ChatConversationModel.fromJson(Map<String, dynamic> json) {
|
factory ChatConversationModel.fromJson(Map<String, dynamic> json) {
|
||||||
return ChatConversationModel(
|
return ChatConversationModel(
|
||||||
userId: json['user_id'] is int ? json['user_id'] : int.tryParse(json['user_id']?.toString() ?? '0') ?? 0,
|
userId: json['user_id'] is int
|
||||||
|
? json['user_id']
|
||||||
|
: int.tryParse(json['user_id']?.toString() ?? '0') ?? 0,
|
||||||
userUuid: json['user_uuid']?.toString() ?? '',
|
userUuid: json['user_uuid']?.toString() ?? '',
|
||||||
fullName: json['full_name']?.toString() ?? 'طالب صَقِل',
|
fullName: json['full_name']?.toString() ?? 'طالب صَقِل',
|
||||||
role: json['role']?.toString() ?? 'student',
|
role: json['role']?.toString() ?? 'student',
|
||||||
@@ -288,11 +302,17 @@ class ChatMessageModel {
|
|||||||
|
|
||||||
factory ChatMessageModel.fromJson(Map<String, dynamic> json) {
|
factory ChatMessageModel.fromJson(Map<String, dynamic> json) {
|
||||||
return ChatMessageModel(
|
return ChatMessageModel(
|
||||||
id: json['id'] is int ? json['id'] : int.tryParse(json['id']?.toString() ?? '0') ?? 0,
|
id: json['id'] is int
|
||||||
|
? json['id']
|
||||||
|
: int.tryParse(json['id']?.toString() ?? '0') ?? 0,
|
||||||
uuid: json['uuid']?.toString() ?? '',
|
uuid: json['uuid']?.toString() ?? '',
|
||||||
isMine: json['is_mine'] ?? false,
|
isMine: json['is_mine'] ?? false,
|
||||||
senderId: json['sender_id'] is int ? json['sender_id'] : int.tryParse(json['sender_id']?.toString() ?? '0') ?? 0,
|
senderId: json['sender_id'] is int
|
||||||
receiverId: json['receiver_id'] is int ? json['receiver_id'] : int.tryParse(json['receiver_id']?.toString() ?? '0') ?? 0,
|
? json['sender_id']
|
||||||
|
: int.tryParse(json['sender_id']?.toString() ?? '0') ?? 0,
|
||||||
|
receiverId: json['receiver_id'] is int
|
||||||
|
? json['receiver_id']
|
||||||
|
: int.tryParse(json['receiver_id']?.toString() ?? '0') ?? 0,
|
||||||
message: json['message']?.toString() ?? '',
|
message: json['message']?.toString() ?? '',
|
||||||
messageType: json['message_type']?.toString() ?? 'text',
|
messageType: json['message_type']?.toString() ?? 'text',
|
||||||
mediaUrl: json['media_url']?.toString(),
|
mediaUrl: json['media_url']?.toString(),
|
||||||
|
|||||||
@@ -47,16 +47,22 @@ class TeacherRepository {
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
Future<Map<String, dynamic>> requestOtp(String phoneNumber) async {
|
Future<Map<String, dynamic>> requestOtp(String phoneNumber) async {
|
||||||
AppLogger.log('Requesting Teacher OTP for $phoneNumber...', tag: 'TEACHER_REPO');
|
AppLogger.log('Requesting Teacher OTP for $phoneNumber...',
|
||||||
|
tag: 'TEACHER_REPO');
|
||||||
try {
|
try {
|
||||||
final response = await _client.post(
|
final response = await _client
|
||||||
|
.post(
|
||||||
Uri.parse('$baseUrl${AppConfig.otpRequestEndpoint}'),
|
Uri.parse('$baseUrl${AppConfig.otpRequestEndpoint}'),
|
||||||
headers: {'Content-Type': 'application/json', 'Accept': 'application/json'},
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
body: json.encode({
|
body: json.encode({
|
||||||
'phone_number': phoneNumber,
|
'phone_number': phoneNumber,
|
||||||
'role': 'teacher',
|
'role': 'teacher',
|
||||||
}),
|
}),
|
||||||
).timeout(const Duration(seconds: 30));
|
)
|
||||||
|
.timeout(const Duration(seconds: 30));
|
||||||
|
|
||||||
return json.decode(response.body);
|
return json.decode(response.body);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -65,12 +71,18 @@ class TeacherRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> verifyOtp(String phoneNumber, String otp, {String? fullName}) async {
|
Future<Map<String, dynamic>> verifyOtp(String phoneNumber, String otp,
|
||||||
AppLogger.log('Verifying Teacher OTP for $phoneNumber...', tag: 'TEACHER_REPO');
|
{String? fullName}) async {
|
||||||
|
AppLogger.log('Verifying Teacher OTP for $phoneNumber...',
|
||||||
|
tag: 'TEACHER_REPO');
|
||||||
try {
|
try {
|
||||||
final response = await _client.post(
|
final response = await _client
|
||||||
|
.post(
|
||||||
Uri.parse('$baseUrl${AppConfig.otpVerifyEndpoint}'),
|
Uri.parse('$baseUrl${AppConfig.otpVerifyEndpoint}'),
|
||||||
headers: {'Content-Type': 'application/json', 'Accept': 'application/json'},
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
},
|
||||||
body: json.encode({
|
body: json.encode({
|
||||||
'phone_number': phoneNumber,
|
'phone_number': phoneNumber,
|
||||||
'otp': otp,
|
'otp': otp,
|
||||||
@@ -78,7 +90,8 @@ class TeacherRepository {
|
|||||||
if (fullName != null) 'full_name': fullName,
|
if (fullName != null) 'full_name': fullName,
|
||||||
'device_fingerprint': 'saqel_teacher_flutter',
|
'device_fingerprint': 'saqel_teacher_flutter',
|
||||||
}),
|
}),
|
||||||
).timeout(const Duration(seconds: 30));
|
)
|
||||||
|
.timeout(const Duration(seconds: 30));
|
||||||
|
|
||||||
final decoded = json.decode(response.body);
|
final decoded = json.decode(response.body);
|
||||||
if (decoded['status'] == 'success' && decoded['data']?['token'] != null) {
|
if (decoded['status'] == 'success' && decoded['data']?['token'] != null) {
|
||||||
@@ -95,10 +108,12 @@ class TeacherRepository {
|
|||||||
Future<Map<String, dynamic>?> getMe() async {
|
Future<Map<String, dynamic>?> getMe() async {
|
||||||
try {
|
try {
|
||||||
final headers = await _authHeaders();
|
final headers = await _authHeaders();
|
||||||
final response = await _client.get(
|
final response = await _client
|
||||||
|
.get(
|
||||||
Uri.parse('$baseUrl${AppConfig.meEndpoint}'),
|
Uri.parse('$baseUrl${AppConfig.meEndpoint}'),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
).timeout(const Duration(seconds: 4));
|
)
|
||||||
|
.timeout(const Duration(seconds: 4));
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
return json.decode(response.body);
|
return json.decode(response.body);
|
||||||
@@ -114,10 +129,12 @@ class TeacherRepository {
|
|||||||
Future<TeacherProfileModel> getProfileStatus() async {
|
Future<TeacherProfileModel> getProfileStatus() async {
|
||||||
try {
|
try {
|
||||||
final headers = await _authHeaders();
|
final headers = await _authHeaders();
|
||||||
final response = await _client.get(
|
final response = await _client
|
||||||
|
.get(
|
||||||
Uri.parse('$baseUrl${AppConfig.profileStatusEndpoint}'),
|
Uri.parse('$baseUrl${AppConfig.profileStatusEndpoint}'),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
).timeout(const Duration(seconds: 4));
|
)
|
||||||
|
.timeout(const Duration(seconds: 4));
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final decoded = json.decode(response.body);
|
final decoded = json.decode(response.body);
|
||||||
@@ -141,7 +158,8 @@ class TeacherRepository {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final headers = await _authHeaders();
|
final headers = await _authHeaders();
|
||||||
final response = await _client.post(
|
final response = await _client
|
||||||
|
.post(
|
||||||
Uri.parse('$baseUrl${AppConfig.profileSetupEndpoint}'),
|
Uri.parse('$baseUrl${AppConfig.profileSetupEndpoint}'),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: json.encode({
|
body: json.encode({
|
||||||
@@ -151,7 +169,8 @@ class TeacherRepository {
|
|||||||
'grades_taught': gradesTaught,
|
'grades_taught': gradesTaught,
|
||||||
'bio': bio,
|
'bio': bio,
|
||||||
}),
|
}),
|
||||||
).timeout(const Duration(seconds: 5));
|
)
|
||||||
|
.timeout(const Duration(seconds: 5));
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final decoded = json.decode(response.body);
|
final decoded = json.decode(response.body);
|
||||||
@@ -181,10 +200,12 @@ class TeacherRepository {
|
|||||||
Future<List<ChatConversationModel>> getConversations() async {
|
Future<List<ChatConversationModel>> getConversations() async {
|
||||||
try {
|
try {
|
||||||
final headers = await _authHeaders();
|
final headers = await _authHeaders();
|
||||||
final response = await _client.get(
|
final response = await _client
|
||||||
|
.get(
|
||||||
Uri.parse('$baseUrl${AppConfig.chatConversationsEndpoint}'),
|
Uri.parse('$baseUrl${AppConfig.chatConversationsEndpoint}'),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
).timeout(const Duration(seconds: 4));
|
)
|
||||||
|
.timeout(const Duration(seconds: 4));
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final decoded = json.decode(response.body);
|
final decoded = json.decode(response.body);
|
||||||
@@ -192,7 +213,7 @@ class TeacherRepository {
|
|||||||
final list = (decoded['data'] as List)
|
final list = (decoded['data'] as List)
|
||||||
.map((c) => ChatConversationModel.fromJson(c))
|
.map((c) => ChatConversationModel.fromJson(c))
|
||||||
.toList();
|
.toList();
|
||||||
if (list.isNotEmpty) return list;
|
return list;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
@@ -203,14 +224,18 @@ class TeacherRepository {
|
|||||||
Future<List<ChatMessageModel>> getMessages(int otherUserId) async {
|
Future<List<ChatMessageModel>> getMessages(int otherUserId) async {
|
||||||
try {
|
try {
|
||||||
final headers = await _authHeaders();
|
final headers = await _authHeaders();
|
||||||
final response = await _client.get(
|
final response = await _client
|
||||||
Uri.parse('$baseUrl${AppConfig.chatMessagesEndpoint}?other_user_id=$otherUserId'),
|
.get(
|
||||||
|
Uri.parse(
|
||||||
|
'$baseUrl${AppConfig.chatMessagesEndpoint}?other_user_id=$otherUserId'),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
).timeout(const Duration(seconds: 4));
|
)
|
||||||
|
.timeout(const Duration(seconds: 4));
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final decoded = json.decode(response.body);
|
final decoded = json.decode(response.body);
|
||||||
if (decoded['status'] == 'success' && decoded['data']?['messages'] is List) {
|
if (decoded['status'] == 'success' &&
|
||||||
|
decoded['data']?['messages'] is List) {
|
||||||
return (decoded['data']['messages'] as List)
|
return (decoded['data']['messages'] as List)
|
||||||
.map((m) => ChatMessageModel.fromJson(m))
|
.map((m) => ChatMessageModel.fromJson(m))
|
||||||
.toList();
|
.toList();
|
||||||
@@ -229,7 +254,8 @@ class TeacherRepository {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final headers = await _authHeaders();
|
final headers = await _authHeaders();
|
||||||
final response = await _client.post(
|
final response = await _client
|
||||||
|
.post(
|
||||||
Uri.parse('$baseUrl${AppConfig.chatMessagesEndpoint}'),
|
Uri.parse('$baseUrl${AppConfig.chatMessagesEndpoint}'),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: json.encode({
|
body: json.encode({
|
||||||
@@ -238,7 +264,8 @@ class TeacherRepository {
|
|||||||
'message_type': messageType,
|
'message_type': messageType,
|
||||||
if (mediaUrl != null) 'media_url': mediaUrl,
|
if (mediaUrl != null) 'media_url': mediaUrl,
|
||||||
}),
|
}),
|
||||||
).timeout(const Duration(seconds: 5));
|
)
|
||||||
|
.timeout(const Duration(seconds: 5));
|
||||||
|
|
||||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
final decoded = json.decode(response.body);
|
final decoded = json.decode(response.body);
|
||||||
@@ -265,7 +292,8 @@ class TeacherRepository {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final headers = await _authHeaders();
|
final headers = await _authHeaders();
|
||||||
final response = await _client.post(
|
final response = await _client
|
||||||
|
.post(
|
||||||
Uri.parse('$baseUrl${AppConfig.broadcastEndpoint}'),
|
Uri.parse('$baseUrl${AppConfig.broadcastEndpoint}'),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: json.encode({
|
body: json.encode({
|
||||||
@@ -276,14 +304,16 @@ class TeacherRepository {
|
|||||||
'grade_level': gradeLevel,
|
'grade_level': gradeLevel,
|
||||||
'course_id': courseId,
|
'course_id': courseId,
|
||||||
}),
|
}),
|
||||||
).timeout(const Duration(seconds: 5));
|
)
|
||||||
|
.timeout(const Duration(seconds: 5));
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final decoded = json.decode(response.body);
|
final decoded = json.decode(response.body);
|
||||||
return decoded['status'] == 'success';
|
return decoded['status'] == 'success';
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
AppLogger.error('broadcastAnnouncement notice', error: e, tag: 'TEACHER_REPO');
|
AppLogger.error('broadcastAnnouncement notice',
|
||||||
|
error: e, tag: 'TEACHER_REPO');
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -299,7 +329,8 @@ class TeacherRepository {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final headers = await _authHeaders();
|
final headers = await _authHeaders();
|
||||||
final response = await _client.post(
|
final response = await _client
|
||||||
|
.post(
|
||||||
Uri.parse('$baseUrl${AppConfig.auditVideoEndpoint}'),
|
Uri.parse('$baseUrl${AppConfig.auditVideoEndpoint}'),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: json.encode({
|
body: json.encode({
|
||||||
@@ -307,7 +338,8 @@ class TeacherRepository {
|
|||||||
'duration_minutes': durationMinutes,
|
'duration_minutes': durationMinutes,
|
||||||
'subject': subject,
|
'subject': subject,
|
||||||
}),
|
}),
|
||||||
).timeout(const Duration(seconds: 4));
|
)
|
||||||
|
.timeout(const Duration(seconds: 4));
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final decoded = json.decode(response.body);
|
final decoded = json.decode(response.body);
|
||||||
@@ -325,6 +357,7 @@ class TeacherRepository {
|
|||||||
required double durationMinutes,
|
required double durationMinutes,
|
||||||
required String gradeLevel,
|
required String gradeLevel,
|
||||||
required String subject,
|
required String subject,
|
||||||
|
required String curriculumKey,
|
||||||
String? fileName,
|
String? fileName,
|
||||||
double? fileSizeMb,
|
double? fileSizeMb,
|
||||||
String? filePath,
|
String? filePath,
|
||||||
@@ -343,6 +376,7 @@ class TeacherRepository {
|
|||||||
'duration_minutes': durationMinutes.toString(),
|
'duration_minutes': durationMinutes.toString(),
|
||||||
'grade_level': gradeLevel,
|
'grade_level': gradeLevel,
|
||||||
'subject': subject,
|
'subject': subject,
|
||||||
|
'curriculum_key': curriculumKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
final resolvedName = fileName ?? 'lesson.mp4';
|
final resolvedName = fileName ?? 'lesson.mp4';
|
||||||
@@ -362,24 +396,49 @@ class TeacherRepository {
|
|||||||
throw StateError('لم يتم اختيار ملف فيديو صالح للرفع.');
|
throw StateError('لم يتم اختيار ملف فيديو صالح للرفع.');
|
||||||
}
|
}
|
||||||
|
|
||||||
final streamed = await _client.send(request).timeout(const Duration(minutes: 10));
|
final streamed =
|
||||||
|
await _client.send(request).timeout(const Duration(minutes: 10));
|
||||||
final response = await http.Response.fromStream(streamed);
|
final response = await http.Response.fromStream(streamed);
|
||||||
|
|
||||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
final decoded = json.decode(response.body) as Map<String, dynamic>;
|
||||||
return json.decode(response.body);
|
if (response.statusCode == 200 || response.statusCode == 201)
|
||||||
|
return decoded;
|
||||||
|
throw StateError(
|
||||||
|
decoded['message']?.toString() ?? 'رفض الخادم رفع الحصة.');
|
||||||
|
} on StateError {
|
||||||
|
rethrow;
|
||||||
|
} catch (error) {
|
||||||
|
throw StateError('تعذر رفع الحصة: $error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
|
||||||
|
|
||||||
throw StateError('لم يتم رفع الحصة أو نشرها على الخادم.');
|
Future<Map<String, dynamic>> getCurriculumTree() async {
|
||||||
|
final headers = await _authHeaders();
|
||||||
|
final response = await _client
|
||||||
|
.get(
|
||||||
|
Uri.parse('$baseUrl${AppConfig.curriculumTreeEndpoint}'),
|
||||||
|
headers: headers,
|
||||||
|
)
|
||||||
|
.timeout(const Duration(seconds: 30));
|
||||||
|
final decoded = json.decode(response.body) as Map<String, dynamic>;
|
||||||
|
if (response.statusCode == 200 &&
|
||||||
|
decoded['status'] == 'success' &&
|
||||||
|
decoded['data'] is Map) {
|
||||||
|
return Map<String, dynamic>.from(decoded['data'] as Map);
|
||||||
|
}
|
||||||
|
throw StateError(
|
||||||
|
decoded['message']?.toString() ?? 'تعذر تحميل شجرة المنهاج.');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getMonetizationDashboard() async {
|
Future<Map<String, dynamic>> getMonetizationDashboard() async {
|
||||||
try {
|
try {
|
||||||
final headers = await _authHeaders();
|
final headers = await _authHeaders();
|
||||||
final response = await _client.get(
|
final response = await _client
|
||||||
|
.get(
|
||||||
Uri.parse('$baseUrl${AppConfig.monetizationEndpoint}'),
|
Uri.parse('$baseUrl${AppConfig.monetizationEndpoint}'),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
).timeout(const Duration(seconds: 4));
|
)
|
||||||
|
.timeout(const Duration(seconds: 4));
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final decoded = json.decode(response.body);
|
final decoded = json.decode(response.body);
|
||||||
@@ -398,21 +457,24 @@ class TeacherRepository {
|
|||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final headers = await _authHeaders();
|
final headers = await _authHeaders();
|
||||||
final response = await _client.post(
|
final response = await _client
|
||||||
|
.post(
|
||||||
Uri.parse('$baseUrl${AppConfig.cliqPayoutEndpoint}'),
|
Uri.parse('$baseUrl${AppConfig.cliqPayoutEndpoint}'),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
body: json.encode({
|
body: json.encode({
|
||||||
'cliq_alias': cliqAlias,
|
'cliq_alias': cliqAlias,
|
||||||
'amount_jod': amountJod,
|
'amount_jod': amountJod,
|
||||||
}),
|
}),
|
||||||
).timeout(const Duration(seconds: 5));
|
)
|
||||||
|
.timeout(const Duration(seconds: 5));
|
||||||
|
|
||||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
final decoded = json.decode(response.body);
|
final decoded = json.decode(response.body);
|
||||||
return TeacherPayoutRequestModel(
|
return TeacherPayoutRequestModel(
|
||||||
cliqAlias: cliqAlias,
|
cliqAlias: cliqAlias,
|
||||||
amountJod: amountJod,
|
amountJod: amountJod,
|
||||||
requestedAt: 'اليوم، ${DateTime.now().hour}:${DateTime.now().minute.toString().padLeft(2, '0')}',
|
requestedAt:
|
||||||
|
'اليوم، ${DateTime.now().hour}:${DateTime.now().minute.toString().padLeft(2, '0')}',
|
||||||
status: decoded['queue_status'] ?? decoded['status'] ?? 'queued',
|
status: decoded['queue_status'] ?? decoded['status'] ?? 'queued',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -424,22 +486,25 @@ class TeacherRepository {
|
|||||||
Future<List<TeacherHomeworkModel>> getHomeworks() async {
|
Future<List<TeacherHomeworkModel>> getHomeworks() async {
|
||||||
try {
|
try {
|
||||||
final headers = await _authHeaders();
|
final headers = await _authHeaders();
|
||||||
final response = await _client.get(
|
final response = await _client
|
||||||
|
.get(
|
||||||
Uri.parse('$baseUrl${AppConfig.qnaEndpoint}'),
|
Uri.parse('$baseUrl${AppConfig.qnaEndpoint}'),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
).timeout(const Duration(seconds: 4));
|
)
|
||||||
|
.timeout(const Duration(seconds: 4));
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final decoded = json.decode(response.body);
|
final decoded = json.decode(response.body);
|
||||||
if (decoded['status'] == 'success' && decoded['data']?['homeworks'] is List) {
|
if (decoded['status'] == 'success' &&
|
||||||
|
decoded['data']?['homeworks'] is List) {
|
||||||
return (decoded['data']['homeworks'] as List)
|
return (decoded['data']['homeworks'] as List)
|
||||||
.map((h) => TeacherHomeworkModel(
|
.map((h) => TeacherHomeworkModel(
|
||||||
id: h['id'].toString(),
|
id: h['id'].toString(),
|
||||||
title: h['title']?.toString() ?? '',
|
title: h['title']?.toString() ?? '',
|
||||||
className: h['class_name']?.toString() ?? '',
|
className: h['class_name']?.toString() ?? '',
|
||||||
submitted: h['submitted']?.toString() ?? '0 من 38',
|
submitted: h['submitted']?.toString() ?? '',
|
||||||
dueDate: h['due_date']?.toString() ?? '',
|
dueDate: h['due_date']?.toString() ?? '',
|
||||||
averageScore: h['average_score']?.toString() ?? '90%',
|
averageScore: h['average_score']?.toString() ?? '',
|
||||||
status: h['status']?.toString() ?? 'active',
|
status: h['status']?.toString() ?? 'active',
|
||||||
))
|
))
|
||||||
.toList();
|
.toList();
|
||||||
@@ -453,14 +518,17 @@ class TeacherRepository {
|
|||||||
Future<List<StudentDoubtModel>> getStudentDoubts() async {
|
Future<List<StudentDoubtModel>> getStudentDoubts() async {
|
||||||
try {
|
try {
|
||||||
final headers = await _authHeaders();
|
final headers = await _authHeaders();
|
||||||
final response = await _client.get(
|
final response = await _client
|
||||||
|
.get(
|
||||||
Uri.parse('$baseUrl${AppConfig.qnaEndpoint}'),
|
Uri.parse('$baseUrl${AppConfig.qnaEndpoint}'),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
).timeout(const Duration(seconds: 4));
|
)
|
||||||
|
.timeout(const Duration(seconds: 4));
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final decoded = json.decode(response.body);
|
final decoded = json.decode(response.body);
|
||||||
if (decoded['status'] == 'success' && decoded['data']?['doubts'] is List) {
|
if (decoded['status'] == 'success' &&
|
||||||
|
decoded['data']?['doubts'] is List) {
|
||||||
return (decoded['data']['doubts'] as List)
|
return (decoded['data']['doubts'] as List)
|
||||||
.map((d) => StudentDoubtModel(
|
.map((d) => StudentDoubtModel(
|
||||||
id: d['id'].toString(),
|
id: d['id'].toString(),
|
||||||
@@ -483,10 +551,12 @@ class TeacherRepository {
|
|||||||
Future<List<TeacherCourseModel>> getPublishedCourses() async {
|
Future<List<TeacherCourseModel>> getPublishedCourses() async {
|
||||||
try {
|
try {
|
||||||
final headers = await _authHeaders();
|
final headers = await _authHeaders();
|
||||||
final response = await _client.get(
|
final response = await _client
|
||||||
|
.get(
|
||||||
Uri.parse('$baseUrl${AppConfig.coursesEndpoint}'),
|
Uri.parse('$baseUrl${AppConfig.coursesEndpoint}'),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
).timeout(const Duration(seconds: 4));
|
)
|
||||||
|
.timeout(const Duration(seconds: 4));
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final decoded = json.decode(response.body);
|
final decoded = json.decode(response.body);
|
||||||
@@ -494,13 +564,15 @@ class TeacherRepository {
|
|||||||
return (decoded['data'] as List).map((c) {
|
return (decoded['data'] as List).map((c) {
|
||||||
return TeacherCourseModel(
|
return TeacherCourseModel(
|
||||||
id: c['id'].toString(),
|
id: c['id'].toString(),
|
||||||
title: c['title'] ?? 'الفيزياء — الصف العاشر',
|
title: c['title']?.toString() ?? '',
|
||||||
grade: 'الصف العاشر الأساسي',
|
grade: c['grade_level']?.toString() ?? '',
|
||||||
totalLessons: int.tryParse(c['lessons_total']?.toString() ?? '0') ?? 0,
|
totalLessons:
|
||||||
militaryStudents: 83,
|
int.tryParse(c['lessons_total']?.toString() ?? '0') ?? 0,
|
||||||
externalSubscribers: 0,
|
militaryStudents: (c['military_students'] as num?)?.toInt() ?? 0,
|
||||||
priceJod: double.tryParse(c['price_jod']?.toString() ?? '20.0') ?? 20.0,
|
externalSubscribers: (c['external_subscribers'] as num?)?.toInt() ?? 0,
|
||||||
netRevenueJod: 0.0,
|
priceJod:
|
||||||
|
double.tryParse(c['price_jod']?.toString() ?? '') ?? 0,
|
||||||
|
netRevenueJod: double.tryParse(c['net_revenue_jod']?.toString() ?? '') ?? 0,
|
||||||
);
|
);
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
@@ -517,10 +589,12 @@ class TeacherRepository {
|
|||||||
Future<Map<String, dynamic>> getMyReputation() async {
|
Future<Map<String, dynamic>> getMyReputation() async {
|
||||||
try {
|
try {
|
||||||
final headers = await _authHeaders();
|
final headers = await _authHeaders();
|
||||||
final response = await _client.get(
|
final response = await _client
|
||||||
|
.get(
|
||||||
Uri.parse('$baseUrl${AppConfig.reputationEndpoint}'),
|
Uri.parse('$baseUrl${AppConfig.reputationEndpoint}'),
|
||||||
headers: headers,
|
headers: headers,
|
||||||
).timeout(const Duration(seconds: 4));
|
)
|
||||||
|
.timeout(const Duration(seconds: 4));
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final decoded = json.decode(response.body);
|
final decoded = json.decode(response.body);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import '../../data/repositories/teacher_repository.dart';
|
|||||||
abstract class TeacherAuthState {}
|
abstract class TeacherAuthState {}
|
||||||
|
|
||||||
class TeacherAuthInitial extends TeacherAuthState {}
|
class TeacherAuthInitial extends TeacherAuthState {}
|
||||||
|
|
||||||
class TeacherAuthLoading extends TeacherAuthState {}
|
class TeacherAuthLoading extends TeacherAuthState {}
|
||||||
|
|
||||||
class TeacherAuthOtpSent extends TeacherAuthState {
|
class TeacherAuthOtpSent extends TeacherAuthState {
|
||||||
@@ -42,13 +43,16 @@ class TeacherAuthCubit extends Cubit<TeacherAuthState> {
|
|||||||
try {
|
try {
|
||||||
final token = await _storage.getToken();
|
final token = await _storage.getToken();
|
||||||
if (token == null || token.isEmpty) {
|
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());
|
emit(TeacherUnauthenticated());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final profile = await repository.getProfileStatus();
|
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));
|
emit(TeacherAuthenticated(profile: profile));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
AppLogger.error('Session check failed', error: e, tag: 'TEACHER_AUTH');
|
AppLogger.error('Session check failed', error: e, tag: 'TEACHER_AUTH');
|
||||||
@@ -67,7 +71,8 @@ class TeacherAuthCubit extends Cubit<TeacherAuthState> {
|
|||||||
if (res['status'] == 'success') {
|
if (res['status'] == 'success') {
|
||||||
emit(TeacherAuthOtpSent(phoneNumber: phoneNumber));
|
emit(TeacherAuthOtpSent(phoneNumber: phoneNumber));
|
||||||
} else {
|
} else {
|
||||||
emit(TeacherAuthError(res['message']?.toString() ?? 'فشل إرسال رمز التحقق'));
|
emit(TeacherAuthError(
|
||||||
|
res['message']?.toString() ?? 'فشل إرسال رمز التحقق'));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(TeacherAuthError(e.toString()));
|
emit(TeacherAuthError(e.toString()));
|
||||||
@@ -84,9 +89,13 @@ class TeacherAuthCubit extends Cubit<TeacherAuthState> {
|
|||||||
}) async {
|
}) async {
|
||||||
emit(TeacherAuthLoading());
|
emit(TeacherAuthLoading());
|
||||||
try {
|
try {
|
||||||
final res = await repository.verifyOtp(phoneNumber, otp, fullName: fullName);
|
final res =
|
||||||
|
await repository.verifyOtp(phoneNumber, otp, fullName: fullName);
|
||||||
if (res['status'] == 'success') {
|
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 {
|
try {
|
||||||
await repository.setupProfile(
|
await repository.setupProfile(
|
||||||
fullName: fullName!.trim(),
|
fullName: fullName!.trim(),
|
||||||
@@ -100,7 +109,8 @@ class TeacherAuthCubit extends Cubit<TeacherAuthState> {
|
|||||||
final profile = await repository.getProfileStatus();
|
final profile = await repository.getProfileStatus();
|
||||||
emit(TeacherAuthenticated(profile: profile));
|
emit(TeacherAuthenticated(profile: profile));
|
||||||
} else {
|
} else {
|
||||||
emit(TeacherAuthError(res['message']?.toString() ?? 'رمز التحقق غير صحيح'));
|
emit(TeacherAuthError(
|
||||||
|
res['message']?.toString() ?? 'رمز التحقق غير صحيح'));
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(TeacherAuthError(e.toString()));
|
emit(TeacherAuthError(e.toString()));
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ class TeacherMonetizationState {
|
|||||||
bool? isLoading,
|
bool? isLoading,
|
||||||
}) {
|
}) {
|
||||||
return TeacherMonetizationState(
|
return TeacherMonetizationState(
|
||||||
institutionalStudents: institutionalStudents ?? this.institutionalStudents,
|
institutionalStudents:
|
||||||
|
institutionalStudents ?? this.institutionalStudents,
|
||||||
studentSubscribers: studentSubscribers ?? this.studentSubscribers,
|
studentSubscribers: studentSubscribers ?? this.studentSubscribers,
|
||||||
pricePerCourse: pricePerCourse ?? this.pricePerCourse,
|
pricePerCourse: pricePerCourse ?? this.pricePerCourse,
|
||||||
grossRevenue: grossRevenue ?? this.grossRevenue,
|
grossRevenue: grossRevenue ?? this.grossRevenue,
|
||||||
@@ -86,7 +87,7 @@ class TeacherMonetizationCubit extends Cubit<TeacherMonetizationState> {
|
|||||||
|
|
||||||
TeacherMonetizationCubit({required this.repository})
|
TeacherMonetizationCubit({required this.repository})
|
||||||
: super(const TeacherMonetizationState(
|
: super(const TeacherMonetizationState(
|
||||||
institutionalStudents: 83,
|
institutionalStudents: 0,
|
||||||
studentSubscribers: 0,
|
studentSubscribers: 0,
|
||||||
pricePerCourse: 20.0,
|
pricePerCourse: 20.0,
|
||||||
grossRevenue: 0.0,
|
grossRevenue: 0.0,
|
||||||
@@ -95,7 +96,7 @@ class TeacherMonetizationCubit extends Cubit<TeacherMonetizationState> {
|
|||||||
platformShare: 0.0,
|
platformShare: 0.0,
|
||||||
availableBalance: 0.0,
|
availableBalance: 0.0,
|
||||||
totalWithdrawn: 0.0,
|
totalWithdrawn: 0.0,
|
||||||
cliqAlias: '0798583052@CLIQ',
|
cliqAlias: '',
|
||||||
courses: [],
|
courses: [],
|
||||||
payoutRequests: [],
|
payoutRequests: [],
|
||||||
isSubmittingPayout: false,
|
isSubmittingPayout: false,
|
||||||
@@ -108,20 +109,26 @@ class TeacherMonetizationCubit extends Cubit<TeacherMonetizationState> {
|
|||||||
final data = await repository.getMonetizationDashboard();
|
final data = await repository.getMonetizationDashboard();
|
||||||
final courses = await repository.getPublishedCourses();
|
final courses = await repository.getPublishedCourses();
|
||||||
|
|
||||||
final audience = data['audience_breakdown'] as Map<String, dynamic>? ?? {};
|
final audience =
|
||||||
final inst = audience['institutional_students'] as Map<String, dynamic>? ?? {};
|
data['audience_breakdown'] as Map<String, dynamic>? ?? {};
|
||||||
final mkt = audience['marketplace_students'] 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 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 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 tShare = gross * 0.55;
|
||||||
final dShare = gross * 0.15;
|
final dShare = gross * 0.15;
|
||||||
final pShare = gross * 0.30;
|
final pShare = gross * 0.30;
|
||||||
final avail = (wallet['available_balance_jod'] as num?)?.toDouble() ?? tShare;
|
final avail =
|
||||||
final withdrawn = (wallet['total_withdrawn_jod'] as num?)?.toDouble() ?? 0.0;
|
(wallet['available_balance_jod'] as num?)?.toDouble() ?? tShare;
|
||||||
final alias = wallet['cliq_payout_alias']?.toString() ?? '0798583052@CLIQ';
|
final withdrawn =
|
||||||
|
(wallet['total_withdrawn_jod'] as num?)?.toDouble() ?? 0.0;
|
||||||
|
final alias = wallet['cliq_payout_alias']?.toString() ?? '';
|
||||||
|
|
||||||
List<TeacherPayoutRequestModel> payouts = [];
|
List<TeacherPayoutRequestModel> payouts = [];
|
||||||
if (wallet['recent_payouts'] is List) {
|
if (wallet['recent_payouts'] is List) {
|
||||||
@@ -176,8 +183,11 @@ class TeacherMonetizationCubit extends Cubit<TeacherMonetizationState> {
|
|||||||
cliqAlias: cliqAlias,
|
cliqAlias: cliqAlias,
|
||||||
amountJod: amountJod,
|
amountJod: amountJod,
|
||||||
);
|
);
|
||||||
final updatedList = List<TeacherPayoutRequestModel>.from(state.payoutRequests)..insert(0, req);
|
final updatedList =
|
||||||
final newAvail = (state.availableBalance - amountJod).clamp(0.0, double.infinity);
|
List<TeacherPayoutRequestModel>.from(state.payoutRequests)
|
||||||
|
..insert(0, req);
|
||||||
|
final newAvail =
|
||||||
|
(state.availableBalance - amountJod).clamp(0.0, double.infinity);
|
||||||
final newWithdrawn = state.totalWithdrawn + amountJod;
|
final newWithdrawn = state.totalWithdrawn + amountJod;
|
||||||
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
|
|||||||
@@ -58,16 +58,21 @@ class TeacherQnACubit extends Cubit<TeacherQnAState> {
|
|||||||
|
|
||||||
Future<void> loadQnA() async {
|
Future<void> loadQnA() async {
|
||||||
emit(state.copyWith(isLoading: true));
|
emit(state.copyWith(isLoading: true));
|
||||||
final hwList = await repository.getHomeworks();
|
try {
|
||||||
final doubtsList = await repository.getStudentDoubts();
|
final results = await Future.wait([
|
||||||
final conversationsList = await repository.getConversations();
|
repository.getHomeworks(),
|
||||||
|
repository.getStudentDoubts(),
|
||||||
|
repository.getConversations(),
|
||||||
|
]);
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
homeworks: hwList,
|
homeworks: results[0] as List<TeacherHomeworkModel>,
|
||||||
doubts: doubtsList,
|
doubts: results[1] as List<StudentDoubtModel>,
|
||||||
conversations: conversationsList,
|
conversations: results[2] as List<ChatConversationModel>,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
));
|
));
|
||||||
|
} catch (_) {
|
||||||
|
emit(state.copyWith(isLoading: false));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispatchNewHomework({
|
void dispatchNewHomework({
|
||||||
@@ -86,14 +91,19 @@ class TeacherQnACubit extends Cubit<TeacherQnAState> {
|
|||||||
final target = state.doubts[targetIndex];
|
final target = state.doubts[targetIndex];
|
||||||
final replyText = voiceText?.trim() ?? '';
|
final replyText = voiceText?.trim() ?? '';
|
||||||
if (replyText.isEmpty) {
|
if (replyText.isEmpty) {
|
||||||
throw StateError('لا يمكن تسجيل رد صوتي دون ملف صوت حقيقي؛ أرسل رداً نصياً حالياً.');
|
throw StateError(
|
||||||
|
'لا يمكن تسجيل رد صوتي دون ملف صوت حقيقي؛ أرسل رداً نصياً حالياً.');
|
||||||
}
|
}
|
||||||
await repository.sendMessage(
|
await repository.sendMessage(
|
||||||
receiverId: target.studentId,
|
receiverId: target.studentId,
|
||||||
message: replyText,
|
message: replyText,
|
||||||
messageType: 'text',
|
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));
|
emit(state.copyWith(doubts: updated));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,8 +116,8 @@ class TeacherQnACubit extends Cubit<TeacherQnAState> {
|
|||||||
final success = await repository.broadcastAnnouncement(
|
final success = await repository.broadcastAnnouncement(
|
||||||
title: title,
|
title: title,
|
||||||
message: explanationText,
|
message: explanationText,
|
||||||
messageType: 'voice',
|
messageType: 'text',
|
||||||
mediaUrl: 'https://saqel.intaleqapp.com/assets/audio/teacher_broadcast.mp3',
|
mediaUrl: null,
|
||||||
gradeLevel: gradeLevel,
|
gradeLevel: gradeLevel,
|
||||||
);
|
);
|
||||||
emit(state.copyWith(isBroadcasting: false));
|
emit(state.copyWith(isBroadcasting: false));
|
||||||
|
|||||||
@@ -17,6 +17,14 @@ class TeacherStudioState {
|
|||||||
final String lessonTitle;
|
final String lessonTitle;
|
||||||
final double durationMinutes;
|
final double durationMinutes;
|
||||||
final String gradeLevel;
|
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 String? selectedFileName;
|
||||||
final double? selectedFileSizeMb;
|
final double? selectedFileSizeMb;
|
||||||
final String? selectedFilePath;
|
final String? selectedFilePath;
|
||||||
@@ -30,7 +38,15 @@ class TeacherStudioState {
|
|||||||
const TeacherStudioState({
|
const TeacherStudioState({
|
||||||
required this.lessonTitle,
|
required this.lessonTitle,
|
||||||
required this.durationMinutes,
|
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.selectedFileName,
|
||||||
this.selectedFileSizeMb,
|
this.selectedFileSizeMb,
|
||||||
this.selectedFilePath,
|
this.selectedFilePath,
|
||||||
@@ -46,6 +62,15 @@ class TeacherStudioState {
|
|||||||
String? lessonTitle,
|
String? lessonTitle,
|
||||||
double? durationMinutes,
|
double? durationMinutes,
|
||||||
String? gradeLevel,
|
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,
|
String? selectedFileName,
|
||||||
double? selectedFileSizeMb,
|
double? selectedFileSizeMb,
|
||||||
String? selectedFilePath,
|
String? selectedFilePath,
|
||||||
@@ -60,6 +85,14 @@ class TeacherStudioState {
|
|||||||
lessonTitle: lessonTitle ?? this.lessonTitle,
|
lessonTitle: lessonTitle ?? this.lessonTitle,
|
||||||
durationMinutes: durationMinutes ?? this.durationMinutes,
|
durationMinutes: durationMinutes ?? this.durationMinutes,
|
||||||
gradeLevel: gradeLevel ?? this.gradeLevel,
|
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,
|
selectedFileName: selectedFileName ?? this.selectedFileName,
|
||||||
selectedFileSizeMb: selectedFileSizeMb ?? this.selectedFileSizeMb,
|
selectedFileSizeMb: selectedFileSizeMb ?? this.selectedFileSizeMb,
|
||||||
selectedFilePath: selectedFilePath ?? this.selectedFilePath,
|
selectedFilePath: selectedFilePath ?? this.selectedFilePath,
|
||||||
@@ -78,8 +111,8 @@ class TeacherStudioCubit extends Cubit<TeacherStudioState> {
|
|||||||
|
|
||||||
TeacherStudioCubit({required this.repository})
|
TeacherStudioCubit({required this.repository})
|
||||||
: super(const TeacherStudioState(
|
: super(const TeacherStudioState(
|
||||||
lessonTitle: 'شرح قاعدة لنتز والحث الكهرومغناطيسي — فيزياء 2008',
|
lessonTitle: '',
|
||||||
durationMinutes: 13.0,
|
durationMinutes: 20.0,
|
||||||
isAuditing: false,
|
isAuditing: false,
|
||||||
auditResult: null,
|
auditResult: null,
|
||||||
));
|
));
|
||||||
@@ -96,6 +129,137 @@ class TeacherStudioCubit extends Cubit<TeacherStudioState> {
|
|||||||
emit(state.copyWith(gradeLevel: grade));
|
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(
|
void selectVideoFile(
|
||||||
String fileName,
|
String fileName,
|
||||||
double sizeMb,
|
double sizeMb,
|
||||||
@@ -115,20 +279,18 @@ class TeacherStudioCubit extends Cubit<TeacherStudioState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> runQualityGate() async {
|
Future<void> runQualityGate() async {
|
||||||
emit(state.copyWith(isAuditing: true));
|
if (state.selectedFileName == null ||
|
||||||
try {
|
(state.selectedFilePath == null && state.selectedFileBytes == null)) {
|
||||||
final result = await repository.auditStudioVideo(
|
|
||||||
title: state.lessonTitle,
|
|
||||||
durationMinutes: state.durationMinutes,
|
|
||||||
subject: 'الفيزياء',
|
|
||||||
);
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
isAuditing: false,
|
errorMessage: 'اختر ملف فيديو فعلياً قبل بدء فحص الجودة.'));
|
||||||
auditResult: result,
|
return;
|
||||||
));
|
|
||||||
} catch (_) {
|
|
||||||
emit(state.copyWith(isAuditing: false));
|
|
||||||
}
|
}
|
||||||
|
if (curriculumKey.isEmpty || state.lessonTitle.trim().isEmpty) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
errorMessage: 'اختر الصف والمبحث والفصل والوحدة والدرس أولاً.'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await uploadAndPublishLesson();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> uploadAndPublishLesson() async {
|
Future<bool> uploadAndPublishLesson() async {
|
||||||
@@ -136,28 +298,45 @@ class TeacherStudioCubit extends Cubit<TeacherStudioState> {
|
|||||||
(state.selectedFilePath == null && state.selectedFileBytes == null)) {
|
(state.selectedFilePath == null && state.selectedFileBytes == null)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
emit(state.copyWith(isUploading: true));
|
emit(state.copyWith(isUploading: true, isAuditing: true, clearError: true));
|
||||||
try {
|
try {
|
||||||
final res = await repository.uploadLesson(
|
final res = await repository.uploadLesson(
|
||||||
title: state.lessonTitle,
|
title: state.lessonTitle,
|
||||||
durationMinutes: state.durationMinutes,
|
durationMinutes: state.durationMinutes,
|
||||||
gradeLevel: state.gradeLevel,
|
gradeLevel: state.gradeLevel,
|
||||||
subject: 'الفيزياء والعلوم التطبيقية',
|
subject: selectedSubjectName,
|
||||||
fileName: state.selectedFileName ?? 'lentz_law_physics_2008.mp4',
|
curriculumKey: curriculumKey,
|
||||||
fileSizeMb: state.selectedFileSizeMb ?? 48.2,
|
fileName: state.selectedFileName!,
|
||||||
|
fileSizeMb: state.selectedFileSizeMb,
|
||||||
filePath: state.selectedFilePath,
|
filePath: state.selectedFilePath,
|
||||||
fileBytes: state.selectedFileBytes,
|
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(
|
emit(state.copyWith(
|
||||||
|
isAuditing: false,
|
||||||
isUploading: false,
|
isUploading: false,
|
||||||
isUploaded: true,
|
isUploaded: true,
|
||||||
|
auditResult: result,
|
||||||
uploadSuccessMessage: msg,
|
uploadSuccessMessage: msg,
|
||||||
));
|
));
|
||||||
return true;
|
return true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(state.copyWith(isUploading: false));
|
emit(state.copyWith(
|
||||||
|
isAuditing: false,
|
||||||
|
isUploading: false,
|
||||||
|
errorMessage: e.toString().replaceFirst('Bad state: ', '')));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,8 @@ class SaqelTeacherApp extends StatelessWidget {
|
|||||||
return MultiBlocProvider(
|
return MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider<TeacherAuthCubit>(
|
BlocProvider<TeacherAuthCubit>(
|
||||||
create: (_) => TeacherAuthCubit(repository: repository)..checkSession(),
|
create: (_) =>
|
||||||
|
TeacherAuthCubit(repository: repository)..checkSession(),
|
||||||
),
|
),
|
||||||
BlocProvider<TeacherStudioCubit>(
|
BlocProvider<TeacherStudioCubit>(
|
||||||
create: (_) => TeacherStudioCubit(repository: repository),
|
create: (_) => TeacherStudioCubit(repository: repository),
|
||||||
@@ -73,7 +74,8 @@ class TeacherAuthGate extends StatelessWidget {
|
|||||||
return const Scaffold(
|
return const Scaffold(
|
||||||
backgroundColor: TeacherTheme.backgroundDark,
|
backgroundColor: TeacherTheme.backgroundDark,
|
||||||
body: Center(
|
body: Center(
|
||||||
child: CircularProgressIndicator(color: TeacherTheme.emeraldPrimary),
|
child:
|
||||||
|
CircularProgressIndicator(color: TeacherTheme.emeraldPrimary),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,7 +63,8 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
void _onSendOtp() {
|
void _onSendOtp() {
|
||||||
final phone = _phoneController.text.trim();
|
final phone = _phoneController.text.trim();
|
||||||
if (phone.isEmpty || phone.length < 9) {
|
if (phone.isEmpty || phone.length < 9) {
|
||||||
SaqelToast.showError(context, 'يرجى إدخال رقم هاتف أردني صحيح يبدأ بـ 07');
|
SaqelToast.showError(
|
||||||
|
context, 'يرجى إدخال رقم هاتف أردني صحيح يبدأ بـ 07');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
context.read<TeacherAuthCubit>().sendOtp(phone);
|
context.read<TeacherAuthCubit>().sendOtp(phone);
|
||||||
@@ -76,8 +77,12 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
SaqelToast.showError(context, 'يرجى إدخال رمز التحقق المكون من 6 أرقام');
|
SaqelToast.showError(context, 'يرجى إدخال رمز التحقق المكون من 6 أرقام');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_fullNameController.text.trim().isEmpty || _selectedSubject == null || _schoolController.text.trim().isEmpty || _selectedGrades.isEmpty) {
|
if (_fullNameController.text.trim().isEmpty ||
|
||||||
SaqelToast.showError(context, 'أكمل الاسم والمادة والمدرسة وصفًا واحدًا على الأقل قبل التحقق');
|
_selectedSubject == null ||
|
||||||
|
_schoolController.text.trim().isEmpty ||
|
||||||
|
_selectedGrades.isEmpty) {
|
||||||
|
SaqelToast.showError(context,
|
||||||
|
'أكمل الاسم والمادة والمدرسة وصفًا واحدًا على الأقل قبل التحقق');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
context.read<TeacherAuthCubit>().verifyOtp(
|
context.read<TeacherAuthCubit>().verifyOtp(
|
||||||
@@ -105,7 +110,8 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
child: BlocConsumer<TeacherAuthCubit, TeacherAuthState>(
|
child: BlocConsumer<TeacherAuthCubit, TeacherAuthState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state is TeacherAuthError) {
|
if (state is TeacherAuthError) {
|
||||||
SaqelToast.showError(context, state.message, title: 'تنبيه المصادقة');
|
SaqelToast.showError(context, state.message,
|
||||||
|
title: 'تنبيه المصادقة');
|
||||||
} else if (state is TeacherAuthOtpSent) {
|
} else if (state is TeacherAuthOtpSent) {
|
||||||
setState(() => _isEnteringOtp = true);
|
setState(() => _isEnteringOtp = true);
|
||||||
SaqelToast.showWhatsApp(
|
SaqelToast.showWhatsApp(
|
||||||
@@ -117,14 +123,16 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final isLoading = state is TeacherAuthLoading;
|
final isLoading = state is TeacherAuthLoading;
|
||||||
final isOtpStep = _isEnteringOtp || state is TeacherAuthOtpSent;
|
final isOtpStep =
|
||||||
|
_isEnteringOtp || state is TeacherAuthOtpSent;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(28),
|
padding: const EdgeInsets.all(28),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: TeacherTheme.surfaceDark,
|
color: TeacherTheme.surfaceDark,
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
border: Border.all(color: TeacherTheme.surfaceBorder, width: 1.2),
|
border: Border.all(
|
||||||
|
color: TeacherTheme.surfaceBorder, width: 1.2),
|
||||||
boxShadow: const [
|
boxShadow: const [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Color(0x3310B981),
|
color: Color(0x3310B981),
|
||||||
@@ -143,7 +151,10 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
height: 68,
|
height: 68,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: const LinearGradient(
|
gradient: const LinearGradient(
|
||||||
colors: [TeacherTheme.emeraldPrimary, TeacherTheme.emeraldDark],
|
colors: [
|
||||||
|
TeacherTheme.emeraldPrimary,
|
||||||
|
TeacherTheme.emeraldDark
|
||||||
|
],
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
),
|
),
|
||||||
@@ -194,11 +205,14 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
// Backend Live Gateway Badge
|
// Backend Live Gateway Badge
|
||||||
Center(
|
Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF0C241D),
|
color: const Color(0xFF0C241D),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
border: Border.all(color: TeacherTheme.emeraldPrimary.withOpacity(0.4)),
|
border: Border.all(
|
||||||
|
color: TeacherTheme.emeraldPrimary
|
||||||
|
.withOpacity(0.4)),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -213,7 +227,9 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
AppConfig.baseUrl.replaceAll('https://', '').replaceAll('http://', ''),
|
AppConfig.baseUrl
|
||||||
|
.replaceAll('https://', '')
|
||||||
|
.replaceAll('http://', ''),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: TeacherTheme.emeraldLight,
|
color: TeacherTheme.emeraldLight,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
@@ -230,7 +246,10 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
// Step 1: Teacher Info & Phone
|
// Step 1: Teacher Info & Phone
|
||||||
const Text(
|
const Text(
|
||||||
'اسم المعلم المعتمد:',
|
'اسم المعلم المعتمد:',
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12.5),
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12.5),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
@@ -242,30 +261,41 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
|
|
||||||
const Text(
|
const Text(
|
||||||
'المادة الأكاديمية التخصصية:',
|
'المادة الأكاديمية التخصصية:',
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12.5),
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12.5),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: TeacherTheme.surfaceCard,
|
color: TeacherTheme.surfaceCard,
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
border: Border.all(color: TeacherTheme.surfaceBorder),
|
border: Border.all(
|
||||||
|
color: TeacherTheme.surfaceBorder),
|
||||||
),
|
),
|
||||||
child: DropdownButtonHideUnderline(
|
child: DropdownButtonHideUnderline(
|
||||||
child: DropdownButton<String>(
|
child: DropdownButton<String>(
|
||||||
value: _selectedSubject,
|
value: _selectedSubject,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
dropdownColor: TeacherTheme.surfaceDark,
|
dropdownColor: TeacherTheme.surfaceDark,
|
||||||
icon: const Icon(CupertinoIcons.chevron_down, color: TeacherTheme.emeraldPrimary, size: 18),
|
icon: const Icon(CupertinoIcons.chevron_down,
|
||||||
|
color: TeacherTheme.emeraldPrimary,
|
||||||
|
size: 18),
|
||||||
items: _availableSubjects.map((s) {
|
items: _availableSubjects.map((s) {
|
||||||
return DropdownMenuItem(
|
return DropdownMenuItem(
|
||||||
value: s,
|
value: s,
|
||||||
child: Text(s, style: const TextStyle(color: Colors.white, fontSize: 12.5)),
|
child: Text(s,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12.5)),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
if (val != null) setState(() => _selectedSubject = val);
|
if (val != null)
|
||||||
|
setState(() => _selectedSubject = val);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -274,7 +304,10 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
|
|
||||||
const Text(
|
const Text(
|
||||||
'المدرسة / المديرية التابع لها:',
|
'المدرسة / المديرية التابع لها:',
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12.5),
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12.5),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
@@ -286,21 +319,34 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
|
|
||||||
const Text(
|
const Text(
|
||||||
'الصفوف والشعب التي تدرسها:',
|
'الصفوف والشعب التي تدرسها:',
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12.5),
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12.5),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
runSpacing: 8,
|
runSpacing: 8,
|
||||||
children: _allGrades.map((grade) {
|
children: _allGrades.map((grade) {
|
||||||
final isSelected = _selectedGrades.contains(grade);
|
final isSelected =
|
||||||
|
_selectedGrades.contains(grade);
|
||||||
return FilterChip(
|
return FilterChip(
|
||||||
selected: isSelected,
|
selected: isSelected,
|
||||||
label: Text(grade, style: TextStyle(color: isSelected ? Colors.black : Colors.white, fontSize: 11.5, fontWeight: FontWeight.bold)),
|
label: Text(grade,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isSelected
|
||||||
|
? Colors.black
|
||||||
|
: Colors.white,
|
||||||
|
fontSize: 11.5,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
selectedColor: TeacherTheme.emeraldPrimary,
|
selectedColor: TeacherTheme.emeraldPrimary,
|
||||||
backgroundColor: TeacherTheme.surfaceCard,
|
backgroundColor: TeacherTheme.surfaceCard,
|
||||||
checkmarkColor: Colors.black,
|
checkmarkColor: Colors.black,
|
||||||
side: BorderSide(color: isSelected ? TeacherTheme.emeraldPrimary : TeacherTheme.surfaceBorder),
|
side: BorderSide(
|
||||||
|
color: isSelected
|
||||||
|
? TeacherTheme.emeraldPrimary
|
||||||
|
: TeacherTheme.surfaceBorder),
|
||||||
onSelected: (selected) {
|
onSelected: (selected) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
@@ -319,7 +365,10 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
|
|
||||||
const Text(
|
const Text(
|
||||||
'رقم الهاتف للدخول والتحقق (WhatsApp OTP):',
|
'رقم الهاتف للدخول والتحقق (WhatsApp OTP):',
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 12.5),
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 12.5),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
@@ -335,15 +384,23 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: TeacherTheme.emeraldPrimary,
|
backgroundColor: TeacherTheme.emeraldPrimary,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding:
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14)),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
child: isLoading
|
child: isLoading
|
||||||
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.black, strokeWidth: 2))
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.black, strokeWidth: 2))
|
||||||
: const Text(
|
: const Text(
|
||||||
'إرسال رمز التحقق عبر الواتساب 📲',
|
'إرسال رمز التحقق عبر الواتساب 📲',
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w900),
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w900),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
] else ...[
|
] else ...[
|
||||||
@@ -353,25 +410,34 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF0C241D),
|
color: const Color(0xFF0C241D),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(color: const Color(0xFF25D366).withOpacity(0.5)),
|
border: Border.all(
|
||||||
|
color: const Color(0xFF25D366)
|
||||||
|
.withOpacity(0.5)),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: const [
|
children: const [
|
||||||
Icon(CupertinoIcons.chat_bubble_2_fill, color: Color(0xFF25D366), size: 20),
|
Icon(CupertinoIcons.chat_bubble_2_fill,
|
||||||
|
color: Color(0xFF25D366), size: 20),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'تم إرسال الرمز عبر الواتساب بنجاح 📲',
|
'تم إرسال الرمز عبر الواتساب بنجاح 📲',
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13),
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
'أدخل رمز التحقق المرسل إلى: ${_phoneController.text}',
|
'أدخل رمز التحقق المرسل إلى: ${_phoneController.text}',
|
||||||
style: const TextStyle(color: TeacherTheme.emeraldLight, fontSize: 13, fontWeight: FontWeight.w700),
|
style: const TextStyle(
|
||||||
|
color: TeacherTheme.emeraldLight,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w700),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -381,7 +447,10 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
|
|
||||||
const Text(
|
const Text(
|
||||||
'رمز التحقق المعتمد (OTP):',
|
'رمز التحقق المعتمد (OTP):',
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13),
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildOtpField(controller: _otpController),
|
_buildOtpField(controller: _otpController),
|
||||||
@@ -392,15 +461,23 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: TeacherTheme.emeraldPrimary,
|
backgroundColor: TeacherTheme.emeraldPrimary,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding:
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(14)),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
child: isLoading
|
child: isLoading
|
||||||
? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.black, strokeWidth: 2))
|
? const SizedBox(
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: Colors.black, strokeWidth: 2))
|
||||||
: const Text(
|
: const Text(
|
||||||
'تأكيد الدخول لملف المعلم ✨',
|
'تأكيد الدخول لملف المعلم ✨',
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w900),
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w900),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
@@ -410,16 +487,31 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
children: [
|
children: [
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: isLoading ? null : _onSendOtp,
|
onPressed: isLoading ? null : _onSendOtp,
|
||||||
icon: const Icon(CupertinoIcons.refresh_circled, size: 16, color: Color(0xFF25D366)),
|
icon: const Icon(
|
||||||
label: const Text('إعادة إرسال الرمز 📲', style: TextStyle(color: Color(0xFF25D366), fontSize: 12, fontWeight: FontWeight.bold)),
|
CupertinoIcons.refresh_circled,
|
||||||
|
size: 16,
|
||||||
|
color: Color(0xFF25D366)),
|
||||||
|
label: const Text('إعادة إرسال الرمز 📲',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFF25D366),
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() => _isEnteringOtp = false);
|
setState(() => _isEnteringOtp = false);
|
||||||
context.read<TeacherAuthCubit>().resetToPhone();
|
context
|
||||||
|
.read<TeacherAuthCubit>()
|
||||||
|
.resetToPhone();
|
||||||
},
|
},
|
||||||
icon: const Icon(CupertinoIcons.pencil_ellipsis_rectangle, size: 16, color: Color(0xFF94A3B8)),
|
icon: const Icon(
|
||||||
label: const Text('تعديل رقم الهاتف ✏️', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 12)),
|
CupertinoIcons.pencil_ellipsis_rectangle,
|
||||||
|
size: 16,
|
||||||
|
color: Color(0xFF94A3B8)),
|
||||||
|
label: const Text('تعديل رقم الهاتف ✏️',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFF94A3B8),
|
||||||
|
fontSize: 12)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -452,13 +544,15 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 13.5, fontWeight: FontWeight.w600),
|
style: const TextStyle(
|
||||||
|
color: Colors.white, fontSize: 13.5, fontWeight: FontWeight.w600),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
prefixIcon: Icon(icon, color: TeacherTheme.emeraldPrimary, size: 18),
|
prefixIcon: Icon(icon, color: TeacherTheme.emeraldPrimary, size: 18),
|
||||||
hintText: hint,
|
hintText: hint,
|
||||||
hintStyle: const TextStyle(color: Color(0xFF64748B), fontSize: 12.5),
|
hintStyle: const TextStyle(color: Color(0xFF64748B), fontSize: 12.5),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
contentPadding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -471,7 +565,8 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: TeacherTheme.surfaceCard,
|
color: TeacherTheme.surfaceCard,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(color: TeacherTheme.emeraldPrimary.withOpacity(0.6), width: 1.5),
|
border: Border.all(
|
||||||
|
color: TeacherTheme.emeraldPrimary.withOpacity(0.6), width: 1.5),
|
||||||
boxShadow: const [
|
boxShadow: const [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Color(0x2210B981),
|
color: Color(0x2210B981),
|
||||||
@@ -493,7 +588,8 @@ class _TeacherAuthScreenState extends State<TeacherAuthScreen> {
|
|||||||
),
|
),
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
counterText: '',
|
counterText: '',
|
||||||
prefixIcon: Icon(CupertinoIcons.lock_shield_fill, color: TeacherTheme.emeraldPrimary, size: 22),
|
prefixIcon: Icon(CupertinoIcons.lock_shield_fill,
|
||||||
|
color: TeacherTheme.emeraldPrimary, size: 22),
|
||||||
hintText: '• • • • • •',
|
hintText: '• • • • • •',
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
color: Color(0xFF475569),
|
color: Color(0xFF475569),
|
||||||
|
|||||||
@@ -21,13 +21,16 @@ class TeacherAssignmentsQnATab extends StatefulWidget {
|
|||||||
const TeacherAssignmentsQnATab({super.key});
|
const TeacherAssignmentsQnATab({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TeacherAssignmentsQnATab> createState() => _TeacherAssignmentsQnATabState();
|
State<TeacherAssignmentsQnATab> createState() =>
|
||||||
|
_TeacherAssignmentsQnATabState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
||||||
final TextEditingController _broadcastTitleCtrl = TextEditingController(text: 'توضيح بيداغوجي: مسار الشحنة في الوعاء المغناطيسي');
|
final TextEditingController _broadcastTitleCtrl = TextEditingController(
|
||||||
|
text: 'توضيح بيداغوجي: مسار الشحنة في الوعاء المغناطيسي');
|
||||||
final TextEditingController _broadcastMessageCtrl = TextEditingController(
|
final TextEditingController _broadcastMessageCtrl = TextEditingController(
|
||||||
text: 'أعزائي طلبة الصف العاشر: انتبهوا إلى أن قوة لورنتز تكون دائماً عمودية على كل من متجه السرعة ومتجه المجال، لذلك لا تبذل شغلاً ولا تغيّر الطاقة الحركية، بل تُوجّه الجسيم في مسار حلزوني محصور داخل الوعاء.',
|
text:
|
||||||
|
'أعزائي طلبة الصف العاشر: انتبهوا إلى أن قوة لورنتز تكون دائماً عمودية على كل من متجه السرعة ومتجه المجال، لذلك لا تبذل شغلاً ولا تغيّر الطاقة الحركية، بل تُوجّه الجسيم في مسار حلزوني محصور داخل الوعاء.',
|
||||||
);
|
);
|
||||||
String _selectedBroadcastGrade = 'الصف العاشر الأساسي';
|
String _selectedBroadcastGrade = 'الصف العاشر الأساسي';
|
||||||
|
|
||||||
@@ -68,7 +71,8 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
className: 'الصف العاشر الأساسي (شعبة أ وب)',
|
className: 'الصف العاشر الأساسي (شعبة أ وب)',
|
||||||
dueDate: 'الخميس القادم الساعة 08:00 م',
|
dueDate: 'الخميس القادم الساعة 08:00 م',
|
||||||
);
|
);
|
||||||
SaqelToast.showSuccess(context, 'تم إسناد ورقة العمل وإشعار الطلبة بنجاح 🚀');
|
SaqelToast.showSuccess(
|
||||||
|
context, 'تم إسناد ورقة العمل وإشعار الطلبة بنجاح 🚀');
|
||||||
},
|
},
|
||||||
child: const Text('إسناد للطلبة الآن'),
|
child: const Text('إسناد للطلبة الآن'),
|
||||||
),
|
),
|
||||||
@@ -95,7 +99,8 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: TeacherTheme.surfaceDark,
|
color: TeacherTheme.surfaceDark,
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
border: Border(top: BorderSide(color: TeacherTheme.emeraldPrimary, width: 2)),
|
border: Border(
|
||||||
|
top: BorderSide(color: TeacherTheme.emeraldPrimary, width: 2)),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@@ -110,7 +115,8 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
color: TeacherTheme.emeraldPrimary.withOpacity(0.15),
|
color: TeacherTheme.emeraldPrimary.withOpacity(0.15),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: const Icon(CupertinoIcons.waveform_path_badge_plus, color: TeacherTheme.emeraldPrimary, size: 24),
|
child: const Icon(CupertinoIcons.waveform_path_badge_plus,
|
||||||
|
color: TeacherTheme.emeraldPrimary, size: 24),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
const Expanded(
|
const Expanded(
|
||||||
@@ -119,12 +125,16 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'بث مقطع صوتي وإشعار فوري للشعبة 🎙️',
|
'بث مقطع صوتي وإشعار فوري للشعبة 🎙️',
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 15),
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
fontSize: 15),
|
||||||
),
|
),
|
||||||
SizedBox(height: 2),
|
SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
'يصل التسجيل الصوتي عبر خادم الويب سوكت (0ms) لكافة الطلبة المشتركين',
|
'يصل التسجيل الصوتي عبر خادم الويب سوكت (0ms) لكافة الطلبة المشتركين',
|
||||||
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 11),
|
style:
|
||||||
|
TextStyle(color: Color(0xFF94A3B8), fontSize: 11),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -133,18 +143,28 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
|
|
||||||
const Text('عنوان التوجيه الصوتي:', style: TextStyle(color: Colors.white70, fontSize: 12, fontWeight: FontWeight.bold)),
|
const Text('عنوان التوجيه الصوتي:',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_buildInputContainer(
|
_buildInputContainer(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _broadcastTitleCtrl,
|
controller: _broadcastTitleCtrl,
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 13),
|
style: const TextStyle(color: Colors.white, fontSize: 13),
|
||||||
decoration: const InputDecoration(border: InputBorder.none, hintText: 'عنوان التوجيه الصوتي'),
|
decoration: const InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintText: 'عنوان التوجيه الصوتي'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
const Text('الشعبة المستهدفة:', style: TextStyle(color: Colors.white70, fontSize: 12, fontWeight: FontWeight.bold)),
|
const Text('الشعبة المستهدفة:',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_buildInputContainer(
|
_buildInputContainer(
|
||||||
child: DropdownButtonHideUnderline(
|
child: DropdownButtonHideUnderline(
|
||||||
@@ -152,51 +172,79 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
value: _selectedBroadcastGrade,
|
value: _selectedBroadcastGrade,
|
||||||
isExpanded: true,
|
isExpanded: true,
|
||||||
dropdownColor: TeacherTheme.surfaceDark,
|
dropdownColor: TeacherTheme.surfaceDark,
|
||||||
icon: const Icon(CupertinoIcons.chevron_down, color: TeacherTheme.emeraldPrimary, size: 16),
|
icon: const Icon(CupertinoIcons.chevron_down,
|
||||||
|
color: TeacherTheme.emeraldPrimary, size: 16),
|
||||||
items: const [
|
items: const [
|
||||||
DropdownMenuItem(value: 'الصف العاشر الأساسي', child: Text('الصف العاشر الأساسي (فيزياء)', style: TextStyle(color: Colors.white, fontSize: 12.5))),
|
DropdownMenuItem(
|
||||||
DropdownMenuItem(value: 'الأول ثانوي العلمي', child: Text('الأول ثانوي العلمي (ميكانيكا)', style: TextStyle(color: Colors.white, fontSize: 12.5))),
|
value: 'الصف العاشر الأساسي',
|
||||||
DropdownMenuItem(value: 'الثاني ثانوي (التوجيهي)', child: Text('الثاني ثانوي التوجيهي (مكثف)', style: TextStyle(color: Colors.white, fontSize: 12.5))),
|
child: Text('الصف العاشر الأساسي (فيزياء)',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white, fontSize: 12.5))),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'الأول ثانوي العلمي',
|
||||||
|
child: Text('الأول ثانوي العلمي (ميكانيكا)',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white, fontSize: 12.5))),
|
||||||
|
DropdownMenuItem(
|
||||||
|
value: 'الثاني ثانوي (التوجيهي)',
|
||||||
|
child: Text('الثاني ثانوي التوجيهي (مكثف)',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white, fontSize: 12.5))),
|
||||||
],
|
],
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
if (val != null) setState(() => _selectedBroadcastGrade = val);
|
if (val != null)
|
||||||
|
setState(() => _selectedBroadcastGrade = val);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
const Text('نص الشرح أو ملخص المقطع الصوتي:', style: TextStyle(color: Colors.white70, fontSize: 12, fontWeight: FontWeight.bold)),
|
const Text('نص الشرح أو ملخص المقطع الصوتي:',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_buildInputContainer(
|
_buildInputContainer(
|
||||||
child: TextField(
|
child: TextField(
|
||||||
controller: _broadcastMessageCtrl,
|
controller: _broadcastMessageCtrl,
|
||||||
maxLines: 4,
|
maxLines: 4,
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 12.5, height: 1.4),
|
style: const TextStyle(
|
||||||
decoration: const InputDecoration(border: InputBorder.none, hintText: 'اكتب التوجيه المفاهيمي للشعبة...'),
|
color: Colors.white, fontSize: 12.5, height: 1.4),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintText: 'اكتب التوجيه المفاهيمي للشعبة...'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Voice Recording Strip Simulation
|
// Voice Recording Strip Simulation
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF0C241D),
|
color: const Color(0xFF0C241D),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: TeacherTheme.emeraldPrimary.withOpacity(0.5)),
|
border: Border.all(
|
||||||
|
color: TeacherTheme.emeraldPrimary.withOpacity(0.5)),
|
||||||
),
|
),
|
||||||
child: const Row(
|
child: const Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(CupertinoIcons.mic_fill, color: TeacherTheme.emeraldPrimary, size: 18),
|
Icon(CupertinoIcons.mic_fill,
|
||||||
|
color: TeacherTheme.emeraldPrimary, size: 18),
|
||||||
SizedBox(width: 10),
|
SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'مقطع صوتي مسجل بنقاء عالي: 01:14 دقيقة (ترميز AAC - 128kbps)',
|
'مقطع صوتي مسجل بنقاء عالي: 01:14 دقيقة (ترميز AAC - 128kbps)',
|
||||||
style: TextStyle(color: TeacherTheme.emeraldLight, fontSize: 11.5, fontWeight: FontWeight.w600),
|
style: TextStyle(
|
||||||
|
color: TeacherTheme.emeraldLight,
|
||||||
|
fontSize: 11.5,
|
||||||
|
fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Icon(CupertinoIcons.play_circle_fill, color: TeacherTheme.emeraldPrimary, size: 22),
|
Icon(CupertinoIcons.play_circle_fill,
|
||||||
|
color: TeacherTheme.emeraldPrimary, size: 22),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -205,7 +253,9 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
Navigator.of(ctx).pop();
|
Navigator.of(ctx).pop();
|
||||||
final success = await context.read<TeacherQnACubit>().broadcastAudioAnnouncement(
|
final success = await context
|
||||||
|
.read<TeacherQnACubit>()
|
||||||
|
.broadcastAudioAnnouncement(
|
||||||
title: _broadcastTitleCtrl.text.trim(),
|
title: _broadcastTitleCtrl.text.trim(),
|
||||||
explanationText: _broadcastMessageCtrl.text.trim(),
|
explanationText: _broadcastMessageCtrl.text.trim(),
|
||||||
gradeLevel: _selectedBroadcastGrade,
|
gradeLevel: _selectedBroadcastGrade,
|
||||||
@@ -219,12 +269,15 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon: const Icon(CupertinoIcons.paperplane_fill, size: 16),
|
icon: const Icon(CupertinoIcons.paperplane_fill, size: 16),
|
||||||
label: const Text('بث المقطع الصوتي للشعبة الآن 🚀', style: TextStyle(fontSize: 13, fontWeight: FontWeight.bold)),
|
label: const Text('بث المقطع الصوتي للشعبة الآن 🚀',
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 13, fontWeight: FontWeight.bold)),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: TeacherTheme.emeraldPrimary,
|
backgroundColor: TeacherTheme.emeraldPrimary,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -244,17 +297,22 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: TeacherTheme.surfaceCard,
|
color: TeacherTheme.surfaceCard,
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
border: Border(top: BorderSide(color: TeacherTheme.emeraldPrimary, width: 2)),
|
border: Border(
|
||||||
|
top: BorderSide(color: TeacherTheme.emeraldPrimary, width: 2)),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.all(24),
|
padding: const EdgeInsets.all(24),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Icon(CupertinoIcons.mic_circle_fill, size: 56, color: TeacherTheme.emeraldPrimary),
|
const Icon(CupertinoIcons.mic_circle_fill,
|
||||||
|
size: 56, color: TeacherTheme.emeraldPrimary),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
'تسجيل رد صوتي سقراطي للطالبة ($studentName) 🎙️',
|
'تسجيل رد صوتي سقراطي للطالبة ($studentName) 🎙️',
|
||||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w800, color: Colors.white),
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
const Text(
|
const Text(
|
||||||
@@ -267,7 +325,8 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<TeacherQnACubit>().sendVoiceReply(
|
context.read<TeacherQnACubit>().sendVoiceReply(
|
||||||
doubtId,
|
doubtId,
|
||||||
voiceText: 'رد الأستاذ صوتياً على استفسار الطالبة $studentName حول الوعاء المغناطيسي وحصر البلازما.',
|
voiceText:
|
||||||
|
'رد الأستاذ صوتياً على استفسار الطالبة $studentName حول الوعاء المغناطيسي وحصر البلازما.',
|
||||||
);
|
);
|
||||||
Navigator.of(ctx).pop();
|
Navigator.of(ctx).pop();
|
||||||
SaqelToast.showSuccess(
|
SaqelToast.showSuccess(
|
||||||
@@ -282,7 +341,8 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
backgroundColor: TeacherTheme.emeraldPrimary,
|
backgroundColor: TeacherTheme.emeraldPrimary,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
minimumSize: const Size(double.infinity, 44),
|
minimumSize: const Size(double.infinity, 44),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -297,7 +357,9 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
return BlocBuilder<TeacherQnACubit, TeacherQnAState>(
|
return BlocBuilder<TeacherQnACubit, TeacherQnAState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.isLoading) {
|
if (state.isLoading) {
|
||||||
return const Center(child: CupertinoActivityIndicator(color: TeacherTheme.emeraldPrimary));
|
return const Center(
|
||||||
|
child: CupertinoActivityIndicator(
|
||||||
|
color: TeacherTheme.emeraldPrimary));
|
||||||
}
|
}
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
@@ -315,7 +377,8 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(18),
|
borderRadius: BorderRadius.circular(18),
|
||||||
border: Border.all(color: TeacherTheme.emeraldPrimary, width: 1.2),
|
border: Border.all(
|
||||||
|
color: TeacherTheme.emeraldPrimary, width: 1.2),
|
||||||
boxShadow: const [
|
boxShadow: const [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Color(0x3310B981),
|
color: Color(0x3310B981),
|
||||||
@@ -334,7 +397,8 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(color: TeacherTheme.emeraldPrimary),
|
border: Border.all(color: TeacherTheme.emeraldPrimary),
|
||||||
),
|
),
|
||||||
child: const Icon(CupertinoIcons.waveform_path_ecg, color: TeacherTheme.emeraldPrimary, size: 24),
|
child: const Icon(CupertinoIcons.waveform_path_ecg,
|
||||||
|
color: TeacherTheme.emeraldPrimary, size: 24),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 14),
|
const SizedBox(width: 14),
|
||||||
const Expanded(
|
const Expanded(
|
||||||
@@ -345,16 +409,21 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'بث صوتي وتوجيهي عبر الويب سوكت',
|
'بث صوتي وتوجيهي عبر الويب سوكت',
|
||||||
style: TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 13.5),
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
fontSize: 13.5),
|
||||||
),
|
),
|
||||||
SizedBox(width: 6),
|
SizedBox(width: 6),
|
||||||
Icon(CupertinoIcons.antenna_radiowaves_left_right, color: TeacherTheme.emeraldPrimary, size: 14),
|
Icon(CupertinoIcons.antenna_radiowaves_left_right,
|
||||||
|
color: TeacherTheme.emeraldPrimary, size: 14),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
SizedBox(height: 4),
|
SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'بث مقطع صوتي حي وشرح مباشر لجميع طلبة الشعبة المشتركين (0ms)',
|
'بث مقطع صوتي حي وشرح مباشر لجميع طلبة الشعبة المشتركين (0ms)',
|
||||||
style: TextStyle(color: Color(0xFF94A3B8), fontSize: 11),
|
style: TextStyle(
|
||||||
|
color: Color(0xFF94A3B8), fontSize: 11),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -364,10 +433,14 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: TeacherTheme.emeraldPrimary,
|
backgroundColor: TeacherTheme.emeraldPrimary,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
padding: const EdgeInsets.symmetric(
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
horizontal: 12, vertical: 10),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
),
|
),
|
||||||
child: const Text('بدء البث 🎙️', style: TextStyle(fontSize: 12, fontWeight: FontWeight.w800)),
|
child: const Text('بدء البث 🎙️',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12, fontWeight: FontWeight.w800)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -380,18 +453,27 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
children: [
|
children: [
|
||||||
const Row(
|
const Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(CupertinoIcons.doc_text_fill, color: TeacherTheme.emeraldPrimary, size: 16),
|
Icon(CupertinoIcons.doc_text_fill,
|
||||||
|
color: TeacherTheme.emeraldPrimary, size: 16),
|
||||||
SizedBox(width: 6),
|
SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
'أوراق العمل والواجبات الوزارية 📝',
|
'أوراق العمل والواجبات الوزارية 📝',
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: Colors.white),
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: _dispatchNewHomework,
|
onPressed: _dispatchNewHomework,
|
||||||
icon: const Icon(CupertinoIcons.plus_circle_fill, size: 15, color: TeacherTheme.emeraldPrimary),
|
icon: const Icon(CupertinoIcons.plus_circle_fill,
|
||||||
label: const Text('إسناد واجب جديد', style: TextStyle(color: TeacherTheme.emeraldPrimary, fontSize: 12, fontWeight: FontWeight.bold)),
|
size: 15, color: TeacherTheme.emeraldPrimary),
|
||||||
|
label: const Text('إسناد واجب جديد',
|
||||||
|
style: TextStyle(
|
||||||
|
color: TeacherTheme.emeraldPrimary,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -404,11 +486,15 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
// Student Inquiries / Real Chat Header
|
// Student Inquiries / Real Chat Header
|
||||||
const Row(
|
const Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(CupertinoIcons.chat_bubble_2_fill, color: TeacherTheme.cyberCyan, size: 18),
|
Icon(CupertinoIcons.chat_bubble_2_fill,
|
||||||
|
color: TeacherTheme.cyberCyan, size: 18),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'استفسارات الطلبة والمحادثات المباشرة (غرفة الشكوك) 💬',
|
'استفسارات الطلبة والمحادثات المباشرة (غرفة الشكوك) 💬',
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: Colors.white),
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -444,7 +530,10 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: TeacherTheme.surfaceDark,
|
color: TeacherTheme.surfaceDark,
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
border: Border.all(color: isCompleted ? TeacherTheme.surfaceBorder : TeacherTheme.emeraldPrimary.withOpacity(0.3)),
|
border: Border.all(
|
||||||
|
color: isCompleted
|
||||||
|
? TeacherTheme.surfaceBorder
|
||||||
|
: TeacherTheme.emeraldPrimary.withOpacity(0.3)),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -455,13 +544,18 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
hw.title,
|
hw.title,
|
||||||
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w800, color: Colors.white),
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isCompleted ? const Color(0xFF1E293B) : TeacherTheme.emeraldPrimary.withOpacity(0.15),
|
color: isCompleted
|
||||||
|
? const Color(0xFF1E293B)
|
||||||
|
: TeacherTheme.emeraldPrimary.withOpacity(0.15),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -469,7 +563,9 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10.5,
|
fontSize: 10.5,
|
||||||
fontWeight: FontWeight.w800,
|
fontWeight: FontWeight.w800,
|
||||||
color: isCompleted ? const Color(0xFF94A3B8) : TeacherTheme.emeraldPrimary,
|
color: isCompleted
|
||||||
|
? const Color(0xFF94A3B8)
|
||||||
|
: TeacherTheme.emeraldPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -478,9 +574,15 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Text(hw.className, style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8))),
|
Text(hw.className,
|
||||||
|
style:
|
||||||
|
const TextStyle(fontSize: 11, color: Color(0xFF94A3B8))),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text('المعدل: ${hw.averageScore}', style: const TextStyle(fontSize: 11, fontWeight: FontWeight.bold, color: TeacherTheme.royalGold)),
|
Text('المعدل: ${hw.averageScore}',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: TeacherTheme.royalGold)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -496,7 +598,9 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
color: TeacherTheme.surfaceDark,
|
color: TeacherTheme.surfaceDark,
|
||||||
borderRadius: BorderRadius.circular(14),
|
borderRadius: BorderRadius.circular(14),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: doubt.replied ? TeacherTheme.surfaceBorder : TeacherTheme.cyberCyan.withOpacity(0.5),
|
color: doubt.replied
|
||||||
|
? TeacherTheme.surfaceBorder
|
||||||
|
: TeacherTheme.cyberCyan.withOpacity(0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -517,7 +621,10 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
child: Center(
|
child: Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
doubt.studentName.characters.first,
|
doubt.studentName.characters.first,
|
||||||
style: const TextStyle(color: TeacherTheme.cyberCyan, fontWeight: FontWeight.bold, fontSize: 13),
|
style: const TextStyle(
|
||||||
|
color: TeacherTheme.cyberCyan,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -525,13 +632,21 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(doubt.studentName, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w800, fontSize: 12.5)),
|
Text(doubt.studentName,
|
||||||
Text(doubt.className, style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 10.5)),
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
fontSize: 12.5)),
|
||||||
|
Text(doubt.className,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF94A3B8), fontSize: 10.5)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(doubt.time, style: const TextStyle(color: Color(0xFF64748B), fontSize: 10.5)),
|
Text(doubt.time,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF64748B), fontSize: 10.5)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
@@ -543,7 +658,8 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
doubt.question,
|
doubt.question,
|
||||||
style: const TextStyle(color: Colors.white70, fontSize: 12, height: 1.4),
|
style: const TextStyle(
|
||||||
|
color: Colors.white70, fontSize: 12, height: 1.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
@@ -552,7 +668,8 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
children: [
|
children: [
|
||||||
if (doubt.replied)
|
if (doubt.replied)
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: TeacherTheme.emeraldPrimary.withOpacity(0.15),
|
color: TeacherTheme.emeraldPrimary.withOpacity(0.15),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
@@ -560,22 +677,32 @@ class _TeacherAssignmentsQnATabState extends State<TeacherAssignmentsQnATab> {
|
|||||||
child: const Row(
|
child: const Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(CupertinoIcons.checkmark_alt_circle_fill, color: TeacherTheme.emeraldPrimary, size: 14),
|
Icon(CupertinoIcons.checkmark_alt_circle_fill,
|
||||||
|
color: TeacherTheme.emeraldPrimary, size: 14),
|
||||||
SizedBox(width: 4),
|
SizedBox(width: 4),
|
||||||
Text('تم الرد بصوت مسجل 🎧', style: TextStyle(color: TeacherTheme.emeraldPrimary, fontSize: 11, fontWeight: FontWeight.bold)),
|
Text('تم الرد بصوت مسجل 🎧',
|
||||||
|
style: TextStyle(
|
||||||
|
color: TeacherTheme.emeraldPrimary,
|
||||||
|
fontSize: 11,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () => _recordVoiceReply(doubt.id, doubt.studentName),
|
onPressed: () =>
|
||||||
|
_recordVoiceReply(doubt.id, doubt.studentName),
|
||||||
icon: const Icon(CupertinoIcons.mic_fill, size: 14),
|
icon: const Icon(CupertinoIcons.mic_fill, size: 14),
|
||||||
label: const Text('تسجيل رد صوتي سقراطي 🎙️', style: TextStyle(fontSize: 11.5, fontWeight: FontWeight.w800)),
|
label: const Text('تسجيل رد صوتي سقراطي 🎙️',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11.5, fontWeight: FontWeight.w800)),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: TeacherTheme.emeraldPrimary,
|
backgroundColor: TeacherTheme.emeraldPrimary,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
padding:
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -31,9 +31,10 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
context.read<TeacherMonetizationCubit>().loadMonetization();
|
context.read<TeacherMonetizationCubit>().loadMonetization();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showCliqPayoutDialog(BuildContext ctx, double availableBalance, String defaultAlias) {
|
void _showCliqPayoutDialog(
|
||||||
|
BuildContext ctx, double availableBalance, String defaultAlias) {
|
||||||
final TextEditingController cliqController = TextEditingController(
|
final TextEditingController cliqController = TextEditingController(
|
||||||
text: defaultAlias.isNotEmpty ? defaultAlias : '0798583052@CLIQ',
|
text: defaultAlias,
|
||||||
);
|
);
|
||||||
final TextEditingController amountController = TextEditingController(
|
final TextEditingController amountController = TextEditingController(
|
||||||
text: availableBalance > 0 ? availableBalance.toInt().toString() : '50',
|
text: availableBalance > 0 ? availableBalance.toInt().toString() : '50',
|
||||||
@@ -62,17 +63,22 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
children: [
|
children: [
|
||||||
const Row(
|
const Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(CupertinoIcons.bolt_fill, color: TeacherTheme.emeraldPrimary, size: 22),
|
Icon(CupertinoIcons.bolt_fill,
|
||||||
|
color: TeacherTheme.emeraldPrimary, size: 22),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'طلب سحب فوري عبر كليك (CliQ)',
|
'طلب سحب فوري عبر كليك (CliQ)',
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800, color: Colors.white),
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Navigator.pop(bCtx),
|
onPressed: () => Navigator.pop(bCtx),
|
||||||
icon: const Icon(CupertinoIcons.xmark_circle_fill, color: Color(0xFF64748B)),
|
icon: const Icon(CupertinoIcons.xmark_circle_fill,
|
||||||
|
color: Color(0xFF64748B)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -87,11 +93,15 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
style: const TextStyle(color: Colors.white, fontSize: 13),
|
style: const TextStyle(color: Colors.white, fontSize: 13),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'معرّف كليك (CliQ Alias) أو رقم الهاتف',
|
labelText: 'معرّف كليك (CliQ Alias) أو رقم الهاتف',
|
||||||
labelStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12),
|
labelStyle:
|
||||||
|
const TextStyle(color: Color(0xFF94A3B8), fontSize: 12),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFF1E293B),
|
fillColor: const Color(0xFF1E293B),
|
||||||
prefixIcon: const Icon(CupertinoIcons.person_crop_circle, color: TeacherTheme.emeraldPrimary, size: 20),
|
prefixIcon: const Icon(CupertinoIcons.person_crop_circle,
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
color: TeacherTheme.emeraldPrimary, size: 20),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide.none),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -101,11 +111,15 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
style: const TextStyle(color: Colors.white, fontSize: 13),
|
style: const TextStyle(color: Colors.white, fontSize: 13),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'المبلغ المطلوب سحبه (دينار أردني)',
|
labelText: 'المبلغ المطلوب سحبه (دينار أردني)',
|
||||||
labelStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12),
|
labelStyle:
|
||||||
|
const TextStyle(color: Color(0xFF94A3B8), fontSize: 12),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFF1E293B),
|
fillColor: const Color(0xFF1E293B),
|
||||||
prefixIcon: const Icon(CupertinoIcons.money_dollar, color: TeacherTheme.emeraldPrimary, size: 20),
|
prefixIcon: const Icon(CupertinoIcons.money_dollar,
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
color: TeacherTheme.emeraldPrimary, size: 20),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide.none),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
@@ -121,18 +135,21 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
Navigator.pop(bCtx);
|
Navigator.pop(bCtx);
|
||||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
ScaffoldMessenger.of(ctx).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('تم إدراج طلب السحب بمبلغ ${amountController.text} د.أ في طابور كليك بنجاح ⚡'),
|
content: Text(
|
||||||
|
'تم إدراج طلب السحب بمبلغ ${amountController.text} د.أ في طابور كليك بنجاح ⚡'),
|
||||||
backgroundColor: TeacherTheme.emeraldPrimary,
|
backgroundColor: TeacherTheme.emeraldPrimary,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
icon: const Icon(CupertinoIcons.paperplane_fill, size: 16),
|
icon: const Icon(CupertinoIcons.paperplane_fill, size: 16),
|
||||||
label: const Text('تأكيد وإرسال لطابور السحب الفوري (CliQ Queue)'),
|
label:
|
||||||
|
const Text('تأكيد وإرسال لطابور السحب الفوري (CliQ Queue)'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: TeacherTheme.emeraldPrimary,
|
backgroundColor: TeacherTheme.emeraldPrimary,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -142,7 +159,8 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _showIbanPayoutDialog(BuildContext ctx, double availableBalance) {
|
void _showIbanPayoutDialog(BuildContext ctx, double availableBalance) {
|
||||||
final TextEditingController ibanController = TextEditingController(text: 'JO94MEPA0000000000000000000000');
|
final TextEditingController ibanController =
|
||||||
|
TextEditingController(text: 'JO94MEPA0000000000000000000000');
|
||||||
final TextEditingController amountController = TextEditingController(
|
final TextEditingController amountController = TextEditingController(
|
||||||
text: availableBalance > 0 ? availableBalance.toInt().toString() : '100',
|
text: availableBalance > 0 ? availableBalance.toInt().toString() : '100',
|
||||||
);
|
);
|
||||||
@@ -170,17 +188,22 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
children: [
|
children: [
|
||||||
const Row(
|
const Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(CupertinoIcons.building_2_fill, color: TeacherTheme.cyberCyan, size: 22),
|
Icon(CupertinoIcons.building_2_fill,
|
||||||
|
color: TeacherTheme.cyberCyan, size: 22),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'طلب تحويل بنكي رسمي (IBAN Transfer)',
|
'طلب تحويل بنكي رسمي (IBAN Transfer)',
|
||||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800, color: Colors.white),
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Navigator.pop(bCtx),
|
onPressed: () => Navigator.pop(bCtx),
|
||||||
icon: const Icon(CupertinoIcons.xmark_circle_fill, color: Color(0xFF64748B)),
|
icon: const Icon(CupertinoIcons.xmark_circle_fill,
|
||||||
|
color: Color(0xFF64748B)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -195,11 +218,15 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
style: const TextStyle(color: Colors.white, fontSize: 13),
|
style: const TextStyle(color: Colors.white, fontSize: 13),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'رقم الحساب المصرفي الدولي (IBAN)',
|
labelText: 'رقم الحساب المصرفي الدولي (IBAN)',
|
||||||
labelStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12),
|
labelStyle:
|
||||||
|
const TextStyle(color: Color(0xFF94A3B8), fontSize: 12),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFF1E293B),
|
fillColor: const Color(0xFF1E293B),
|
||||||
prefixIcon: const Icon(CupertinoIcons.creditcard, color: TeacherTheme.cyberCyan, size: 20),
|
prefixIcon: const Icon(CupertinoIcons.creditcard,
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
color: TeacherTheme.cyberCyan, size: 20),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide.none),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -209,11 +236,15 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
style: const TextStyle(color: Colors.white, fontSize: 13),
|
style: const TextStyle(color: Colors.white, fontSize: 13),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'المبلغ المطلوب تحويله (دينار أردني)',
|
labelText: 'المبلغ المطلوب تحويله (دينار أردني)',
|
||||||
labelStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12),
|
labelStyle:
|
||||||
|
const TextStyle(color: Color(0xFF94A3B8), fontSize: 12),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFF1E293B),
|
fillColor: const Color(0xFF1E293B),
|
||||||
prefixIcon: const Icon(CupertinoIcons.money_dollar, color: TeacherTheme.cyberCyan, size: 20),
|
prefixIcon: const Icon(CupertinoIcons.money_dollar,
|
||||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none),
|
color: TeacherTheme.cyberCyan, size: 20),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
borderSide: BorderSide.none),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
@@ -229,7 +260,8 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
Navigator.pop(bCtx);
|
Navigator.pop(bCtx);
|
||||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
ScaffoldMessenger.of(ctx).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text('تم تسجيل أمر التحويل المصرفي بمبلغ ${amountController.text} د.أ بنجاح 🏦'),
|
content: Text(
|
||||||
|
'تم تسجيل أمر التحويل المصرفي بمبلغ ${amountController.text} د.أ بنجاح 🏦'),
|
||||||
backgroundColor: TeacherTheme.cyberCyan,
|
backgroundColor: TeacherTheme.cyberCyan,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -240,7 +272,8 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
backgroundColor: TeacherTheme.cyberCyan,
|
backgroundColor: TeacherTheme.cyberCyan,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -274,11 +307,15 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'تصنيف قاعدة الطلبة (المؤسسي مقابل الخارجي) 👥',
|
'تصنيف قاعدة الطلبة (المؤسسي مقابل الخارجي) 👥',
|
||||||
style: TextStyle(fontSize: 13.5, fontWeight: FontWeight.w800, color: Colors.white),
|
style: TextStyle(
|
||||||
|
fontSize: 13.5,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'تحديث الكشوفات: اليوم',
|
'تحديث الكشوفات: اليوم',
|
||||||
style: TextStyle(fontSize: 11, color: Color(0xFF64748B)),
|
style:
|
||||||
|
TextStyle(fontSize: 11, color: Color(0xFF64748B)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -288,7 +325,9 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF064E3B).withOpacity(0.3),
|
color: const Color(0xFF064E3B).withOpacity(0.3),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: TeacherTheme.emeraldPrimary.withOpacity(0.3)),
|
border: Border.all(
|
||||||
|
color:
|
||||||
|
TeacherTheme.emeraldPrimary.withOpacity(0.3)),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
@@ -298,18 +337,26 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'طلبة الثقافة العسكرية والمدارس الشريكة',
|
'طلبة الثقافة العسكرية والمدارس الشريكة',
|
||||||
style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.w700, color: Colors.white),
|
style: TextStyle(
|
||||||
|
fontSize: 12.5,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white),
|
||||||
),
|
),
|
||||||
SizedBox(height: 2),
|
SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
'مشمولون برسم (0.00 د.أ) ضمن العقد المؤسسي',
|
'مشمولون برسم (0.00 د.أ) ضمن العقد المؤسسي',
|
||||||
style: TextStyle(fontSize: 11, color: TeacherTheme.emeraldLight),
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: TeacherTheme.emeraldLight),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${state.institutionalStudents.toInt()} طالباً 🎖️',
|
'${state.institutionalStudents.toInt()} طالباً 🎖️',
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: TeacherTheme.emeraldLight),
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: TeacherTheme.emeraldLight),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -320,7 +367,8 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFF0369A1).withOpacity(0.2),
|
color: const Color(0xFF0369A1).withOpacity(0.2),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: const Color(0xFF0284C7).withOpacity(0.3)),
|
border: Border.all(
|
||||||
|
color: const Color(0xFF0284C7).withOpacity(0.3)),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
@@ -330,18 +378,26 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'الطلبة المستقلون (سوق صَقِل المفتوح)',
|
'الطلبة المستقلون (سوق صَقِل المفتوح)',
|
||||||
style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.w700, color: Colors.white),
|
style: TextStyle(
|
||||||
|
fontSize: 12.5,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white),
|
||||||
),
|
),
|
||||||
SizedBox(height: 2),
|
SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
'مشتركون عبر نظام الدفع الفوري كليك (CliQ)',
|
'مشتركون عبر نظام الدفع الفوري كليك (CliQ)',
|
||||||
style: TextStyle(fontSize: 11, color: TeacherTheme.cyberCyan),
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: TeacherTheme.cyberCyan),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${state.studentSubscribers.toInt()} طالباً 💳',
|
'${state.studentSubscribers.toInt()} طالباً 💳',
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: TeacherTheme.cyberCyan),
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: TeacherTheme.cyberCyan),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -361,7 +417,8 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
border: Border.all(color: TeacherTheme.emeraldPrimary.withOpacity(0.4)),
|
border: Border.all(
|
||||||
|
color: TeacherTheme.emeraldPrimary.withOpacity(0.4)),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: TeacherTheme.emeraldPrimary.withOpacity(0.15),
|
color: TeacherTheme.emeraldPrimary.withOpacity(0.15),
|
||||||
@@ -385,14 +442,18 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8, vertical: 3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withOpacity(0.3),
|
color: Colors.black.withOpacity(0.3),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'CliQ & IBAN',
|
'CliQ & IBAN',
|
||||||
style: TextStyle(fontSize: 11, color: TeacherTheme.emeraldLight, fontWeight: FontWeight.w700),
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: TeacherTheme.emeraldLight,
|
||||||
|
fontWeight: FontWeight.w700),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
@@ -411,7 +472,8 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'إجمالي المبالغ المسحوبة سابقاً: ${state.totalWithdrawn.toStringAsFixed(0)} د.أ',
|
'إجمالي المبالغ المسحوبة سابقاً: ${state.totalWithdrawn.toStringAsFixed(0)} د.أ',
|
||||||
style: const TextStyle(fontSize: 11.5, color: Color(0xFF6EE7B7)),
|
style: const TextStyle(
|
||||||
|
fontSize: 11.5, color: Color(0xFF6EE7B7)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
@@ -420,32 +482,43 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_showCliqPayoutDialog(context, state.availableBalance, state.cliqAlias);
|
_showCliqPayoutDialog(context,
|
||||||
|
state.availableBalance, state.cliqAlias);
|
||||||
},
|
},
|
||||||
icon: const Icon(CupertinoIcons.bolt_fill, size: 16),
|
icon:
|
||||||
|
const Icon(CupertinoIcons.bolt_fill, size: 16),
|
||||||
label: const Text(
|
label: const Text(
|
||||||
'سحب فوري عبر كليك (CliQ)',
|
'سحب فوري عبر كليك (CliQ)',
|
||||||
style: TextStyle(fontSize: 12, fontWeight: FontWeight.w800),
|
style: TextStyle(
|
||||||
|
fontSize: 12, fontWeight: FontWeight.w800),
|
||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: TeacherTheme.emeraldPrimary,
|
backgroundColor: TeacherTheme.emeraldPrimary,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
_showIbanPayoutDialog(context, state.availableBalance);
|
_showIbanPayoutDialog(
|
||||||
|
context, state.availableBalance);
|
||||||
},
|
},
|
||||||
icon: const Icon(CupertinoIcons.building_2_fill, size: 16, color: Colors.white),
|
icon: const Icon(CupertinoIcons.building_2_fill,
|
||||||
label: const Text('تحويل بنكي', style: TextStyle(fontSize: 12, color: Colors.white)),
|
size: 16, color: Colors.white),
|
||||||
|
label: const Text('تحويل بنكي',
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 12, color: Colors.white)),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
side: BorderSide(color: Colors.white.withOpacity(0.3)),
|
side: BorderSide(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12),
|
color: Colors.white.withOpacity(0.3)),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 12, horizontal: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -495,33 +568,22 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
TeacherTheme.cyberCyan,
|
TeacherTheme.cyberCyan,
|
||||||
),
|
),
|
||||||
|
|
||||||
const Divider(color: TeacherTheme.surfaceBorder, height: 24),
|
const Divider(
|
||||||
|
color: TeacherTheme.surfaceBorder, height: 24),
|
||||||
|
|
||||||
// Subscribers Slider
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text('المشتركون الفعليون:',
|
||||||
'محاكي المشتركين خارج الثقافة:',
|
style: TextStyle(
|
||||||
style: TextStyle(fontSize: 12, color: Color(0xFF94A3B8)),
|
fontSize: 12, color: Color(0xFF94A3B8))),
|
||||||
),
|
Text('${state.studentSubscribers.toInt()} طالب مشترك',
|
||||||
Text(
|
style: const TextStyle(
|
||||||
'${state.studentSubscribers.toInt()} طالب مشترك',
|
fontSize: 13.5,
|
||||||
style: const TextStyle(fontSize: 13.5, fontWeight: FontWeight.w800, color: TeacherTheme.emeraldLight),
|
fontWeight: FontWeight.w800,
|
||||||
),
|
color: TeacherTheme.emeraldLight)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Slider(
|
|
||||||
value: state.studentSubscribers,
|
|
||||||
min: 50,
|
|
||||||
max: 2000,
|
|
||||||
divisions: 39,
|
|
||||||
activeColor: TeacherTheme.emeraldPrimary,
|
|
||||||
inactiveColor: TeacherTheme.surfaceBorder,
|
|
||||||
onChanged: (val) {
|
|
||||||
context.read<TeacherMonetizationCubit>().updateSubscribers(val);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -530,19 +592,26 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
// Active Courses Selling
|
// Active Courses Selling
|
||||||
const Text(
|
const Text(
|
||||||
'الدورات المنشورة في سوق صَقِل الخارجي:',
|
'الدورات المنشورة في سوق صَقِل الخارجي:',
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: Colors.white),
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
if (state.courses.isNotEmpty)
|
if (state.courses.isNotEmpty)
|
||||||
...state.courses.map((course) => Padding(
|
...state.courses
|
||||||
|
.map((course) => Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
child: _courseEarningCard(
|
child: _courseEarningCard(
|
||||||
title: course.title,
|
title: course.title,
|
||||||
students: course.militaryStudents + course.externalSubscribers,
|
students: course.militaryStudents +
|
||||||
revenue: '${(course.priceJod * (course.externalSubscribers > 0 ? course.externalSubscribers : 1) * 0.55).toStringAsFixed(0)} د.أ',
|
course.externalSubscribers,
|
||||||
|
revenue:
|
||||||
|
'${(course.priceJod * (course.externalSubscribers > 0 ? course.externalSubscribers : 1) * 0.55).toStringAsFixed(0)} د.أ',
|
||||||
),
|
),
|
||||||
)).toList()
|
))
|
||||||
|
.toList()
|
||||||
else ...[
|
else ...[
|
||||||
_courseEarningCard(
|
_courseEarningCard(
|
||||||
title: 'الفيزياء والعلوم التطبيقية — الصف العاشر الأساسي',
|
title: 'الفيزياء والعلوم التطبيقية — الصف العاشر الأساسي',
|
||||||
@@ -556,11 +625,15 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
const SizedBox(height: 18),
|
const SizedBox(height: 18),
|
||||||
const Row(
|
const Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(CupertinoIcons.arrow_right_arrow_left, size: 16, color: TeacherTheme.emeraldPrimary),
|
Icon(CupertinoIcons.arrow_right_arrow_left,
|
||||||
|
size: 16, color: TeacherTheme.emeraldPrimary),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'سجل عمليات وسحوبات كليك (CliQ & IBAN Queue):',
|
'سجل عمليات وسحوبات كليك (CliQ & IBAN Queue):',
|
||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: Colors.white),
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -583,7 +656,9 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
color: TeacherTheme.surfaceCard,
|
color: TeacherTheme.surfaceCard,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isDone ? TeacherTheme.emeraldPrimary.withOpacity(0.3) : TeacherTheme.surfaceBorder,
|
color: isDone
|
||||||
|
? TeacherTheme.emeraldPrimary.withOpacity(0.3)
|
||||||
|
: TeacherTheme.surfaceBorder,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -592,9 +667,13 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
isDone ? CupertinoIcons.checkmark_circle_fill : CupertinoIcons.clock_fill,
|
isDone
|
||||||
|
? CupertinoIcons.checkmark_circle_fill
|
||||||
|
: CupertinoIcons.clock_fill,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: isDone ? TeacherTheme.emeraldPrimary : TeacherTheme.royalGold,
|
color: isDone
|
||||||
|
? TeacherTheme.emeraldPrimary
|
||||||
|
: TeacherTheme.royalGold,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Column(
|
Column(
|
||||||
@@ -602,12 +681,16 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
payout.cliqAlias,
|
payout.cliqAlias,
|
||||||
style: const TextStyle(fontSize: 12.5, fontWeight: FontWeight.w700, color: Colors.white),
|
style: const TextStyle(
|
||||||
|
fontSize: 12.5,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Colors.white),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
payout.requestedAt,
|
payout.requestedAt,
|
||||||
style: const TextStyle(fontSize: 11, color: Color(0xFF64748B)),
|
style:
|
||||||
|
const TextStyle(fontSize: 11, color: Color(0xFF64748B)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -621,14 +704,18 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13.5,
|
fontSize: 13.5,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
color: isDone ? TeacherTheme.emeraldPrimary : TeacherTheme.royalGold,
|
color: isDone
|
||||||
|
? TeacherTheme.emeraldPrimary
|
||||||
|
: TeacherTheme.royalGold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
isDone ? 'تم التحويل بنجاح' : 'في طابور المقاصة ⚡',
|
isDone ? 'تم التحويل بنجاح' : 'في طابور المقاصة ⚡',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
color: isDone ? TeacherTheme.emeraldLight : TeacherTheme.royalGold,
|
color: isDone
|
||||||
|
? TeacherTheme.emeraldLight
|
||||||
|
: TeacherTheme.royalGold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -644,12 +731,19 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Container(width: 8, height: 8, decoration: BoxDecoration(color: color, shape: BoxShape.circle)),
|
Container(
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
decoration:
|
||||||
|
BoxDecoration(color: color, shape: BoxShape.circle)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(label, style: const TextStyle(fontSize: 12.5, color: Colors.white)),
|
Text(label,
|
||||||
|
style: const TextStyle(fontSize: 12.5, color: Colors.white)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(amount, style: TextStyle(fontSize: 13.5, fontWeight: FontWeight.w900, color: color)),
|
Text(amount,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13.5, fontWeight: FontWeight.w900, color: color)),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -673,17 +767,28 @@ class _TeacherMonetizationTabState extends State<TeacherMonetizationTab> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(title, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w800, color: Colors.white)),
|
Text(title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
color: Colors.white)),
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 3),
|
||||||
Text('$students طالب مشترك · رسوم 20 د.أ', style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8))),
|
Text('$students طالب مشترك · رسوم 20 د.أ',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11, color: Color(0xFF94A3B8))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
Text(revenue, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: TeacherTheme.emeraldPrimary)),
|
Text(revenue,
|
||||||
const Text('صافي أرباحك', style: TextStyle(fontSize: 10, color: Color(0xFF64748B))),
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: TeacherTheme.emeraldPrimary)),
|
||||||
|
const Text('صافي أرباحك',
|
||||||
|
style: TextStyle(fontSize: 10, color: Color(0xFF64748B))),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
+76
-41
@@ -20,22 +20,24 @@ class TeacherReputationScorecardTab extends StatefulWidget {
|
|||||||
const TeacherReputationScorecardTab({super.key});
|
const TeacherReputationScorecardTab({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<TeacherReputationScorecardTab> createState() => _TeacherReputationScorecardTabState();
|
State<TeacherReputationScorecardTab> createState() =>
|
||||||
|
_TeacherReputationScorecardTabState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TeacherReputationScorecardTabState extends State<TeacherReputationScorecardTab> {
|
class _TeacherReputationScorecardTabState
|
||||||
|
extends State<TeacherReputationScorecardTab> {
|
||||||
final TeacherRepository _repository = TeacherRepository();
|
final TeacherRepository _repository = TeacherRepository();
|
||||||
bool _isLoading = true;
|
int _studentsEnrolled = 0;
|
||||||
int _studentsEnrolled = 83;
|
double _compositeMerit = 0;
|
||||||
double _compositeMerit = 96.5;
|
double _starRating = 0;
|
||||||
double _starRating = 4.92;
|
double _successRate = 0;
|
||||||
double _successRate = 98.2;
|
double _curriculumAlignment = 0;
|
||||||
double _curriculumAlignment = 0.96;
|
double _socraticInteraction = 0;
|
||||||
double _socraticInteraction = 0.89;
|
double _audioClarity = 0;
|
||||||
double _audioClarity = 0.94;
|
double _cognitiveFocus = 0;
|
||||||
double _cognitiveFocus = 0.92;
|
String _reputationTier = 'بانتظار بيانات الأداء';
|
||||||
String _reputationTier = 'معلم نخبوي معتمد 💎';
|
String _aiGuidance =
|
||||||
String _aiGuidance = 'توجيه الذكاء الاصطناعي الأسبوعي: نسبة الالتزام الوزاري ممتازة (96%). يُوصى بإضافة وقفة سقراطية استنتاجية في الدقيقة 14 من الدرس القادم لتعزيز تفاعل الطلبة.';
|
'ستظهر التوصية بعد توفر حصص محللة وتفاعل فعلي من الطلبة.';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -48,28 +50,38 @@ class _TeacherReputationScorecardTabState extends State<TeacherReputationScoreca
|
|||||||
final data = await _repository.getMyReputation();
|
final data = await _repository.getMyReputation();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_studentsEnrolled = (data['total_students_enrolled'] as num?)?.toInt() ?? 83;
|
_studentsEnrolled =
|
||||||
_compositeMerit = (data['composite_merit_score'] as num?)?.toDouble() ?? 96.5;
|
(data['total_students_enrolled'] as num?)?.toInt() ?? 0;
|
||||||
_starRating = (data['star_equivalent'] as num?)?.toDouble() ?? 4.92;
|
_compositeMerit =
|
||||||
_successRate = (data['success_rate_percentage'] as num?)?.toDouble() ?? 98.2;
|
(data['composite_merit_score'] as num?)?.toDouble() ?? 0;
|
||||||
_curriculumAlignment = ((data['curriculum_alignment_pct'] as num?)?.toDouble() ?? 96.0) / 100.0;
|
_starRating = (data['star_equivalent'] as num?)?.toDouble() ?? 0;
|
||||||
_socraticInteraction = ((data['socratic_interaction_pct'] as num?)?.toDouble() ?? 89.0) / 100.0;
|
_successRate =
|
||||||
_audioClarity = ((data['audio_clarity_pct'] as num?)?.toDouble() ?? 94.0) / 100.0;
|
(data['success_rate_percentage'] as num?)?.toDouble() ?? 0;
|
||||||
_cognitiveFocus = ((data['cognitive_focus_pct'] as num?)?.toDouble() ?? 92.0) / 100.0;
|
_curriculumAlignment =
|
||||||
_reputationTier = data['reputation_tier']?.toString() ?? 'معلم نخبوي معتمد 💎';
|
((data['curriculum_alignment_pct'] as num?)?.toDouble() ?? 0) /
|
||||||
|
100.0;
|
||||||
|
_socraticInteraction =
|
||||||
|
((data['socratic_interaction_pct'] as num?)?.toDouble() ?? 0) /
|
||||||
|
100.0;
|
||||||
|
_audioClarity =
|
||||||
|
((data['audio_clarity_pct'] as num?)?.toDouble() ?? 0) / 100.0;
|
||||||
|
_cognitiveFocus =
|
||||||
|
((data['cognitive_focus_pct'] as num?)?.toDouble() ?? 0) / 100.0;
|
||||||
|
_reputationTier =
|
||||||
|
data['reputation_tier']?.toString() ?? 'بانتظار بيانات الأداء';
|
||||||
_aiGuidance = data['ai_recommendation']?.toString() ?? _aiGuidance;
|
_aiGuidance = data['ai_recommendation']?.toString() ?? _aiGuidance;
|
||||||
_isLoading = false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
if (mounted) setState(() => _isLoading = false);
|
// Keep the honest zero-state when no reputation data is available.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final authState = context.watch<TeacherAuthCubit>().state;
|
final authState = context.watch<TeacherAuthCubit>().state;
|
||||||
final teacherName = (authState is TeacherAuthenticated) ? authState.profile.name : 'المهندس حمزة الغويريين';
|
final teacherName =
|
||||||
|
(authState is TeacherAuthenticated) ? authState.profile.name : 'المعلم';
|
||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: _loadReputation,
|
onRefresh: _loadReputation,
|
||||||
@@ -91,14 +103,16 @@ class _TeacherReputationScorecardTabState extends State<TeacherReputationScoreca
|
|||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
border: Border.all(color: const Color(0xFF8B5CF6).withOpacity(0.5)),
|
border:
|
||||||
|
Border.all(color: const Color(0xFF8B5CF6).withOpacity(0.5)),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const CircleAvatar(
|
const CircleAvatar(
|
||||||
radius: 32,
|
radius: 32,
|
||||||
backgroundColor: Color(0xFF8B5CF6),
|
backgroundColor: Color(0xFF8B5CF6),
|
||||||
child: Icon(CupertinoIcons.rosette, size: 36, color: Colors.white),
|
child: Icon(CupertinoIcons.rosette,
|
||||||
|
size: 36, color: Colors.white),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
@@ -111,7 +125,8 @@ class _TeacherReputationScorecardTabState extends State<TeacherReputationScoreca
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: TeacherTheme.emeraldPrimary.withOpacity(0.2),
|
color: TeacherTheme.emeraldPrimary.withOpacity(0.2),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
@@ -129,9 +144,15 @@ class _TeacherReputationScorecardTabState extends State<TeacherReputationScoreca
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
children: [
|
children: [
|
||||||
_ScoreStat(label: 'التقييم العام', value: '${_starRating.toStringAsFixed(2)} / 5.0'),
|
_ScoreStat(
|
||||||
_ScoreStat(label: 'الطلاب المخدومون', value: '$_studentsEnrolled طالب'),
|
label: 'التقييم العام',
|
||||||
_ScoreStat(label: 'نسبة النجاح', value: '${_successRate.toStringAsFixed(1)}%'),
|
value: '${_starRating.toStringAsFixed(2)} / 5.0'),
|
||||||
|
_ScoreStat(
|
||||||
|
label: 'الطلاب المخدومون',
|
||||||
|
value: '$_studentsEnrolled طالب'),
|
||||||
|
_ScoreStat(
|
||||||
|
label: 'نسبة النجاح',
|
||||||
|
value: '${_successRate.toStringAsFixed(1)}%'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -159,13 +180,17 @@ class _TeacherReputationScorecardTabState extends State<TeacherReputationScoreca
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
_metricProgress('الالتزام بالمنهاج والنتاجات الوزارية', _curriculumAlignment, TeacherTheme.emeraldPrimary),
|
_metricProgress('الالتزام بالمنهاج والنتاجات الوزارية',
|
||||||
|
_curriculumAlignment, TeacherTheme.emeraldPrimary),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_metricProgress('التفاعل السقراطي وإثارة التفكير', _socraticInteraction, TeacherTheme.cyberCyan),
|
_metricProgress('التفاعل السقراطي وإثارة التفكير',
|
||||||
|
_socraticInteraction, TeacherTheme.cyberCyan),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_metricProgress('الوضوح الصوتي وسلامة اللغة', _audioClarity, TeacherTheme.royalGold),
|
_metricProgress('الوضوح الصوتي وسلامة اللغة', _audioClarity,
|
||||||
|
TeacherTheme.royalGold),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_metricProgress('إدارة الوقت والتركيز الإدراكي (20-25 دقيقة)', _cognitiveFocus, const Color(0xFF8B5CF6)),
|
_metricProgress('إدارة الوقت والتركيز الإدراكي (20-25 دقيقة)',
|
||||||
|
_cognitiveFocus, const Color(0xFF8B5CF6)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -182,12 +207,14 @@ class _TeacherReputationScorecardTabState extends State<TeacherReputationScoreca
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Icon(CupertinoIcons.lightbulb_fill, color: TeacherTheme.royalGold, size: 20),
|
const Icon(CupertinoIcons.lightbulb_fill,
|
||||||
|
color: TeacherTheme.royalGold, size: 20),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
_aiGuidance,
|
_aiGuidance,
|
||||||
style: const TextStyle(fontSize: 12, color: Color(0xFFCBD5E1), height: 1.4),
|
style: const TextStyle(
|
||||||
|
fontSize: 12, color: Color(0xFFCBD5E1), height: 1.4),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -206,8 +233,11 @@ class _TeacherReputationScorecardTabState extends State<TeacherReputationScoreca
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(title, style: const TextStyle(fontSize: 12, color: Colors.white)),
|
Text(title,
|
||||||
Text('${(value * 100).toInt()}%', style: TextStyle(fontSize: 12.5, fontWeight: FontWeight.w800, color: color)),
|
style: const TextStyle(fontSize: 12, color: Colors.white)),
|
||||||
|
Text('${(value * 100).toInt()}%',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12.5, fontWeight: FontWeight.w800, color: color)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
@@ -235,9 +265,14 @@ class _ScoreStat extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Text(value, style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w900, color: Colors.white)),
|
Text(value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
color: Colors.white)),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(label, style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8))),
|
Text(label,
|
||||||
|
style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8))),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,103 @@ class TeacherStudioUploadTab extends StatefulWidget {
|
|||||||
State<TeacherStudioUploadTab> createState() => _TeacherStudioUploadTabState();
|
State<TeacherStudioUploadTab> createState() => _TeacherStudioUploadTabState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _CurriculumSelectors extends StatelessWidget {
|
||||||
|
final TeacherStudioState state;
|
||||||
|
final VoidCallback onLessonChanged;
|
||||||
|
const _CurriculumSelectors(
|
||||||
|
{required this.state, required this.onLessonChanged});
|
||||||
|
|
||||||
|
Map<String, dynamic> _map(dynamic value) =>
|
||||||
|
value is Map ? Map<String, dynamic>.from(value) : {};
|
||||||
|
String _name(dynamic value, String fallback) =>
|
||||||
|
value is Map ? value['name']?.toString() ?? fallback : fallback;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final subjects = _map(state.curriculumTree[state.gradeKey]?['subjects']);
|
||||||
|
final semesters = _map(subjects[state.subjectKey]?['semesters']);
|
||||||
|
final units = _map(semesters[state.semesterKey]?['units']);
|
||||||
|
final lessons = units[state.unitKey]?['lessons'] is List
|
||||||
|
? units[state.unitKey]['lessons'] as List
|
||||||
|
: const [];
|
||||||
|
if (state.isCurriculumLoading)
|
||||||
|
return const Center(
|
||||||
|
child:
|
||||||
|
CupertinoActivityIndicator(color: TeacherTheme.emeraldPrimary));
|
||||||
|
if (state.curriculumTree.isEmpty)
|
||||||
|
return const Text('لم تُرجع واجهة المنهاج أي صفوف متاحة.',
|
||||||
|
style: TextStyle(color: Color(0xFF94A3B8)));
|
||||||
|
return Wrap(
|
||||||
|
spacing: 10,
|
||||||
|
runSpacing: 10,
|
||||||
|
children: [
|
||||||
|
_drop(
|
||||||
|
'الصف',
|
||||||
|
state.gradeKey,
|
||||||
|
state.curriculumTree.entries
|
||||||
|
.map((e) => MapEntry(e.key, _name(e.value, e.key))),
|
||||||
|
(v) => context.read<TeacherStudioCubit>().selectGrade(v)),
|
||||||
|
_drop(
|
||||||
|
'المبحث',
|
||||||
|
state.subjectKey,
|
||||||
|
subjects.entries.map((e) => MapEntry(e.key, _name(e.value, e.key))),
|
||||||
|
(v) => context.read<TeacherStudioCubit>().selectSubject(v)),
|
||||||
|
_drop(
|
||||||
|
'الفصل',
|
||||||
|
state.semesterKey,
|
||||||
|
semesters.entries
|
||||||
|
.map((e) => MapEntry(e.key, _name(e.value, e.key))),
|
||||||
|
(v) => context.read<TeacherStudioCubit>().selectSemester(v)),
|
||||||
|
_drop(
|
||||||
|
'الوحدة',
|
||||||
|
state.unitKey,
|
||||||
|
units.entries.map((e) => MapEntry(e.key, _name(e.value, e.key))),
|
||||||
|
(v) => context.read<TeacherStudioCubit>().selectUnit(v)),
|
||||||
|
_drop(
|
||||||
|
'الدرس',
|
||||||
|
state.lessonKey,
|
||||||
|
lessons.whereType<Map>().map((e) => MapEntry(
|
||||||
|
e['id']?.toString() ?? '', e['title']?.toString() ?? '')), (v) {
|
||||||
|
context.read<TeacherStudioCubit>().selectLesson(v);
|
||||||
|
onLessonChanged();
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _drop(
|
||||||
|
String label,
|
||||||
|
String value,
|
||||||
|
Iterable<MapEntry<String, String>> entries,
|
||||||
|
ValueChanged<String> changed) {
|
||||||
|
final items = entries.where((e) => e.key.isNotEmpty).toList();
|
||||||
|
return SizedBox(
|
||||||
|
width: 260,
|
||||||
|
child: DropdownButtonFormField<String>(
|
||||||
|
value: items.any((e) => e.key == value) ? value : null,
|
||||||
|
isExpanded: true,
|
||||||
|
dropdownColor: const Color(0xFF161F30),
|
||||||
|
style: const TextStyle(color: Colors.white, fontSize: 12),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: label,
|
||||||
|
labelStyle: const TextStyle(color: Color(0xFF94A3B8)),
|
||||||
|
border:
|
||||||
|
OutlineInputBorder(borderRadius: BorderRadius.circular(10))),
|
||||||
|
items: items
|
||||||
|
.map((e) => DropdownMenuItem(
|
||||||
|
value: e.key,
|
||||||
|
child: Text(e.value, overflow: TextOverflow.ellipsis)))
|
||||||
|
.toList(),
|
||||||
|
onChanged: items.isEmpty
|
||||||
|
? null
|
||||||
|
: (v) {
|
||||||
|
if (v != null) changed(v);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
||||||
late final TextEditingController _titleController;
|
late final TextEditingController _titleController;
|
||||||
|
|
||||||
@@ -33,6 +130,13 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
_titleController = TextEditingController(
|
_titleController = TextEditingController(
|
||||||
text: context.read<TeacherStudioCubit>().state.lessonTitle,
|
text: context.read<TeacherStudioCubit>().state.lessonTitle,
|
||||||
);
|
);
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
if (!mounted) return;
|
||||||
|
await context.read<TeacherStudioCubit>().loadCurriculum();
|
||||||
|
if (mounted)
|
||||||
|
_titleController.text =
|
||||||
|
context.read<TeacherStudioCubit>().state.lessonTitle;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -42,21 +146,23 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _pickVideo(BuildContext ctx) async {
|
Future<void> _pickVideo(BuildContext ctx) async {
|
||||||
|
try {
|
||||||
final result = await FilePicker.platform.pickFiles(
|
final result = await FilePicker.platform.pickFiles(
|
||||||
type: FileType.custom,
|
type: FileType.custom,
|
||||||
allowedExtensions: const ['mp4', 'mov', 'webm'],
|
allowedExtensions: const ['mp4', 'mov', 'webm'],
|
||||||
withData: kIsWeb,
|
withData: kIsWeb);
|
||||||
);
|
|
||||||
if (!mounted || result == null || result.files.isEmpty) return;
|
if (!mounted || result == null || result.files.isEmpty) return;
|
||||||
|
|
||||||
final file = result.files.single;
|
final file = result.files.single;
|
||||||
ctx.read<TeacherStudioCubit>().selectVideoFile(
|
ctx.read<TeacherStudioCubit>().selectVideoFile(
|
||||||
file.name,
|
file.name,
|
||||||
file.size / (1024 * 1024),
|
file.size / (1024 * 1024),
|
||||||
ctx.read<TeacherStudioCubit>().state.durationMinutes,
|
ctx.read<TeacherStudioCubit>().state.durationMinutes,
|
||||||
path: file.path,
|
path: file.path,
|
||||||
bytes: file.bytes,
|
bytes: file.bytes);
|
||||||
);
|
} catch (e) {
|
||||||
|
if (mounted)
|
||||||
|
ctx.read<TeacherStudioCubit>().reportError('تعذر فتح الملف: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -80,14 +186,16 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(color: TeacherTheme.emeraldPrimary.withOpacity(0.4)),
|
border: Border.all(
|
||||||
|
color: TeacherTheme.emeraldPrimary.withOpacity(0.4)),
|
||||||
),
|
),
|
||||||
child: const Column(
|
child: const Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(CupertinoIcons.sparkles, color: TeacherTheme.emeraldLight, size: 20),
|
Icon(CupertinoIcons.sparkles,
|
||||||
|
color: TeacherTheme.emeraldLight, size: 20),
|
||||||
SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
'بوابة جودة حصص الأستوديو الرقمية (صَقِل 2.0)',
|
'بوابة جودة حصص الأستوديو الرقمية (صَقِل 2.0)',
|
||||||
@@ -135,19 +243,35 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextField(
|
TextField(
|
||||||
controller: _titleController,
|
controller: _titleController,
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 13.5),
|
style:
|
||||||
onChanged: (val) => context.read<TeacherStudioCubit>().updateLessonTitle(val),
|
const TextStyle(color: Colors.white, fontSize: 13.5),
|
||||||
|
onChanged: (val) => context
|
||||||
|
.read<TeacherStudioCubit>()
|
||||||
|
.updateLessonTitle(val),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFF161F30),
|
fillColor: const Color(0xFF161F30),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
borderSide: const BorderSide(color: Color(0xFF334155)),
|
borderSide:
|
||||||
|
const BorderSide(color: Color(0xFF334155)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
_CurriculumSelectors(
|
||||||
|
state: state,
|
||||||
|
onLessonChanged: () {
|
||||||
|
final title = context
|
||||||
|
.read<TeacherStudioCubit>()
|
||||||
|
.state
|
||||||
|
.lessonTitle;
|
||||||
|
_titleController.text = title;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Video Attachment Dropzone Card
|
// Video Attachment Dropzone Card
|
||||||
const Text(
|
const Text(
|
||||||
'ملف فيديو الحصة للشرح الرقمي (1080p):',
|
'ملف فيديو الحصة للشرح الرقمي (1080p):',
|
||||||
@@ -164,7 +288,9 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
color: const Color(0xFF161F30),
|
color: const Color(0xFF161F30),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: state.selectedFileName != null ? TeacherTheme.emeraldPrimary : const Color(0xFF334155),
|
color: state.selectedFileName != null
|
||||||
|
? TeacherTheme.emeraldPrimary
|
||||||
|
: const Color(0xFF334155),
|
||||||
width: state.selectedFileName != null ? 1.5 : 1.0,
|
width: state.selectedFileName != null ? 1.5 : 1.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -174,12 +300,17 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
width: 44,
|
width: 44,
|
||||||
height: 44,
|
height: 44,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: (state.selectedFileName != null ? TeacherTheme.emeraldPrimary : const Color(0xFF334155)).withOpacity(0.2),
|
color: (state.selectedFileName != null
|
||||||
|
? TeacherTheme.emeraldPrimary
|
||||||
|
: const Color(0xFF334155))
|
||||||
|
.withOpacity(0.2),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
CupertinoIcons.videocam_fill,
|
CupertinoIcons.videocam_fill,
|
||||||
color: state.selectedFileName != null ? TeacherTheme.emeraldLight : const Color(0xFF94A3B8),
|
color: state.selectedFileName != null
|
||||||
|
? TeacherTheme.emeraldLight
|
||||||
|
: const Color(0xFF94A3B8),
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -189,29 +320,44 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
state.selectedFileName ?? 'لم يتم اختيار ملف فيديو بعد',
|
state.selectedFileName ??
|
||||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13),
|
'لم يتم اختيار ملف فيديو بعد',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 13),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
state.selectedFileName != null
|
state.selectedFileName != null
|
||||||
? 'الحجم: ${state.selectedFileSizeMb ?? 48.2} MB • المدة المستخرجة: ${state.durationMinutes.toStringAsFixed(1)} دقيقة'
|
? 'الحجم: ${state.selectedFileSizeMb?.toStringAsFixed(2)} MB • أدخل المدة الفعلية أدناه'
|
||||||
: 'الصيغ المدعومة: MP4, MOV, WebM (جودة 1080p)',
|
: 'الصيغ المدعومة: MP4, MOV, WebM (جودة 1080p)',
|
||||||
style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 11),
|
style: const TextStyle(
|
||||||
|
color: Color(0xFF94A3B8), fontSize: 11),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () => _pickVideo(context),
|
onPressed: () => _pickVideo(context),
|
||||||
icon: const Icon(CupertinoIcons.folder_badge_plus, size: 15),
|
icon: const Icon(CupertinoIcons.folder_badge_plus,
|
||||||
label: Text(state.selectedFileName != null ? 'تغيير الملف' : 'اختيار ملف 📁'),
|
size: 15),
|
||||||
|
label: Text(state.selectedFileName != null
|
||||||
|
? 'تغيير الملف'
|
||||||
|
: 'اختيار ملف 📁'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: state.selectedFileName != null ? const Color(0xFF1E293B) : TeacherTheme.emeraldPrimary,
|
backgroundColor: state.selectedFileName != null
|
||||||
foregroundColor: state.selectedFileName != null ? Colors.white : Colors.black,
|
? const Color(0xFF1E293B)
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
: TeacherTheme.emeraldPrimary,
|
||||||
textStyle: const TextStyle(fontSize: 11.5, fontWeight: FontWeight.bold),
|
foregroundColor: state.selectedFileName != null
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
? 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)),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -232,7 +378,8 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 10, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isDurationGood
|
color: isDurationGood
|
||||||
? TeacherTheme.emeraldPrimary.withOpacity(0.2)
|
? TeacherTheme.emeraldPrimary.withOpacity(0.2)
|
||||||
@@ -244,7 +391,9 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
color: isDurationGood ? TeacherTheme.emeraldLight : TeacherTheme.crimsonRed,
|
color: isDurationGood
|
||||||
|
? TeacherTheme.emeraldLight
|
||||||
|
: TeacherTheme.crimsonRed,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -255,7 +404,9 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
min: 5.0,
|
min: 5.0,
|
||||||
max: 45.0,
|
max: 45.0,
|
||||||
divisions: 40,
|
divisions: 40,
|
||||||
activeColor: isDurationGood ? TeacherTheme.emeraldPrimary : TeacherTheme.crimsonRed,
|
activeColor: isDurationGood
|
||||||
|
? TeacherTheme.emeraldPrimary
|
||||||
|
: TeacherTheme.crimsonRed,
|
||||||
inactiveColor: TeacherTheme.surfaceBorder,
|
inactiveColor: TeacherTheme.surfaceBorder,
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
context.read<TeacherStudioCubit>().updateDuration(val);
|
context.read<TeacherStudioCubit>().updateDuration(val);
|
||||||
@@ -283,7 +434,9 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
? CupertinoIcons.check_mark_circled_solid
|
? CupertinoIcons.check_mark_circled_solid
|
||||||
: CupertinoIcons.exclamationmark_triangle_fill,
|
: CupertinoIcons.exclamationmark_triangle_fill,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: isDurationGood ? TeacherTheme.emeraldLight : TeacherTheme.crimsonRed,
|
color: isDurationGood
|
||||||
|
? TeacherTheme.emeraldLight
|
||||||
|
: TeacherTheme.crimsonRed,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -293,7 +446,9 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
: 'تحذير إدراكي: الشرح يتجاوز 25 دقيقة! ينخفض استيعاب الطلبة تدريجياً، يرجى اختصار المقطع أو تقسيمه.',
|
: 'تحذير إدراكي: الشرح يتجاوز 25 دقيقة! ينخفض استيعاب الطلبة تدريجياً، يرجى اختصار المقطع أو تقسيمه.',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11.5,
|
fontSize: 11.5,
|
||||||
color: isDurationGood ? const Color(0xFF6EE7B7) : const Color(0xFFFCA5A5),
|
color: isDurationGood
|
||||||
|
? const Color(0xFF6EE7B7)
|
||||||
|
: const Color(0xFFFCA5A5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -307,20 +462,25 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
onPressed: state.isAuditing
|
onPressed: state.isAuditing
|
||||||
? null
|
? null
|
||||||
: () {
|
: () {
|
||||||
context.read<TeacherStudioCubit>().runQualityGate();
|
context
|
||||||
|
.read<TeacherStudioCubit>()
|
||||||
|
.runQualityGate();
|
||||||
},
|
},
|
||||||
icon: state.isAuditing
|
icon: state.isAuditing
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
child: CircularProgressIndicator(strokeWidth: 2, color: Colors.black),
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2, color: Colors.black),
|
||||||
)
|
)
|
||||||
: const Icon(CupertinoIcons.checkmark_shield_fill, size: 18),
|
: const Icon(CupertinoIcons.checkmark_shield_fill,
|
||||||
|
size: 18),
|
||||||
label: Text(
|
label: Text(
|
||||||
state.isAuditing
|
state.isAuditing
|
||||||
? 'جاري التدقيق التربوي عبر محرك صَقِل...'
|
? 'جاري التدقيق التربوي عبر محرك صَقِل...'
|
||||||
: 'فحص الحصة عبر بوابة الجودة الذكية (عتبة 85%) ⚡',
|
: 'فحص الحصة عبر بوابة الجودة الذكية (عتبة 85%) ⚡',
|
||||||
style: const TextStyle(fontSize: 13.5, fontWeight: FontWeight.w800),
|
style: const TextStyle(
|
||||||
|
fontSize: 13.5, fontWeight: FontWeight.w800),
|
||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: TeacherTheme.emeraldPrimary,
|
backgroundColor: TeacherTheme.emeraldPrimary,
|
||||||
@@ -331,13 +491,22 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (state.errorMessage != null) ...[
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(state.errorMessage!,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: TeacherTheme.crimsonRed,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w700)),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Quality Audit Result Card
|
// Quality Audit Result Card
|
||||||
if (state.auditResult != null) _buildAuditResultCard(context, state, state.auditResult!),
|
if (state.auditResult != null)
|
||||||
|
_buildAuditResultCard(context, state, state.auditResult!),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -345,7 +514,8 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAuditResultCard(BuildContext context, TeacherStudioState state, TeacherLessonAuditModel res) {
|
Widget _buildAuditResultCard(BuildContext context, TeacherStudioState state,
|
||||||
|
TeacherLessonAuditModel res) {
|
||||||
final int score = res.qualityScore;
|
final int score = res.qualityScore;
|
||||||
final bool isApproved = score >= 85;
|
final bool isApproved = score >= 85;
|
||||||
|
|
||||||
@@ -355,7 +525,9 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
color: TeacherTheme.surfaceCard,
|
color: TeacherTheme.surfaceCard,
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isApproved ? TeacherTheme.emeraldPrimary : TeacherTheme.crimsonRed,
|
color: isApproved
|
||||||
|
? TeacherTheme.emeraldPrimary
|
||||||
|
: TeacherTheme.crimsonRed,
|
||||||
width: 1.5,
|
width: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -368,23 +540,32 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
isApproved ? CupertinoIcons.checkmark_seal_fill : CupertinoIcons.xmark_seal_fill,
|
isApproved
|
||||||
color: isApproved ? TeacherTheme.emeraldPrimary : TeacherTheme.crimsonRed,
|
? CupertinoIcons.checkmark_seal_fill
|
||||||
|
: CupertinoIcons.xmark_seal_fill,
|
||||||
|
color: isApproved
|
||||||
|
? TeacherTheme.emeraldPrimary
|
||||||
|
: TeacherTheme.crimsonRed,
|
||||||
size: 22,
|
size: 22,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
isApproved ? 'جاهزة لرفع الملف والتحليل' : 'بيانات الحصة بحاجة للمراجعة',
|
isApproved
|
||||||
|
? 'جاهزة لرفع الملف والتحليل'
|
||||||
|
: 'بيانات الحصة بحاجة للمراجعة',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
color: isApproved ? TeacherTheme.emeraldPrimary : TeacherTheme.crimsonRed,
|
color: isApproved
|
||||||
|
? TeacherTheme.emeraldPrimary
|
||||||
|
: TeacherTheme.crimsonRed,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isApproved
|
color: isApproved
|
||||||
? TeacherTheme.emeraldPrimary.withOpacity(0.2)
|
? TeacherTheme.emeraldPrimary.withOpacity(0.2)
|
||||||
@@ -396,7 +577,9 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w900,
|
fontWeight: FontWeight.w900,
|
||||||
color: isApproved ? TeacherTheme.emeraldLight : TeacherTheme.crimsonRed,
|
color: isApproved
|
||||||
|
? TeacherTheme.emeraldLight
|
||||||
|
: TeacherTheme.crimsonRed,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -419,7 +602,8 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_rowMetric('نقاء الصوت ومخارج الحروف:', res.audioClarity),
|
_rowMetric('نقاء الصوت ومخارج الحروف:', res.audioClarity),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_rowMetric('الفواصل السقراطية التفاعلية:', '${res.socraticStopsCount} محطات تفكيرية إلزامية'),
|
_rowMetric('الفواصل السقراطية التفاعلية:',
|
||||||
|
'${res.socraticStopsCount} محطات تفكيرية إلزامية'),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
if (isApproved) ...[
|
if (isApproved) ...[
|
||||||
@@ -430,11 +614,19 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
child: const Row(
|
child: const Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(width: 18, height: 18, child: CircularProgressIndicator(color: TeacherTheme.emeraldPrimary, strokeWidth: 2)),
|
SizedBox(
|
||||||
|
width: 18,
|
||||||
|
height: 18,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
color: TeacherTheme.emeraldPrimary,
|
||||||
|
strokeWidth: 2)),
|
||||||
SizedBox(width: 12),
|
SizedBox(width: 12),
|
||||||
Text(
|
Text(
|
||||||
'جاري رفع الفيديو وبدء معالجة HLS والتحليل السقراطي... ⏳',
|
'جاري رفع الفيديو وبدء معالجة HLS والتحليل السقراطي... ⏳',
|
||||||
style: TextStyle(color: TeacherTheme.emeraldLight, fontSize: 12.5, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
color: TeacherTheme.emeraldLight,
|
||||||
|
fontSize: 12.5,
|
||||||
|
fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -449,20 +641,26 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(CupertinoIcons.checkmark_seal_fill, color: TeacherTheme.emeraldLight, size: 22),
|
const Icon(CupertinoIcons.checkmark_seal_fill,
|
||||||
|
color: TeacherTheme.emeraldLight, size: 22),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
state.uploadSuccessMessage ?? 'تم رفع ونشر الحصة بنجاح في المنهاج وسوق صَقِل! 🚀',
|
state.uploadSuccessMessage ??
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w900),
|
'تم رفع ونشر الحصة بنجاح في المنهاج وسوق صَقِل! 🚀',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w900),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
const Text(
|
const Text(
|
||||||
'الحصة متاحة الآن لطلبة الصف العاشر وسوق التسييل التجاري 🎖️',
|
'الحصة متاحة الآن لطلبة الصف العاشر وسوق التسييل التجاري 🎖️',
|
||||||
style: TextStyle(color: TeacherTheme.emeraldLight, fontSize: 11),
|
style: TextStyle(
|
||||||
|
color: TeacherTheme.emeraldLight, fontSize: 11),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -476,12 +674,15 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
context.read<TeacherStudioCubit>().uploadAndPublishLesson();
|
context.read<TeacherStudioCubit>().uploadAndPublishLesson();
|
||||||
},
|
},
|
||||||
icon: const Icon(CupertinoIcons.cloud_upload_fill, size: 18),
|
icon: const Icon(CupertinoIcons.cloud_upload_fill, size: 18),
|
||||||
label: const Text('رفع ونشر الحصة في المنهاج وسوق التسييل 🚀', style: TextStyle(fontSize: 13.5, fontWeight: FontWeight.w900)),
|
label: const Text('رفع ونشر الحصة في المنهاج وسوق التسييل 🚀',
|
||||||
|
style:
|
||||||
|
TextStyle(fontSize: 13.5, fontWeight: FontWeight.w900)),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: TeacherTheme.emeraldDark,
|
backgroundColor: TeacherTheme.emeraldDark,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
minimumSize: const Size(double.infinity, 46),
|
minimumSize: const Size(double.infinity, 46),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12)),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -495,8 +696,13 @@ class _TeacherStudioUploadTabState extends State<TeacherStudioUploadTab> {
|
|||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF94A3B8))),
|
Text(label,
|
||||||
Text('$value', style: const TextStyle(fontSize: 12.5, fontWeight: FontWeight.w700, color: Color(0xFFCBD5E1))),
|
style: const TextStyle(fontSize: 12, color: Color(0xFF94A3B8))),
|
||||||
|
Text('$value',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12.5,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: Color(0xFFCBD5E1))),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,7 +32,8 @@ class TeacherMainShell extends StatefulWidget {
|
|||||||
class _TeacherMainShellState extends State<TeacherMainShell> {
|
class _TeacherMainShellState extends State<TeacherMainShell> {
|
||||||
int _currentTabIndex = 0;
|
int _currentTabIndex = 0;
|
||||||
|
|
||||||
void _openEditProfileDialog(BuildContext context, TeacherProfileModel profile) {
|
void _openEditProfileDialog(
|
||||||
|
BuildContext context, TeacherProfileModel profile) {
|
||||||
final nameCtrl = TextEditingController(text: profile.name);
|
final nameCtrl = TextEditingController(text: profile.name);
|
||||||
final subjectCtrl = TextEditingController(text: profile.specialization);
|
final subjectCtrl = TextEditingController(text: profile.specialization);
|
||||||
final schoolCtrl = TextEditingController(text: profile.schoolName);
|
final schoolCtrl = TextEditingController(text: profile.schoolName);
|
||||||
@@ -64,7 +65,9 @@ class _TeacherMainShellState extends State<TeacherMainShell> {
|
|||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: TeacherTheme.surfaceDark,
|
color: TeacherTheme.surfaceDark,
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
|
||||||
border: Border(top: BorderSide(color: TeacherTheme.emeraldPrimary, width: 2)),
|
border: Border(
|
||||||
|
top:
|
||||||
|
BorderSide(color: TeacherTheme.emeraldPrimary, width: 2)),
|
||||||
),
|
),
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -73,37 +76,56 @@ class _TeacherMainShellState extends State<TeacherMainShell> {
|
|||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(CupertinoIcons.pencil_circle_fill, color: TeacherTheme.emeraldPrimary, size: 28),
|
const Icon(CupertinoIcons.pencil_circle_fill,
|
||||||
|
color: TeacherTheme.emeraldPrimary, size: 28),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
const Text(
|
const Text(
|
||||||
'تحديث بيانات المعلم والصفوف والمباحث 📋',
|
'تحديث بيانات المعلم والصفوف والمباحث 📋',
|
||||||
style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w800),
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w800),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => Navigator.of(ctx).pop(),
|
onPressed: () => Navigator.of(ctx).pop(),
|
||||||
icon: const Icon(CupertinoIcons.xmark_circle_fill, color: Colors.white54, size: 20),
|
icon: const Icon(CupertinoIcons.xmark_circle_fill,
|
||||||
|
color: Colors.white54, size: 20),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
const Text('الاسم المعتمد:',
|
||||||
const Text('الاسم المعتمد:', style: TextStyle(color: Colors.white70, fontSize: 12, fontWeight: FontWeight.bold)),
|
style: TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_buildInputBox(controller: nameCtrl, hint: 'الاسم الكامل'),
|
_buildInputBox(controller: nameCtrl, hint: 'الاسم الكامل'),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
const Text('المادة التخصصية:',
|
||||||
const Text('المادة التخصصية:', style: TextStyle(color: Colors.white70, fontSize: 12, fontWeight: FontWeight.bold)),
|
style: TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_buildInputBox(controller: subjectCtrl, hint: 'المادة (مثال: الفيزياء)'),
|
_buildInputBox(
|
||||||
|
controller: subjectCtrl, hint: 'المادة (مثال: الفيزياء)'),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
const Text('المدرسة / المديرية:',
|
||||||
const Text('المدرسة / المديرية:', style: TextStyle(color: Colors.white70, fontSize: 12, fontWeight: FontWeight.bold)),
|
style: TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
_buildInputBox(controller: schoolCtrl, hint: 'المدرسة التابع لها'),
|
_buildInputBox(
|
||||||
|
controller: schoolCtrl, hint: 'المدرسة التابع لها'),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
|
const Text('الصفوف والشعب التي تدرسها في المنصة:',
|
||||||
const Text('الصفوف والشعب التي تدرسها في المنصة:', style: TextStyle(color: Colors.white70, fontSize: 12, fontWeight: FontWeight.bold)),
|
style: TextStyle(
|
||||||
|
color: Colors.white70,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Wrap(
|
Wrap(
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
@@ -112,11 +134,18 @@ class _TeacherMainShellState extends State<TeacherMainShell> {
|
|||||||
final isSelected = selectedGrades.contains(grade);
|
final isSelected = selectedGrades.contains(grade);
|
||||||
return FilterChip(
|
return FilterChip(
|
||||||
selected: isSelected,
|
selected: isSelected,
|
||||||
label: Text(grade, style: TextStyle(color: isSelected ? Colors.black : Colors.white, fontSize: 11.5, fontWeight: FontWeight.bold)),
|
label: Text(grade,
|
||||||
|
style: TextStyle(
|
||||||
|
color: isSelected ? Colors.black : Colors.white,
|
||||||
|
fontSize: 11.5,
|
||||||
|
fontWeight: FontWeight.bold)),
|
||||||
selectedColor: TeacherTheme.emeraldPrimary,
|
selectedColor: TeacherTheme.emeraldPrimary,
|
||||||
backgroundColor: TeacherTheme.surfaceCard,
|
backgroundColor: TeacherTheme.surfaceCard,
|
||||||
checkmarkColor: Colors.black,
|
checkmarkColor: Colors.black,
|
||||||
side: BorderSide(color: isSelected ? TeacherTheme.emeraldPrimary : TeacherTheme.surfaceBorder),
|
side: BorderSide(
|
||||||
|
color: isSelected
|
||||||
|
? TeacherTheme.emeraldPrimary
|
||||||
|
: TeacherTheme.surfaceBorder),
|
||||||
onSelected: (selected) {
|
onSelected: (selected) {
|
||||||
setModalState(() {
|
setModalState(() {
|
||||||
if (selected) {
|
if (selected) {
|
||||||
@@ -132,7 +161,6 @@ class _TeacherMainShellState extends State<TeacherMainShell> {
|
|||||||
}).toList(),
|
}).toList(),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<TeacherAuthCubit>().updateProfile(
|
context.read<TeacherAuthCubit>().updateProfile(
|
||||||
@@ -143,15 +171,19 @@ class _TeacherMainShellState extends State<TeacherMainShell> {
|
|||||||
bio: bioCtrl.text.trim(),
|
bio: bioCtrl.text.trim(),
|
||||||
);
|
);
|
||||||
Navigator.of(ctx).pop();
|
Navigator.of(ctx).pop();
|
||||||
SaqelToast.showSuccess(context, 'تم تحديث بيانات المعلم والصفوف بنجاح ✨');
|
SaqelToast.showSuccess(
|
||||||
|
context, 'تم تحديث بيانات المعلم والصفوف بنجاح ✨');
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: TeacherTheme.emeraldPrimary,
|
backgroundColor: TeacherTheme.emeraldPrimary,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12)),
|
||||||
),
|
),
|
||||||
child: const Text('حفظ التعديلات واعتماد الملف 💾', style: TextStyle(fontSize: 13.5, fontWeight: FontWeight.w900)),
|
child: const Text('حفظ التعديلات واعتماد الملف 💾',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13.5, fontWeight: FontWeight.w900)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -162,7 +194,8 @@ class _TeacherMainShellState extends State<TeacherMainShell> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildInputBox({required TextEditingController controller, required String hint}) {
|
Widget _buildInputBox(
|
||||||
|
{required TextEditingController controller, required String hint}) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 2),
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 2),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -173,7 +206,10 @@ class _TeacherMainShellState extends State<TeacherMainShell> {
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 13),
|
style: const TextStyle(color: Colors.white, fontSize: 13),
|
||||||
decoration: InputDecoration(border: InputBorder.none, hintText: hint, hintStyle: const TextStyle(color: Color(0xFF64748B), fontSize: 12)),
|
decoration: InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintText: hint,
|
||||||
|
hintStyle: const TextStyle(color: Color(0xFF64748B), fontSize: 12)),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -185,7 +221,8 @@ class _TeacherMainShellState extends State<TeacherMainShell> {
|
|||||||
textDirection: TextDirection.rtl,
|
textDirection: TextDirection.rtl,
|
||||||
child: CupertinoAlertDialog(
|
child: CupertinoAlertDialog(
|
||||||
title: const Text('تسجيل الخروج من استوديو المعلم 🚪'),
|
title: const Text('تسجيل الخروج من استوديو المعلم 🚪'),
|
||||||
content: const Text('هل أنت متأكد من رغبتك في إنهاء جلسة العمل الحالية؟'),
|
content:
|
||||||
|
const Text('هل أنت متأكد من رغبتك في إنهاء جلسة العمل الحالية؟'),
|
||||||
actions: [
|
actions: [
|
||||||
CupertinoDialogAction(
|
CupertinoDialogAction(
|
||||||
onPressed: () => Navigator.of(ctx).pop(),
|
onPressed: () => Navigator.of(ctx).pop(),
|
||||||
@@ -224,7 +261,9 @@ class _TeacherMainShellState extends State<TeacherMainShell> {
|
|||||||
profile = authState.profile;
|
profile = authState.profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
final initialLetter = profile.name.isNotEmpty ? profile.name.trim().characters.first : 'م';
|
final initialLetter = profile.name.isNotEmpty
|
||||||
|
? profile.name.trim().characters.first
|
||||||
|
: 'م';
|
||||||
|
|
||||||
return Directionality(
|
return Directionality(
|
||||||
textDirection: TextDirection.rtl,
|
textDirection: TextDirection.rtl,
|
||||||
@@ -242,7 +281,10 @@ class _TeacherMainShellState extends State<TeacherMainShell> {
|
|||||||
height: 38,
|
height: 38,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: const LinearGradient(
|
gradient: const LinearGradient(
|
||||||
colors: [TeacherTheme.emeraldPrimary, TeacherTheme.emeraldDark],
|
colors: [
|
||||||
|
TeacherTheme.emeraldPrimary,
|
||||||
|
TeacherTheme.emeraldDark
|
||||||
|
],
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
@@ -276,12 +318,14 @@ class _TeacherMainShellState extends State<TeacherMainShell> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
const Icon(CupertinoIcons.checkmark_seal_fill, color: TeacherTheme.emeraldPrimary, size: 15),
|
const Icon(CupertinoIcons.checkmark_seal_fill,
|
||||||
|
color: TeacherTheme.emeraldPrimary, size: 15),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'${profile.specialization} · ${profile.gradesTaught.join("، ")}',
|
'${profile.specialization} · ${profile.gradesTaught.join("، ")}',
|
||||||
style: const TextStyle(fontSize: 10.5, color: Color(0xFF94A3B8)),
|
style: const TextStyle(
|
||||||
|
fontSize: 10.5, color: Color(0xFF94A3B8)),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -294,13 +338,15 @@ class _TeacherMainShellState extends State<TeacherMainShell> {
|
|||||||
// Edit Profile Button
|
// Edit Profile Button
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'تعديل بيانات المعلم والصفوف',
|
tooltip: 'تعديل بيانات المعلم والصفوف',
|
||||||
icon: const Icon(CupertinoIcons.slider_horizontal_3, color: TeacherTheme.emeraldPrimary, size: 20),
|
icon: const Icon(CupertinoIcons.slider_horizontal_3,
|
||||||
|
color: TeacherTheme.emeraldPrimary, size: 20),
|
||||||
onPressed: () => _openEditProfileDialog(context, profile),
|
onPressed: () => _openEditProfileDialog(context, profile),
|
||||||
),
|
),
|
||||||
// Logout Button
|
// Logout Button
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'تسجيل الخروج',
|
tooltip: 'تسجيل الخروج',
|
||||||
icon: const Icon(CupertinoIcons.square_arrow_right, color: Color(0xFFFF453A), size: 20),
|
icon: const Icon(CupertinoIcons.square_arrow_right,
|
||||||
|
color: Color(0xFFFF453A), size: 20),
|
||||||
onPressed: () => _confirmLogout(context),
|
onPressed: () => _confirmLogout(context),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -317,7 +363,8 @@ class _TeacherMainShellState extends State<TeacherMainShell> {
|
|||||||
bottomNavigationBar: Container(
|
bottomNavigationBar: Container(
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: TeacherTheme.surfaceDark,
|
color: TeacherTheme.surfaceDark,
|
||||||
border: Border(top: BorderSide(color: TeacherTheme.surfaceBorder)),
|
border:
|
||||||
|
Border(top: BorderSide(color: TeacherTheme.surfaceBorder)),
|
||||||
),
|
),
|
||||||
child: BottomNavigationBar(
|
child: BottomNavigationBar(
|
||||||
currentIndex: _currentTabIndex,
|
currentIndex: _currentTabIndex,
|
||||||
@@ -325,7 +372,8 @@ class _TeacherMainShellState extends State<TeacherMainShell> {
|
|||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
selectedItemColor: TeacherTheme.emeraldPrimary,
|
selectedItemColor: TeacherTheme.emeraldPrimary,
|
||||||
unselectedItemColor: const Color(0xFF64748B),
|
unselectedItemColor: const Color(0xFF64748B),
|
||||||
selectedLabelStyle: const TextStyle(fontWeight: FontWeight.w800, fontSize: 11),
|
selectedLabelStyle:
|
||||||
|
const TextStyle(fontWeight: FontWeight.w800, fontSize: 11),
|
||||||
unselectedLabelStyle: const TextStyle(fontSize: 10.5),
|
unselectedLabelStyle: const TextStyle(fontSize: 10.5),
|
||||||
type: BottomNavigationBarType.fixed,
|
type: BottomNavigationBarType.fixed,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
|
|||||||
@@ -10,5 +10,7 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.server</key>
|
<key>com.apple.security.network.server</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -8,5 +8,7 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>com.apple.security.network.server</key>
|
<key>com.apple.security.network.server</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.files.user-selected.read-write</key>
|
||||||
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class VideoController
|
|||||||
$courseId = (int)($request->getBody()['course_id'] ?? $_POST['course_id'] ?? 0);
|
$courseId = (int)($request->getBody()['course_id'] ?? $_POST['course_id'] ?? 0);
|
||||||
$title = trim((string)($request->getBody()['title'] ?? $_POST['title'] ?? ''));
|
$title = trim((string)($request->getBody()['title'] ?? $_POST['title'] ?? ''));
|
||||||
$seqOrder = (int)($request->getBody()['sequence_order'] ?? $_POST['sequence_order'] ?? 1);
|
$seqOrder = (int)($request->getBody()['sequence_order'] ?? $_POST['sequence_order'] ?? 1);
|
||||||
|
$curriculumKey = trim((string)($request->getBody()['curriculum_key'] ?? $_POST['curriculum_key'] ?? ''));
|
||||||
|
|
||||||
if (empty($title)) {
|
if (empty($title)) {
|
||||||
$response->status(400)->json([
|
$response->status(400)->json([
|
||||||
@@ -64,13 +65,19 @@ class VideoController
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$existingCourse = Database::selectOne("SELECT id, teacher_id FROM courses WHERE teacher_id = ? LIMIT 1", [$request->user_id]);
|
$subjectName = trim((string)($request->getBody()['subject'] ?? $_POST['subject'] ?? ''));
|
||||||
|
$gradeLevel = trim((string)($request->getBody()['grade_level'] ?? $_POST['grade_level'] ?? ''));
|
||||||
|
$existingCourse = Database::selectOne(
|
||||||
|
"SELECT c.id, c.teacher_id FROM courses c
|
||||||
|
JOIN subjects s ON s.id = c.subject_id
|
||||||
|
WHERE c.teacher_id = ? AND c.grade_level = ? AND (s.name_ar = ? OR s.name_en = ?)
|
||||||
|
LIMIT 1",
|
||||||
|
[$request->user_id, $gradeLevel, $subjectName, $subjectName]
|
||||||
|
);
|
||||||
if ($existingCourse) {
|
if ($existingCourse) {
|
||||||
$courseId = (int)$existingCourse['id'];
|
$courseId = (int)$existingCourse['id'];
|
||||||
$course = $existingCourse;
|
$course = $existingCourse;
|
||||||
} else {
|
} else {
|
||||||
$subjectName = trim((string)($request->getBody()['subject'] ?? $_POST['subject'] ?? ''));
|
|
||||||
$gradeLevel = trim((string)($request->getBody()['grade_level'] ?? $_POST['grade_level'] ?? ''));
|
|
||||||
$subject = Database::selectOne(
|
$subject = Database::selectOne(
|
||||||
"SELECT id FROM subjects WHERE name_ar = ? OR name_en = ? ORDER BY id LIMIT 1",
|
"SELECT id FROM subjects WHERE name_ar = ? OR name_en = ? ORDER BY id LIMIT 1",
|
||||||
[$subjectName, $subjectName]
|
[$subjectName, $subjectName]
|
||||||
@@ -121,11 +128,12 @@ class VideoController
|
|||||||
|
|
||||||
// Insert lesson record with HLS references
|
// Insert lesson record with HLS references
|
||||||
$lessonId = Database::insert(
|
$lessonId = Database::insert(
|
||||||
"INSERT INTO lessons (course_id, title, sequence_order, storage_type, video_uuid, bunny_video_id, local_path, hls_url, thumbnail_url, duration_seconds, is_free_preview, encoding_status)
|
"INSERT INTO lessons (course_id, title, curriculum_key, sequence_order, storage_type, video_uuid, bunny_video_id, local_path, hls_url, thumbnail_url, duration_seconds, is_free_preview, encoding_status)
|
||||||
VALUES (?, ?, ?, 'api_upload', ?, '', ?, ?, ?, ?, 0, 'ready')",
|
VALUES (?, ?, ?, ?, 'api_upload', ?, '', ?, ?, ?, ?, 0, 'ready')",
|
||||||
[
|
[
|
||||||
$courseId,
|
$courseId,
|
||||||
$title,
|
$title,
|
||||||
|
$curriculumKey !== '' ? $curriculumKey : null,
|
||||||
$seqOrder,
|
$seqOrder,
|
||||||
$uploadResult['video_uuid'],
|
$uploadResult['video_uuid'],
|
||||||
$uploadResult['local_path'],
|
$uploadResult['local_path'],
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ class AiVideoAnalyzerService
|
|||||||
$report = json_decode($text, true);
|
$report = json_decode($text, true);
|
||||||
if ($code !== 200 || !is_array($report) || !in_array($report['decision'] ?? '', ['approved','needs_manual_review','rejected'], true)) throw new \RuntimeException('استجابة Gemini غير صالحة');
|
if ($code !== 200 || !is_array($report) || !in_array($report['decision'] ?? '', ['approved','needs_manual_review','rejected'], true)) throw new \RuntimeException('استجابة Gemini غير صالحة');
|
||||||
$report['duration_seconds'] = $duration;
|
$report['duration_seconds'] = $duration;
|
||||||
if (($report['visual_clarity_score'] ?? 0) < 70 || ($report['pedagogical_readiness_score'] ?? 0) < 70) $report['decision'] = 'needs_manual_review';
|
if (($report['visual_clarity_score'] ?? 0) < 85 || ($report['pedagogical_readiness_score'] ?? 0) < 85) $report['decision'] = 'needs_manual_review';
|
||||||
return $report;
|
return $report;
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
return ['decision' => 'needs_manual_review', 'reason' => 'تعذر إكمال تحليل Gemini؛ لم يتم رفع الفيديو'];
|
return ['decision' => 'needs_manual_review', 'reason' => 'تعذر إكمال تحليل Gemini؛ لم يتم رفع الفيديو'];
|
||||||
|
|||||||
@@ -52,19 +52,19 @@ class TeacherRatingService
|
|||||||
Database::query("
|
Database::query("
|
||||||
CREATE TABLE teacher_performance_metrics (
|
CREATE TABLE teacher_performance_metrics (
|
||||||
teacher_id BIGINT UNSIGNED PRIMARY KEY,
|
teacher_id BIGINT UNSIGNED PRIMARY KEY,
|
||||||
avg_response_minutes INT DEFAULT 4,
|
avg_response_minutes INT DEFAULT 0,
|
||||||
response_rate_percentage DECIMAL(5, 2) DEFAULT 98.50,
|
response_rate_percentage DECIMAL(5, 2) DEFAULT 0,
|
||||||
active_queue_count INT UNSIGNED DEFAULT 0,
|
active_queue_count INT UNSIGNED DEFAULT 0,
|
||||||
total_students_enrolled INT DEFAULT 0,
|
total_students_enrolled INT DEFAULT 0,
|
||||||
total_reviews_count INT DEFAULT 0,
|
total_reviews_count INT DEFAULT 0,
|
||||||
raw_avg_rating DECIMAL(3, 2) DEFAULT 5.00,
|
raw_avg_rating DECIMAL(3, 2) DEFAULT 0,
|
||||||
weighted_student_rating DECIMAL(3, 2) DEFAULT 5.00,
|
weighted_student_rating DECIMAL(3, 2) DEFAULT 0,
|
||||||
ai_engagement_score DECIMAL(5, 2) DEFAULT 96.00,
|
ai_engagement_score DECIMAL(5, 2) DEFAULT 0,
|
||||||
sla_speed_score DECIMAL(5, 2) DEFAULT 98.00,
|
sla_speed_score DECIMAL(5, 2) DEFAULT 0,
|
||||||
mastery_impact_score DECIMAL(5, 2) DEFAULT 94.00,
|
mastery_impact_score DECIMAL(5, 2) DEFAULT 0,
|
||||||
composite_merit_score DECIMAL(5, 2) DEFAULT 96.50,
|
composite_merit_score DECIMAL(5, 2) DEFAULT 0,
|
||||||
star_equivalent DECIMAL(3, 2) DEFAULT 4.90,
|
star_equivalent DECIMAL(3, 2) DEFAULT 0,
|
||||||
reputation_tier VARCHAR(100) DEFAULT 'معلم نخبوي معتمد 💎',
|
reputation_tier VARCHAR(100) DEFAULT 'بانتظار بيانات الأداء',
|
||||||
last_calculated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
last_calculated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
CONSTRAINT fk_tpm_teacher FOREIGN KEY (teacher_id) REFERENCES teachers(id) ON DELETE CASCADE
|
CONSTRAINT fk_tpm_teacher FOREIGN KEY (teacher_id) REFERENCES teachers(id) ON DELETE CASCADE
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
@@ -308,10 +308,6 @@ class TeacherRatingService
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($studentsCount <= 0) {
|
|
||||||
$studentsCount = 83;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'teacher_id' => $teacherId,
|
'teacher_id' => $teacherId,
|
||||||
'composite_merit_score' => $compositeScore,
|
'composite_merit_score' => $compositeScore,
|
||||||
@@ -327,12 +323,12 @@ class TeacherRatingService
|
|||||||
'sla_speed_score' => $slaComponent,
|
'sla_speed_score' => $slaComponent,
|
||||||
'mastery_impact_score' => $masteryComponent,
|
'mastery_impact_score' => $masteryComponent,
|
||||||
'ai_engagement_score' => $aiEngagementScore,
|
'ai_engagement_score' => $aiEngagementScore,
|
||||||
'success_rate_percentage' => 98.2,
|
'success_rate_percentage' => 0.0,
|
||||||
'curriculum_alignment_pct' => 96.0,
|
'curriculum_alignment_pct' => 0.0,
|
||||||
'socratic_interaction_pct' => 89.0,
|
'socratic_interaction_pct' => 0.0,
|
||||||
'audio_clarity_pct' => 94.0,
|
'audio_clarity_pct' => 0.0,
|
||||||
'cognitive_focus_pct' => 92.0,
|
'cognitive_focus_pct' => 0.0,
|
||||||
'ai_recommendation' => 'توجيه الذكاء الاصطناعي الأسبوعي: نسبة الالتزام الوزاري ممتازة (96%). يُوصى بإضافة وقفة سقراطية استنتاجية في الدقيقة 14 من الدرس القادم لتعزيز تفاعل الطلبة.'
|
'ai_recommendation' => 'ستظهر التوصية بعد توفر حصص محللة وتفاعل فعلي من الطلبة.'
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,7 +415,7 @@ class TeacherRatingService
|
|||||||
}
|
}
|
||||||
} catch (\Throwable $e) {}
|
} catch (\Throwable $e) {}
|
||||||
|
|
||||||
return 94.50;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -431,11 +427,11 @@ class TeacherRatingService
|
|||||||
|
|
||||||
$teachers = Database::select(
|
$teachers = Database::select(
|
||||||
"SELECT t.id, t.full_name, t.specialization, t.bio,
|
"SELECT t.id, t.full_name, t.specialization, t.bio,
|
||||||
COALESCE(m.avg_response_minutes, 3) as avg_response_minutes,
|
COALESCE(m.avg_response_minutes, 0) as avg_response_minutes,
|
||||||
COALESCE(m.response_rate_percentage, 99.0) as response_rate_percentage,
|
COALESCE(m.response_rate_percentage, 0) as response_rate_percentage,
|
||||||
COALESCE(m.composite_merit_score, 96.50) as composite_merit_score,
|
COALESCE(m.composite_merit_score, 0) as composite_merit_score,
|
||||||
COALESCE(m.star_equivalent, 4.90) as star_equivalent,
|
COALESCE(m.star_equivalent, 0) as star_equivalent,
|
||||||
COALESCE(m.reputation_tier, 'معلم نخبوي معتمد 💎') as reputation_tier,
|
COALESCE(m.reputation_tier, 'بانتظار بيانات الأداء') as reputation_tier,
|
||||||
(SELECT COUNT(*) FROM lessons WHERE course_id IN (SELECT id FROM courses WHERE teacher_id = t.id)) as lessons_count
|
(SELECT COUNT(*) FROM lessons WHERE course_id IN (SELECT id FROM courses WHERE teacher_id = t.id)) as lessons_count
|
||||||
FROM teachers t
|
FROM teachers t
|
||||||
LEFT JOIN teacher_performance_metrics m ON t.id = m.teacher_id
|
LEFT JOIN teacher_performance_metrics m ON t.id = m.teacher_id
|
||||||
|
|||||||
Reference in New Issue
Block a user