diff --git a/apps/student_app/pubspec.lock b/apps/student_app/pubspec.lock index 08760ee..327e2a6 100644 --- a/apps/student_app/pubspec.lock +++ b/apps/student_app/pubspec.lock @@ -340,10 +340,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.18" material_color_utilities: dependency: transitive description: @@ -356,10 +356,10 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" nested: dependency: transitive description: @@ -585,10 +585,10 @@ packages: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.9" typed_data: dependency: transitive description: diff --git a/apps/teacher_app/lib/core/config/app_config.dart b/apps/teacher_app/lib/core/config/app_config.dart index 799a54f..fa0b2f7 100644 --- a/apps/teacher_app/lib/core/config/app_config.dart +++ b/apps/teacher_app/lib/core/config/app_config.dart @@ -35,6 +35,7 @@ class AppConfig { static const String auditVideoEndpoint = '/api/teacher/audit-studio-video'; static const String uploadLessonEndpoint = '/api/teacher/lessons/upload'; static const String uploadVideoEndpoint = '/api/teacher/videos/upload-direct'; + static const String curriculumTreeEndpoint = '/api/curriculum/tree'; static const String qnaEndpoint = '/api/teacher/qna'; static const String cliqPayoutEndpoint = '/api/teacher/payout/request'; diff --git a/apps/teacher_app/lib/core/services/storage_service.dart b/apps/teacher_app/lib/core/services/storage_service.dart index 3b61c6a..1d594fd 100644 --- a/apps/teacher_app/lib/core/services/storage_service.dart +++ b/apps/teacher_app/lib/core/services/storage_service.dart @@ -39,7 +39,8 @@ class StorageService { return _volatileStore[key]; } - Future saveToken(String token) async => await _writeSafe(_keyToken, token); + Future saveToken(String token) async => + await _writeSafe(_keyToken, token); Future getToken() async => await _readSafe(_keyToken); Future clearSession() async { diff --git a/apps/teacher_app/lib/core/utils/app_logger.dart b/apps/teacher_app/lib/core/utils/app_logger.dart index 362126f..4c5760d 100644 --- a/apps/teacher_app/lib/core/utils/app_logger.dart +++ b/apps/teacher_app/lib/core/utils/app_logger.dart @@ -21,21 +21,25 @@ class AppLogger { if (!kDebugMode) return; final buffer = StringBuffer(); - buffer.writeln('\n๐ŸŒ โ”€โ”€โ”€ [HTTP REQUEST (TEACHER)] โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'); + buffer.writeln( + '\n๐ŸŒ โ”€โ”€โ”€ [HTTP REQUEST (TEACHER)] โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'); buffer.writeln('โžก๏ธ Method: $method'); buffer.writeln('๐Ÿ”— URL: $uri'); if (headers != null && headers.isNotEmpty) { final safeHeaders = Map.from(headers); if (safeHeaders.containsKey('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)}'); } if (body != null) { buffer.writeln('๐Ÿ“ฆ Body: ${body is String ? body : jsonEncode(body)}'); } - buffer.writeln('โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'); + buffer.writeln( + 'โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'); debugPrint(buffer.toString()); } @@ -52,24 +56,29 @@ class AppLogger { final emoji = isSuccess ? 'โœ…' : 'โš ๏ธ'; 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('๐Ÿ”— URL: $uri'); if (duration != null) { buffer.writeln('โฑ๏ธ Time: ${duration.inMilliseconds}ms'); } 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()); } - 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; final buffer = StringBuffer(); - buffer.writeln('\n๐Ÿšจ โ”€โ”€โ”€ [ERROR: $tag] โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'); + buffer.writeln( + '\n๐Ÿšจ โ”€โ”€โ”€ [ERROR: $tag] โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'); buffer.writeln('โŒ Message: $message'); if (error != null) { buffer.writeln('โš ๏ธ Error: $error'); @@ -77,7 +86,8 @@ class AppLogger { if (stackTrace != null) { buffer.writeln('๐Ÿ“ Trace: \n$stackTrace'); } - buffer.writeln('โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'); + buffer.writeln( + 'โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'); debugPrint(buffer.toString()); } } diff --git a/apps/teacher_app/lib/data/models/teacher_models.dart b/apps/teacher_app/lib/data/models/teacher_models.dart index d1b4d30..32c509b 100644 --- a/apps/teacher_app/lib/data/models/teacher_models.dart +++ b/apps/teacher_app/lib/data/models/teacher_models.dart @@ -38,18 +38,28 @@ class TeacherLessonAuditModel { }); factory TeacherLessonAuditModel.fromJson(Map 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( lessonTitle: json['lesson_title'] ?? '', - subject: json['subject'] ?? 'ุงู„ููŠุฒูŠุงุก', - durationMinutes: (json['duration_minutes'] as num?)?.toDouble() ?? 22.5, - durationGatePassed: json['duration_gate_passed'] ?? true, + subject: json['subject'] ?? '', + durationMinutes: (json['duration_minutes'] as num?)?.toDouble() ?? + durationSeconds / 60, + durationGatePassed: json['duration_gate_passed'] ?? + (durationSeconds > 0 && durationSeconds <= 1500), durationWarning: json['duration_warning'], - qualityScore: json['quality_score'] ?? 92, - approvalStatus: json['approval_status'] ?? 'approved_for_broadcast', - curriculumAlignment: json['curriculum_alignment'] ?? '96% ุชุทุงุจู‚ ู…ุน ู…ุฎุฑุฌุงุช ุงู„ู…ู†ู‡ุงุฌ ุงู„ูˆุฒุงุฑูŠ', - audioClarity: json['audio_clarity'] ?? '95% ู†ู‚ุงุก ุตูˆุชูŠ ู…ู…ุชุงุฒ', - socraticStopsCount: json['socratic_stops_count'] ?? 3, - decision: json['decision'] ?? 'ุงู„ุญุตุฉ ู…ุนุชู…ุฏุฉ ูˆู…ุคู‡ู„ุฉ ู„ู„ุจุซ ุงู„ู…ุดูุฑ ูˆุงู„ุนุฑุถ ู„ู„ุจูŠุน ุฎุงุฑุฌ ุงู„ุซู‚ุงูุฉ ุงู„ุนุณูƒุฑูŠุฉ ๐Ÿš€', + qualityScore: (json['quality_score'] as num?)?.toInt() ?? readiness ?? 0, + approvalStatus: json['approval_status'] ?? decisionValue, + curriculumAlignment: + json['curriculum_alignment'] ?? 'ุจุงู†ุชุธุงุฑ ุชุญู„ูŠู„ ุงู„ู…ู„ู ุงู„ูุนู„ูŠ', + audioClarity: json['audio_clarity'] ?? + (visual == null + ? 'ุชู… ุงู„ุชุญู‚ู‚ ู…ู† ูˆุฌูˆุฏ ู…ุณุงุฑ ุตูˆุช ูุนู„ูŠ' + : 'ูˆุถูˆุญ ุงู„ุตูˆุฑุฉ $visual%ุŒ ูˆุชู… ุงู„ุชุญู‚ู‚ ู…ู† ูˆุฌูˆุฏ ุงู„ุตูˆุช'), + socraticStopsCount: (json['socratic_stops_count'] as num?)?.toInt() ?? 0, + decision: json['report'] ?? json['reason'] ?? decisionValue, ); } } @@ -176,10 +186,11 @@ class TeacherProfileModel { factory TeacherProfileModel.fromJson(Map json) { final user = json['user'] as Map? ?? {}; final profile = json['profile'] as Map? ?? {}; - + List grades = []; 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) { grades = [profile['grades_taught'].toString()]; } @@ -191,7 +202,8 @@ class TeacherProfileModel { id: user['id'] is int ? user['id'] : 1, uuid: user['uuid']?.toString() ?? '', name: user['full_name']?.toString() ?? 'ุญุณุงุจ ู…ุนู„ู… ุบูŠุฑ ู…ูƒุชู…ู„', - specialization: profile['specialization']?.toString() ?? 'ุจุงู†ุชุธุงุฑ ุงุณุชูƒู…ุงู„ ุงู„ุชุฎุตุต', + specialization: + profile['specialization']?.toString() ?? 'ุจุงู†ุชุธุงุฑ ุงุณุชูƒู…ุงู„ ุงู„ุชุฎุตุต', schoolName: profile['school_name']?.toString() ?? '', gradesTaught: grades, bio: profile['bio']?.toString() ?? '', @@ -248,7 +260,9 @@ class ChatConversationModel { factory ChatConversationModel.fromJson(Map json) { 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() ?? '', fullName: json['full_name']?.toString() ?? 'ุทุงู„ุจ ุตูŽู‚ูู„', role: json['role']?.toString() ?? 'student', @@ -288,11 +302,17 @@ class ChatMessageModel { factory ChatMessageModel.fromJson(Map json) { 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() ?? '', isMine: json['is_mine'] ?? false, - senderId: json['sender_id'] is int ? 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, + senderId: json['sender_id'] is int + ? 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() ?? '', messageType: json['message_type']?.toString() ?? 'text', mediaUrl: json['media_url']?.toString(), diff --git a/apps/teacher_app/lib/data/repositories/teacher_repository.dart b/apps/teacher_app/lib/data/repositories/teacher_repository.dart index b4ce7ff..ff0145b 100644 --- a/apps/teacher_app/lib/data/repositories/teacher_repository.dart +++ b/apps/teacher_app/lib/data/repositories/teacher_repository.dart @@ -47,16 +47,22 @@ class TeacherRepository { // ============================================================================ Future> 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 { - final response = await _client.post( - Uri.parse('$baseUrl${AppConfig.otpRequestEndpoint}'), - headers: {'Content-Type': 'application/json', 'Accept': 'application/json'}, - body: json.encode({ - 'phone_number': phoneNumber, - 'role': 'teacher', - }), - ).timeout(const Duration(seconds: 30)); + final response = await _client + .post( + Uri.parse('$baseUrl${AppConfig.otpRequestEndpoint}'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: json.encode({ + 'phone_number': phoneNumber, + 'role': 'teacher', + }), + ) + .timeout(const Duration(seconds: 30)); return json.decode(response.body); } catch (e) { @@ -65,20 +71,27 @@ class TeacherRepository { } } - Future> verifyOtp(String phoneNumber, String otp, {String? fullName}) async { - AppLogger.log('Verifying Teacher OTP for $phoneNumber...', tag: 'TEACHER_REPO'); + Future> verifyOtp(String phoneNumber, String otp, + {String? fullName}) async { + AppLogger.log('Verifying Teacher OTP for $phoneNumber...', + tag: 'TEACHER_REPO'); try { - final response = await _client.post( - Uri.parse('$baseUrl${AppConfig.otpVerifyEndpoint}'), - headers: {'Content-Type': 'application/json', 'Accept': 'application/json'}, - body: json.encode({ - 'phone_number': phoneNumber, - 'otp': otp, - 'role': 'teacher', - if (fullName != null) 'full_name': fullName, - 'device_fingerprint': 'saqel_teacher_flutter', - }), - ).timeout(const Duration(seconds: 30)); + final response = await _client + .post( + Uri.parse('$baseUrl${AppConfig.otpVerifyEndpoint}'), + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + body: json.encode({ + 'phone_number': phoneNumber, + 'otp': otp, + 'role': 'teacher', + if (fullName != null) 'full_name': fullName, + 'device_fingerprint': 'saqel_teacher_flutter', + }), + ) + .timeout(const Duration(seconds: 30)); final decoded = json.decode(response.body); if (decoded['status'] == 'success' && decoded['data']?['token'] != null) { @@ -95,10 +108,12 @@ class TeacherRepository { Future?> getMe() async { try { final headers = await _authHeaders(); - final response = await _client.get( - Uri.parse('$baseUrl${AppConfig.meEndpoint}'), - headers: headers, - ).timeout(const Duration(seconds: 4)); + final response = await _client + .get( + Uri.parse('$baseUrl${AppConfig.meEndpoint}'), + headers: headers, + ) + .timeout(const Duration(seconds: 4)); if (response.statusCode == 200) { return json.decode(response.body); @@ -114,10 +129,12 @@ class TeacherRepository { Future getProfileStatus() async { try { final headers = await _authHeaders(); - final response = await _client.get( - Uri.parse('$baseUrl${AppConfig.profileStatusEndpoint}'), - headers: headers, - ).timeout(const Duration(seconds: 4)); + final response = await _client + .get( + Uri.parse('$baseUrl${AppConfig.profileStatusEndpoint}'), + headers: headers, + ) + .timeout(const Duration(seconds: 4)); if (response.statusCode == 200) { final decoded = json.decode(response.body); @@ -141,17 +158,19 @@ class TeacherRepository { }) async { try { final headers = await _authHeaders(); - final response = await _client.post( - Uri.parse('$baseUrl${AppConfig.profileSetupEndpoint}'), - headers: headers, - body: json.encode({ - 'full_name': fullName, - 'specialization': specialization, - 'school_name': schoolName, - 'grades_taught': gradesTaught, - 'bio': bio, - }), - ).timeout(const Duration(seconds: 5)); + final response = await _client + .post( + Uri.parse('$baseUrl${AppConfig.profileSetupEndpoint}'), + headers: headers, + body: json.encode({ + 'full_name': fullName, + 'specialization': specialization, + 'school_name': schoolName, + 'grades_taught': gradesTaught, + 'bio': bio, + }), + ) + .timeout(const Duration(seconds: 5)); if (response.statusCode == 200) { final decoded = json.decode(response.body); @@ -181,10 +200,12 @@ class TeacherRepository { Future> getConversations() async { try { final headers = await _authHeaders(); - final response = await _client.get( - Uri.parse('$baseUrl${AppConfig.chatConversationsEndpoint}'), - headers: headers, - ).timeout(const Duration(seconds: 4)); + final response = await _client + .get( + Uri.parse('$baseUrl${AppConfig.chatConversationsEndpoint}'), + headers: headers, + ) + .timeout(const Duration(seconds: 4)); if (response.statusCode == 200) { final decoded = json.decode(response.body); @@ -192,7 +213,7 @@ class TeacherRepository { final list = (decoded['data'] as List) .map((c) => ChatConversationModel.fromJson(c)) .toList(); - if (list.isNotEmpty) return list; + return list; } } } catch (_) {} @@ -203,14 +224,18 @@ class TeacherRepository { Future> getMessages(int otherUserId) async { try { final headers = await _authHeaders(); - final response = await _client.get( - Uri.parse('$baseUrl${AppConfig.chatMessagesEndpoint}?other_user_id=$otherUserId'), - headers: headers, - ).timeout(const Duration(seconds: 4)); + final response = await _client + .get( + Uri.parse( + '$baseUrl${AppConfig.chatMessagesEndpoint}?other_user_id=$otherUserId'), + headers: headers, + ) + .timeout(const Duration(seconds: 4)); if (response.statusCode == 200) { 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) .map((m) => ChatMessageModel.fromJson(m)) .toList(); @@ -229,16 +254,18 @@ class TeacherRepository { }) async { try { final headers = await _authHeaders(); - final response = await _client.post( - Uri.parse('$baseUrl${AppConfig.chatMessagesEndpoint}'), - headers: headers, - body: json.encode({ - 'receiver_id': receiverId, - 'message': message, - 'message_type': messageType, - if (mediaUrl != null) 'media_url': mediaUrl, - }), - ).timeout(const Duration(seconds: 5)); + final response = await _client + .post( + Uri.parse('$baseUrl${AppConfig.chatMessagesEndpoint}'), + headers: headers, + body: json.encode({ + 'receiver_id': receiverId, + 'message': message, + 'message_type': messageType, + if (mediaUrl != null) 'media_url': mediaUrl, + }), + ) + .timeout(const Duration(seconds: 5)); if (response.statusCode == 200 || response.statusCode == 201) { final decoded = json.decode(response.body); @@ -265,25 +292,28 @@ class TeacherRepository { }) async { try { final headers = await _authHeaders(); - final response = await _client.post( - Uri.parse('$baseUrl${AppConfig.broadcastEndpoint}'), - headers: headers, - body: json.encode({ - 'title': title, - 'message': message, - 'message_type': messageType, - if (mediaUrl != null) 'media_url': mediaUrl, - 'grade_level': gradeLevel, - 'course_id': courseId, - }), - ).timeout(const Duration(seconds: 5)); + final response = await _client + .post( + Uri.parse('$baseUrl${AppConfig.broadcastEndpoint}'), + headers: headers, + body: json.encode({ + 'title': title, + 'message': message, + 'message_type': messageType, + if (mediaUrl != null) 'media_url': mediaUrl, + 'grade_level': gradeLevel, + 'course_id': courseId, + }), + ) + .timeout(const Duration(seconds: 5)); if (response.statusCode == 200) { final decoded = json.decode(response.body); return decoded['status'] == 'success'; } } catch (e) { - AppLogger.error('broadcastAnnouncement notice', error: e, tag: 'TEACHER_REPO'); + AppLogger.error('broadcastAnnouncement notice', + error: e, tag: 'TEACHER_REPO'); } return false; } @@ -299,15 +329,17 @@ class TeacherRepository { }) async { try { final headers = await _authHeaders(); - final response = await _client.post( - Uri.parse('$baseUrl${AppConfig.auditVideoEndpoint}'), - headers: headers, - body: json.encode({ - 'lesson_title': title, - 'duration_minutes': durationMinutes, - 'subject': subject, - }), - ).timeout(const Duration(seconds: 4)); + final response = await _client + .post( + Uri.parse('$baseUrl${AppConfig.auditVideoEndpoint}'), + headers: headers, + body: json.encode({ + 'lesson_title': title, + 'duration_minutes': durationMinutes, + 'subject': subject, + }), + ) + .timeout(const Duration(seconds: 4)); if (response.statusCode == 200) { final decoded = json.decode(response.body); @@ -325,6 +357,7 @@ class TeacherRepository { required double durationMinutes, required String gradeLevel, required String subject, + required String curriculumKey, String? fileName, double? fileSizeMb, String? filePath, @@ -343,6 +376,7 @@ class TeacherRepository { 'duration_minutes': durationMinutes.toString(), 'grade_level': gradeLevel, 'subject': subject, + 'curriculum_key': curriculumKey, }); final resolvedName = fileName ?? 'lesson.mp4'; @@ -362,24 +396,49 @@ class TeacherRepository { 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); - if (response.statusCode == 200 || response.statusCode == 201) { - return json.decode(response.body); - } - } catch (_) {} + final decoded = json.decode(response.body) as Map; + if (response.statusCode == 200 || response.statusCode == 201) + return decoded; + throw StateError( + decoded['message']?.toString() ?? 'ุฑูุถ ุงู„ุฎุงุฏู… ุฑูุน ุงู„ุญุตุฉ.'); + } on StateError { + rethrow; + } catch (error) { + throw StateError('ุชุนุฐุฑ ุฑูุน ุงู„ุญุตุฉ: $error'); + } + } - throw StateError('ู„ู… ูŠุชู… ุฑูุน ุงู„ุญุตุฉ ุฃูˆ ู†ุดุฑู‡ุง ุนู„ู‰ ุงู„ุฎุงุฏู….'); + Future> 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; + if (response.statusCode == 200 && + decoded['status'] == 'success' && + decoded['data'] is Map) { + return Map.from(decoded['data'] as Map); + } + throw StateError( + decoded['message']?.toString() ?? 'ุชุนุฐุฑ ุชุญู…ูŠู„ ุดุฌุฑุฉ ุงู„ู…ู†ู‡ุงุฌ.'); } Future> getMonetizationDashboard() async { try { final headers = await _authHeaders(); - final response = await _client.get( - Uri.parse('$baseUrl${AppConfig.monetizationEndpoint}'), - headers: headers, - ).timeout(const Duration(seconds: 4)); + final response = await _client + .get( + Uri.parse('$baseUrl${AppConfig.monetizationEndpoint}'), + headers: headers, + ) + .timeout(const Duration(seconds: 4)); if (response.statusCode == 200) { final decoded = json.decode(response.body); @@ -398,21 +457,24 @@ class TeacherRepository { }) async { try { final headers = await _authHeaders(); - final response = await _client.post( - Uri.parse('$baseUrl${AppConfig.cliqPayoutEndpoint}'), - headers: headers, - body: json.encode({ - 'cliq_alias': cliqAlias, - 'amount_jod': amountJod, - }), - ).timeout(const Duration(seconds: 5)); + final response = await _client + .post( + Uri.parse('$baseUrl${AppConfig.cliqPayoutEndpoint}'), + headers: headers, + body: json.encode({ + 'cliq_alias': cliqAlias, + 'amount_jod': amountJod, + }), + ) + .timeout(const Duration(seconds: 5)); if (response.statusCode == 200 || response.statusCode == 201) { final decoded = json.decode(response.body); return TeacherPayoutRequestModel( cliqAlias: cliqAlias, 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', ); } @@ -424,22 +486,25 @@ class TeacherRepository { Future> getHomeworks() async { try { final headers = await _authHeaders(); - final response = await _client.get( - Uri.parse('$baseUrl${AppConfig.qnaEndpoint}'), - headers: headers, - ).timeout(const Duration(seconds: 4)); + final response = await _client + .get( + Uri.parse('$baseUrl${AppConfig.qnaEndpoint}'), + headers: headers, + ) + .timeout(const Duration(seconds: 4)); if (response.statusCode == 200) { final decoded = json.decode(response.body); - if (decoded['status'] == 'success' && decoded['data']?['homeworks'] is List) { + if (decoded['status'] == 'success' && + decoded['data']?['homeworks'] is List) { return (decoded['data']['homeworks'] as List) .map((h) => TeacherHomeworkModel( id: h['id'].toString(), title: h['title']?.toString() ?? '', className: h['class_name']?.toString() ?? '', - submitted: h['submitted']?.toString() ?? '0 ู…ู† 38', + submitted: h['submitted']?.toString() ?? '', dueDate: h['due_date']?.toString() ?? '', - averageScore: h['average_score']?.toString() ?? '90%', + averageScore: h['average_score']?.toString() ?? '', status: h['status']?.toString() ?? 'active', )) .toList(); @@ -453,14 +518,17 @@ class TeacherRepository { Future> getStudentDoubts() async { try { final headers = await _authHeaders(); - final response = await _client.get( - Uri.parse('$baseUrl${AppConfig.qnaEndpoint}'), - headers: headers, - ).timeout(const Duration(seconds: 4)); + final response = await _client + .get( + Uri.parse('$baseUrl${AppConfig.qnaEndpoint}'), + headers: headers, + ) + .timeout(const Duration(seconds: 4)); if (response.statusCode == 200) { final decoded = json.decode(response.body); - if (decoded['status'] == 'success' && decoded['data']?['doubts'] is List) { + if (decoded['status'] == 'success' && + decoded['data']?['doubts'] is List) { return (decoded['data']['doubts'] as List) .map((d) => StudentDoubtModel( id: d['id'].toString(), @@ -483,10 +551,12 @@ class TeacherRepository { Future> getPublishedCourses() async { try { final headers = await _authHeaders(); - final response = await _client.get( - Uri.parse('$baseUrl${AppConfig.coursesEndpoint}'), - headers: headers, - ).timeout(const Duration(seconds: 4)); + final response = await _client + .get( + Uri.parse('$baseUrl${AppConfig.coursesEndpoint}'), + headers: headers, + ) + .timeout(const Duration(seconds: 4)); if (response.statusCode == 200) { final decoded = json.decode(response.body); @@ -494,13 +564,15 @@ class TeacherRepository { return (decoded['data'] as List).map((c) { return TeacherCourseModel( id: c['id'].toString(), - title: c['title'] ?? 'ุงู„ููŠุฒูŠุงุก โ€” ุงู„ุตู ุงู„ุนุงุดุฑ', - grade: 'ุงู„ุตู ุงู„ุนุงุดุฑ ุงู„ุฃุณุงุณูŠ', - totalLessons: int.tryParse(c['lessons_total']?.toString() ?? '0') ?? 0, - militaryStudents: 83, - externalSubscribers: 0, - priceJod: double.tryParse(c['price_jod']?.toString() ?? '20.0') ?? 20.0, - netRevenueJod: 0.0, + title: c['title']?.toString() ?? '', + grade: c['grade_level']?.toString() ?? '', + totalLessons: + int.tryParse(c['lessons_total']?.toString() ?? '0') ?? 0, + militaryStudents: (c['military_students'] as num?)?.toInt() ?? 0, + externalSubscribers: (c['external_subscribers'] as num?)?.toInt() ?? 0, + priceJod: + double.tryParse(c['price_jod']?.toString() ?? '') ?? 0, + netRevenueJod: double.tryParse(c['net_revenue_jod']?.toString() ?? '') ?? 0, ); }).toList(); } @@ -517,10 +589,12 @@ class TeacherRepository { Future> getMyReputation() async { try { final headers = await _authHeaders(); - final response = await _client.get( - Uri.parse('$baseUrl${AppConfig.reputationEndpoint}'), - headers: headers, - ).timeout(const Duration(seconds: 4)); + final response = await _client + .get( + Uri.parse('$baseUrl${AppConfig.reputationEndpoint}'), + headers: headers, + ) + .timeout(const Duration(seconds: 4)); if (response.statusCode == 200) { final decoded = json.decode(response.body); diff --git a/apps/teacher_app/lib/logic/cubits/teacher_auth_cubit.dart b/apps/teacher_app/lib/logic/cubits/teacher_auth_cubit.dart index d31e480..01f6721 100644 --- a/apps/teacher_app/lib/logic/cubits/teacher_auth_cubit.dart +++ b/apps/teacher_app/lib/logic/cubits/teacher_auth_cubit.dart @@ -7,6 +7,7 @@ import '../../data/repositories/teacher_repository.dart'; abstract class TeacherAuthState {} class TeacherAuthInitial extends TeacherAuthState {} + class TeacherAuthLoading extends TeacherAuthState {} class TeacherAuthOtpSent extends TeacherAuthState { @@ -42,13 +43,16 @@ class TeacherAuthCubit extends Cubit { try { final token = await _storage.getToken(); if (token == null || token.isEmpty) { - AppLogger.log('No teacher token -> Unauthenticated', tag: 'TEACHER_AUTH'); + AppLogger.log('No teacher token -> Unauthenticated', + tag: 'TEACHER_AUTH'); emit(TeacherUnauthenticated()); return; } final profile = await repository.getProfileStatus(); - AppLogger.log('Teacher authenticated: ${profile.name} (${profile.specialization})', tag: 'TEACHER_AUTH'); + AppLogger.log( + 'Teacher authenticated: ${profile.name} (${profile.specialization})', + tag: 'TEACHER_AUTH'); emit(TeacherAuthenticated(profile: profile)); } catch (e) { AppLogger.error('Session check failed', error: e, tag: 'TEACHER_AUTH'); @@ -67,7 +71,8 @@ class TeacherAuthCubit extends Cubit { if (res['status'] == 'success') { emit(TeacherAuthOtpSent(phoneNumber: phoneNumber)); } else { - emit(TeacherAuthError(res['message']?.toString() ?? 'ูุดู„ ุฅุฑุณุงู„ ุฑู…ุฒ ุงู„ุชุญู‚ู‚')); + emit(TeacherAuthError( + res['message']?.toString() ?? 'ูุดู„ ุฅุฑุณุงู„ ุฑู…ุฒ ุงู„ุชุญู‚ู‚')); } } catch (e) { emit(TeacherAuthError(e.toString())); @@ -84,9 +89,13 @@ class TeacherAuthCubit extends Cubit { }) async { emit(TeacherAuthLoading()); try { - final res = await repository.verifyOtp(phoneNumber, otp, fullName: fullName); + final res = + await repository.verifyOtp(phoneNumber, otp, fullName: fullName); if (res['status'] == 'success') { - if ((fullName ?? '').trim().isNotEmpty && (specialization ?? '').trim().isNotEmpty && (schoolName ?? '').trim().isNotEmpty && (gradesTaught?.isNotEmpty ?? false)) { + if ((fullName ?? '').trim().isNotEmpty && + (specialization ?? '').trim().isNotEmpty && + (schoolName ?? '').trim().isNotEmpty && + (gradesTaught?.isNotEmpty ?? false)) { try { await repository.setupProfile( fullName: fullName!.trim(), @@ -100,7 +109,8 @@ class TeacherAuthCubit extends Cubit { final profile = await repository.getProfileStatus(); emit(TeacherAuthenticated(profile: profile)); } else { - emit(TeacherAuthError(res['message']?.toString() ?? 'ุฑู…ุฒ ุงู„ุชุญู‚ู‚ ุบูŠุฑ ุตุญูŠุญ')); + emit(TeacherAuthError( + res['message']?.toString() ?? 'ุฑู…ุฒ ุงู„ุชุญู‚ู‚ ุบูŠุฑ ุตุญูŠุญ')); } } catch (e) { emit(TeacherAuthError(e.toString())); diff --git a/apps/teacher_app/lib/logic/cubits/teacher_monetization_cubit.dart b/apps/teacher_app/lib/logic/cubits/teacher_monetization_cubit.dart index c76822a..5b65e76 100644 --- a/apps/teacher_app/lib/logic/cubits/teacher_monetization_cubit.dart +++ b/apps/teacher_app/lib/logic/cubits/teacher_monetization_cubit.dart @@ -63,7 +63,8 @@ class TeacherMonetizationState { bool? isLoading, }) { return TeacherMonetizationState( - institutionalStudents: institutionalStudents ?? this.institutionalStudents, + institutionalStudents: + institutionalStudents ?? this.institutionalStudents, studentSubscribers: studentSubscribers ?? this.studentSubscribers, pricePerCourse: pricePerCourse ?? this.pricePerCourse, grossRevenue: grossRevenue ?? this.grossRevenue, @@ -86,7 +87,7 @@ class TeacherMonetizationCubit extends Cubit { TeacherMonetizationCubit({required this.repository}) : super(const TeacherMonetizationState( - institutionalStudents: 83, + institutionalStudents: 0, studentSubscribers: 0, pricePerCourse: 20.0, grossRevenue: 0.0, @@ -95,7 +96,7 @@ class TeacherMonetizationCubit extends Cubit { platformShare: 0.0, availableBalance: 0.0, totalWithdrawn: 0.0, - cliqAlias: '0798583052@CLIQ', + cliqAlias: '', courses: [], payoutRequests: [], isSubmittingPayout: false, @@ -108,20 +109,26 @@ class TeacherMonetizationCubit extends Cubit { final data = await repository.getMonetizationDashboard(); final courses = await repository.getPublishedCourses(); - final audience = data['audience_breakdown'] as Map? ?? {}; - final inst = audience['institutional_students'] as Map? ?? {}; - final mkt = audience['marketplace_students'] as Map? ?? {}; + final audience = + data['audience_breakdown'] as Map? ?? {}; + final inst = + audience['institutional_students'] as Map? ?? {}; + final mkt = + audience['marketplace_students'] as Map? ?? {}; final wallet = data['wallet'] as Map? ?? {}; - final instCount = (inst['count'] as num?)?.toDouble() ?? 83.0; + final instCount = (inst['count'] as num?)?.toDouble() ?? 0.0; final paidCount = (mkt['count'] as num?)?.toDouble() ?? 0.0; - final gross = (data['gross_revenue_jod'] as num?)?.toDouble() ?? (paidCount * 20.0); + final gross = + (data['gross_revenue_jod'] as num?)?.toDouble() ?? (paidCount * 20.0); final tShare = gross * 0.55; final dShare = gross * 0.15; final pShare = gross * 0.30; - final avail = (wallet['available_balance_jod'] as num?)?.toDouble() ?? tShare; - final withdrawn = (wallet['total_withdrawn_jod'] as num?)?.toDouble() ?? 0.0; - final alias = wallet['cliq_payout_alias']?.toString() ?? '0798583052@CLIQ'; + final avail = + (wallet['available_balance_jod'] as num?)?.toDouble() ?? tShare; + final withdrawn = + (wallet['total_withdrawn_jod'] as num?)?.toDouble() ?? 0.0; + final alias = wallet['cliq_payout_alias']?.toString() ?? ''; List payouts = []; if (wallet['recent_payouts'] is List) { @@ -176,8 +183,11 @@ class TeacherMonetizationCubit extends Cubit { cliqAlias: cliqAlias, amountJod: amountJod, ); - final updatedList = List.from(state.payoutRequests)..insert(0, req); - final newAvail = (state.availableBalance - amountJod).clamp(0.0, double.infinity); + final updatedList = + List.from(state.payoutRequests) + ..insert(0, req); + final newAvail = + (state.availableBalance - amountJod).clamp(0.0, double.infinity); final newWithdrawn = state.totalWithdrawn + amountJod; emit(state.copyWith( diff --git a/apps/teacher_app/lib/logic/cubits/teacher_qna_cubit.dart b/apps/teacher_app/lib/logic/cubits/teacher_qna_cubit.dart index a14a06a..8a16d35 100644 --- a/apps/teacher_app/lib/logic/cubits/teacher_qna_cubit.dart +++ b/apps/teacher_app/lib/logic/cubits/teacher_qna_cubit.dart @@ -58,16 +58,21 @@ class TeacherQnACubit extends Cubit { Future loadQnA() async { emit(state.copyWith(isLoading: true)); - final hwList = await repository.getHomeworks(); - final doubtsList = await repository.getStudentDoubts(); - final conversationsList = await repository.getConversations(); - - emit(state.copyWith( - homeworks: hwList, - doubts: doubtsList, - conversations: conversationsList, - isLoading: false, - )); + try { + final results = await Future.wait([ + repository.getHomeworks(), + repository.getStudentDoubts(), + repository.getConversations(), + ]); + emit(state.copyWith( + homeworks: results[0] as List, + doubts: results[1] as List, + conversations: results[2] as List, + isLoading: false, + )); + } catch (_) { + emit(state.copyWith(isLoading: false)); + } } void dispatchNewHomework({ @@ -86,14 +91,19 @@ class TeacherQnACubit extends Cubit { final target = state.doubts[targetIndex]; final replyText = voiceText?.trim() ?? ''; if (replyText.isEmpty) { - throw StateError('ู„ุง ูŠู…ูƒู† ุชุณุฌูŠู„ ุฑุฏ ุตูˆุชูŠ ุฏูˆู† ู…ู„ู ุตูˆุช ุญู‚ูŠู‚ูŠุ› ุฃุฑุณู„ ุฑุฏุงู‹ ู†ุตูŠุงู‹ ุญุงู„ูŠุงู‹.'); + throw StateError( + 'ู„ุง ูŠู…ูƒู† ุชุณุฌูŠู„ ุฑุฏ ุตูˆุชูŠ ุฏูˆู† ู…ู„ู ุตูˆุช ุญู‚ูŠู‚ูŠุ› ุฃุฑุณู„ ุฑุฏุงู‹ ู†ุตูŠุงู‹ ุญุงู„ูŠุงู‹.'); } await repository.sendMessage( receiverId: target.studentId, message: replyText, messageType: 'text', ); - final updated = state.doubts.map((doubt) => doubt.id == doubtId ? doubt.copyWith(replied: true, voiceReply: false) : doubt).toList(); + final updated = state.doubts + .map((doubt) => doubt.id == doubtId + ? doubt.copyWith(replied: true, voiceReply: false) + : doubt) + .toList(); emit(state.copyWith(doubts: updated)); } @@ -106,8 +116,8 @@ class TeacherQnACubit extends Cubit { final success = await repository.broadcastAnnouncement( title: title, message: explanationText, - messageType: 'voice', - mediaUrl: 'https://saqel.intaleqapp.com/assets/audio/teacher_broadcast.mp3', + messageType: 'text', + mediaUrl: null, gradeLevel: gradeLevel, ); emit(state.copyWith(isBroadcasting: false)); diff --git a/apps/teacher_app/lib/logic/cubits/teacher_studio_cubit.dart b/apps/teacher_app/lib/logic/cubits/teacher_studio_cubit.dart index ab9a777..104304e 100644 --- a/apps/teacher_app/lib/logic/cubits/teacher_studio_cubit.dart +++ b/apps/teacher_app/lib/logic/cubits/teacher_studio_cubit.dart @@ -17,6 +17,14 @@ class TeacherStudioState { final String lessonTitle; final double durationMinutes; final String gradeLevel; + final Map curriculumTree; + final String gradeKey; + final String subjectKey; + final String semesterKey; + final String unitKey; + final String lessonKey; + final bool isCurriculumLoading; + final String? errorMessage; final String? selectedFileName; final double? selectedFileSizeMb; final String? selectedFilePath; @@ -30,7 +38,15 @@ class TeacherStudioState { const TeacherStudioState({ required this.lessonTitle, required this.durationMinutes, - this.gradeLevel = 'ุงู„ุตู ุงู„ุนุงุดุฑ ุงู„ุฃุณุงุณูŠ', + this.gradeLevel = '', + this.curriculumTree = const {}, + this.gradeKey = '', + this.subjectKey = '', + this.semesterKey = '', + this.unitKey = '', + this.lessonKey = '', + this.isCurriculumLoading = false, + this.errorMessage, this.selectedFileName, this.selectedFileSizeMb, this.selectedFilePath, @@ -46,6 +62,15 @@ class TeacherStudioState { String? lessonTitle, double? durationMinutes, String? gradeLevel, + Map? curriculumTree, + String? gradeKey, + String? subjectKey, + String? semesterKey, + String? unitKey, + String? lessonKey, + bool? isCurriculumLoading, + String? errorMessage, + bool clearError = false, String? selectedFileName, double? selectedFileSizeMb, String? selectedFilePath, @@ -60,6 +85,14 @@ class TeacherStudioState { lessonTitle: lessonTitle ?? this.lessonTitle, durationMinutes: durationMinutes ?? this.durationMinutes, gradeLevel: gradeLevel ?? this.gradeLevel, + curriculumTree: curriculumTree ?? this.curriculumTree, + gradeKey: gradeKey ?? this.gradeKey, + subjectKey: subjectKey ?? this.subjectKey, + semesterKey: semesterKey ?? this.semesterKey, + unitKey: unitKey ?? this.unitKey, + lessonKey: lessonKey ?? this.lessonKey, + isCurriculumLoading: isCurriculumLoading ?? this.isCurriculumLoading, + errorMessage: clearError ? null : errorMessage ?? this.errorMessage, selectedFileName: selectedFileName ?? this.selectedFileName, selectedFileSizeMb: selectedFileSizeMb ?? this.selectedFileSizeMb, selectedFilePath: selectedFilePath ?? this.selectedFilePath, @@ -78,8 +111,8 @@ class TeacherStudioCubit extends Cubit { TeacherStudioCubit({required this.repository}) : super(const TeacherStudioState( - lessonTitle: 'ุดุฑุญ ู‚ุงุนุฏุฉ ู„ู†ุชุฒ ูˆุงู„ุญุซ ุงู„ูƒู‡ุฑูˆู…ุบู†ุงุทูŠุณูŠ โ€” ููŠุฒูŠุงุก 2008', - durationMinutes: 13.0, + lessonTitle: '', + durationMinutes: 20.0, isAuditing: false, auditResult: null, )); @@ -96,6 +129,137 @@ class TeacherStudioCubit extends Cubit { emit(state.copyWith(gradeLevel: grade)); } + Map _map(dynamic value) => + value is Map ? Map.from(value) : {}; + Map get _subjects => + _map(state.curriculumTree[state.gradeKey]?['subjects']); + Map get _semesters => + _map(_subjects[state.subjectKey]?['semesters']); + Map get _units => + _map(_semesters[state.semesterKey]?['units']); + + Future 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.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 = {}; + 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.fromEntries( + allSubjects.entries.where((entry) { + final subjectName = + _normalized('${entry.key} ${_map(entry.value)['name'] ?? ''}'); + return subjectName.contains(specialization) || + specialization.contains(subjectName) || + specialization.split(' ').any( + (word) => word.length > 3 && subjectName.contains(word)); + })); + if (matched.isNotEmpty) gradeNode['subjects'] = matched; + } + tree[gradeEntry.key] = gradeNode; + } + final key = tree.keys.isNotEmpty ? tree.keys.first : ''; + emit(state.copyWith(curriculumTree: tree, isCurriculumLoading: false)); + selectGrade(key); + } catch (e) { + emit(state.copyWith( + isCurriculumLoading: false, errorMessage: e.toString())); + } + } + + String _normalized(String value) => value + .toLowerCase() + .replaceAll(RegExp(r'[^\u0600-\u06ffa-z0-9]+'), ' ') + .trim(); + + void selectGrade(String key) { + final node = state.curriculumTree[key]; + final name = node is Map ? node['name']?.toString() ?? key : key; + emit(state.copyWith( + gradeKey: key, + gradeLevel: name, + subjectKey: '', + semesterKey: '', + unitKey: '', + lessonKey: '', + clearError: true)); + final values = _map(state.curriculumTree[key]?['subjects']); + selectSubject(values.keys.isNotEmpty ? values.keys.first : ''); + } + + void selectSubject(String key) { + emit(state.copyWith( + subjectKey: key, + semesterKey: '', + unitKey: '', + lessonKey: '', + clearError: true)); + final values = _map(_subjects[key]?['semesters']); + selectSemester(values.keys.isNotEmpty ? values.keys.first : ''); + } + + void selectSemester(String key) { + emit(state.copyWith( + semesterKey: key, unitKey: '', lessonKey: '', clearError: true)); + final values = _map(_semesters[key]?['units']); + selectUnit(values.keys.isNotEmpty ? values.keys.first : ''); + } + + void selectUnit(String key) { + emit(state.copyWith(unitKey: key, lessonKey: '', clearError: true)); + final lessons = _units[key]?['lessons']; + final id = lessons is List && lessons.isNotEmpty && lessons.first is Map + ? (lessons.first as Map)['id']?.toString() ?? '' + : ''; + selectLesson(id); + } + + void selectLesson(String key) { + String title = ''; + final lessons = _units[state.unitKey]?['lessons']; + if (lessons is List) + for (final item in lessons) { + if (item is Map && item['id']?.toString() == key) + title = item['title']?.toString() ?? ''; + } + emit(state.copyWith(lessonKey: key, lessonTitle: title, clearError: true)); + } + + String get selectedSubjectName { + final node = _subjects[state.subjectKey]; + final name = node is Map + ? node['name']?.toString() ?? state.subjectKey + : state.subjectKey; + return name.replaceFirst(RegExp(r'\s*\([^)]*\)\s*$'), '').trim(); + } + + String get curriculumKey => [ + state.gradeKey, + state.subjectKey, + state.semesterKey, + state.unitKey, + state.lessonKey + ].where((e) => e.isNotEmpty).join('/'); + void reportError(String message) => + emit(state.copyWith(errorMessage: message)); + void selectVideoFile( String fileName, double sizeMb, @@ -115,20 +279,18 @@ class TeacherStudioCubit extends Cubit { } Future runQualityGate() async { - emit(state.copyWith(isAuditing: true)); - try { - final result = await repository.auditStudioVideo( - title: state.lessonTitle, - durationMinutes: state.durationMinutes, - subject: 'ุงู„ููŠุฒูŠุงุก', - ); + if (state.selectedFileName == null || + (state.selectedFilePath == null && state.selectedFileBytes == null)) { emit(state.copyWith( - isAuditing: false, - auditResult: result, - )); - } catch (_) { - emit(state.copyWith(isAuditing: false)); + errorMessage: 'ุงุฎุชุฑ ู…ู„ู ููŠุฏูŠูˆ ูุนู„ูŠุงู‹ ู‚ุจู„ ุจุฏุก ูุญุต ุงู„ุฌูˆุฏุฉ.')); + return; } + if (curriculumKey.isEmpty || state.lessonTitle.trim().isEmpty) { + emit(state.copyWith( + errorMessage: 'ุงุฎุชุฑ ุงู„ุตู ูˆุงู„ู…ุจุญุซ ูˆุงู„ูุตู„ ูˆุงู„ูˆุญุฏุฉ ูˆุงู„ุฏุฑุณ ุฃูˆู„ุงู‹.')); + return; + } + await uploadAndPublishLesson(); } Future uploadAndPublishLesson() async { @@ -136,28 +298,45 @@ class TeacherStudioCubit extends Cubit { (state.selectedFilePath == null && state.selectedFileBytes == null)) { return false; } - emit(state.copyWith(isUploading: true)); + emit(state.copyWith(isUploading: true, isAuditing: true, clearError: true)); try { final res = await repository.uploadLesson( title: state.lessonTitle, durationMinutes: state.durationMinutes, gradeLevel: state.gradeLevel, - subject: 'ุงู„ููŠุฒูŠุงุก ูˆุงู„ุนู„ูˆู… ุงู„ุชุทุจูŠู‚ูŠุฉ', - fileName: state.selectedFileName ?? 'lentz_law_physics_2008.mp4', - fileSizeMb: state.selectedFileSizeMb ?? 48.2, + subject: selectedSubjectName, + curriculumKey: curriculumKey, + fileName: state.selectedFileName!, + fileSizeMb: state.selectedFileSizeMb, filePath: state.selectedFilePath, fileBytes: state.selectedFileBytes, ); - final msg = res['message']?.toString() ?? 'ุชู… ุฑูุน ูˆู†ุดุฑ ุงู„ุญุตุฉ ุจู†ุฌุงุญ ููŠ ุงู„ู…ู†ู‡ุงุฌ ุงู„ูˆุฒุงุฑูŠ ูˆุณูˆู‚ ุตูŽู‚ูู„! ๐Ÿš€'; + final data = res['data'] is Map + ? Map.from(res['data'] as Map) + : {}; + final report = data['preflight_report'] is Map + ? Map.from(data['preflight_report'] as Map) + : {}; + final result = TeacherLessonAuditModel.fromJson({ + ...report, + 'lesson_title': state.lessonTitle, + 'subject': selectedSubjectName + }); + final msg = res['message']?.toString() ?? 'ุชู… ุงุนุชู…ุงุฏ ุงู„ุญุตุฉ ูˆุฑูุนู‡ุง ุจู†ุฌุงุญ.'; emit(state.copyWith( + isAuditing: false, isUploading: false, isUploaded: true, + auditResult: result, uploadSuccessMessage: msg, )); return true; } catch (e) { - emit(state.copyWith(isUploading: false)); + emit(state.copyWith( + isAuditing: false, + isUploading: false, + errorMessage: e.toString().replaceFirst('Bad state: ', ''))); return false; } } diff --git a/apps/teacher_app/lib/main.dart b/apps/teacher_app/lib/main.dart index 678368a..c22e9d1 100644 --- a/apps/teacher_app/lib/main.dart +++ b/apps/teacher_app/lib/main.dart @@ -38,7 +38,8 @@ class SaqelTeacherApp extends StatelessWidget { return MultiBlocProvider( providers: [ BlocProvider( - create: (_) => TeacherAuthCubit(repository: repository)..checkSession(), + create: (_) => + TeacherAuthCubit(repository: repository)..checkSession(), ), BlocProvider( create: (_) => TeacherStudioCubit(repository: repository), @@ -73,7 +74,8 @@ class TeacherAuthGate extends StatelessWidget { return const Scaffold( backgroundColor: TeacherTheme.backgroundDark, body: Center( - child: CircularProgressIndicator(color: TeacherTheme.emeraldPrimary), + child: + CircularProgressIndicator(color: TeacherTheme.emeraldPrimary), ), ); } diff --git a/apps/teacher_app/lib/presentation/screens/auth/teacher_auth_screen.dart b/apps/teacher_app/lib/presentation/screens/auth/teacher_auth_screen.dart index da48df8..8a61a2d 100644 --- a/apps/teacher_app/lib/presentation/screens/auth/teacher_auth_screen.dart +++ b/apps/teacher_app/lib/presentation/screens/auth/teacher_auth_screen.dart @@ -63,7 +63,8 @@ class _TeacherAuthScreenState extends State { void _onSendOtp() { final phone = _phoneController.text.trim(); if (phone.isEmpty || phone.length < 9) { - SaqelToast.showError(context, 'ูŠุฑุฌู‰ ุฅุฏุฎุงู„ ุฑู‚ู… ู‡ุงุชู ุฃุฑุฏู†ูŠ ุตุญูŠุญ ูŠุจุฏุฃ ุจู€ 07'); + SaqelToast.showError( + context, 'ูŠุฑุฌู‰ ุฅุฏุฎุงู„ ุฑู‚ู… ู‡ุงุชู ุฃุฑุฏู†ูŠ ุตุญูŠุญ ูŠุจุฏุฃ ุจู€ 07'); return; } context.read().sendOtp(phone); @@ -76,18 +77,22 @@ class _TeacherAuthScreenState extends State { SaqelToast.showError(context, 'ูŠุฑุฌู‰ ุฅุฏุฎุงู„ ุฑู…ุฒ ุงู„ุชุญู‚ู‚ ุงู„ู…ูƒูˆู† ู…ู† 6 ุฃุฑู‚ุงู…'); return; } - if (_fullNameController.text.trim().isEmpty || _selectedSubject == null || _schoolController.text.trim().isEmpty || _selectedGrades.isEmpty) { - SaqelToast.showError(context, 'ุฃูƒู…ู„ ุงู„ุงุณู… ูˆุงู„ู…ุงุฏุฉ ูˆุงู„ู…ุฏุฑุณุฉ ูˆุตูู‹ุง ูˆุงุญุฏู‹ุง ุนู„ู‰ ุงู„ุฃู‚ู„ ู‚ุจู„ ุงู„ุชุญู‚ู‚'); + if (_fullNameController.text.trim().isEmpty || + _selectedSubject == null || + _schoolController.text.trim().isEmpty || + _selectedGrades.isEmpty) { + SaqelToast.showError(context, + 'ุฃูƒู…ู„ ุงู„ุงุณู… ูˆุงู„ู…ุงุฏุฉ ูˆุงู„ู…ุฏุฑุณุฉ ูˆุตูู‹ุง ูˆุงุญุฏู‹ุง ุนู„ู‰ ุงู„ุฃู‚ู„ ู‚ุจู„ ุงู„ุชุญู‚ู‚'); return; } context.read().verifyOtp( - phone, - otp, - fullName: _fullNameController.text.trim(), - specialization: _selectedSubject, - schoolName: _schoolController.text.trim(), - gradesTaught: _selectedGrades.toList(), - ); + phone, + otp, + fullName: _fullNameController.text.trim(), + specialization: _selectedSubject, + schoolName: _schoolController.text.trim(), + gradesTaught: _selectedGrades.toList(), + ); } @override @@ -105,7 +110,8 @@ class _TeacherAuthScreenState extends State { child: BlocConsumer( listener: (context, state) { if (state is TeacherAuthError) { - SaqelToast.showError(context, state.message, title: 'ุชู†ุจูŠู‡ ุงู„ู…ุตุงุฏู‚ุฉ'); + SaqelToast.showError(context, state.message, + title: 'ุชู†ุจูŠู‡ ุงู„ู…ุตุงุฏู‚ุฉ'); } else if (state is TeacherAuthOtpSent) { setState(() => _isEnteringOtp = true); SaqelToast.showWhatsApp( @@ -117,14 +123,16 @@ class _TeacherAuthScreenState extends State { }, builder: (context, state) { final isLoading = state is TeacherAuthLoading; - final isOtpStep = _isEnteringOtp || state is TeacherAuthOtpSent; + final isOtpStep = + _isEnteringOtp || state is TeacherAuthOtpSent; return Container( padding: const EdgeInsets.all(28), decoration: BoxDecoration( color: TeacherTheme.surfaceDark, 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( color: Color(0x3310B981), @@ -143,7 +151,10 @@ class _TeacherAuthScreenState extends State { height: 68, decoration: BoxDecoration( gradient: const LinearGradient( - colors: [TeacherTheme.emeraldPrimary, TeacherTheme.emeraldDark], + colors: [ + TeacherTheme.emeraldPrimary, + TeacherTheme.emeraldDark + ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), @@ -194,11 +205,14 @@ class _TeacherAuthScreenState extends State { // Backend Live Gateway Badge Center( child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 4), decoration: BoxDecoration( color: const Color(0xFF0C241D), borderRadius: BorderRadius.circular(20), - border: Border.all(color: TeacherTheme.emeraldPrimary.withOpacity(0.4)), + border: Border.all( + color: TeacherTheme.emeraldPrimary + .withOpacity(0.4)), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -213,7 +227,9 @@ class _TeacherAuthScreenState extends State { ), const SizedBox(width: 6), Text( - AppConfig.baseUrl.replaceAll('https://', '').replaceAll('http://', ''), + AppConfig.baseUrl + .replaceAll('https://', '') + .replaceAll('http://', ''), style: const TextStyle( color: TeacherTheme.emeraldLight, fontSize: 11, @@ -230,7 +246,10 @@ class _TeacherAuthScreenState extends State { // Step 1: Teacher Info & Phone 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), _buildTextField( @@ -242,30 +261,41 @@ class _TeacherAuthScreenState extends State { 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), Container( - padding: const EdgeInsets.symmetric(horizontal: 12), + padding: + const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: TeacherTheme.surfaceCard, borderRadius: BorderRadius.circular(14), - border: Border.all(color: TeacherTheme.surfaceBorder), + border: Border.all( + color: TeacherTheme.surfaceBorder), ), child: DropdownButtonHideUnderline( child: DropdownButton( value: _selectedSubject, isExpanded: true, 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) { return DropdownMenuItem( 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(), onChanged: (val) { - if (val != null) setState(() => _selectedSubject = val); + if (val != null) + setState(() => _selectedSubject = val); }, ), ), @@ -274,7 +304,10 @@ class _TeacherAuthScreenState extends State { 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), _buildTextField( @@ -286,21 +319,34 @@ class _TeacherAuthScreenState extends State { 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), Wrap( spacing: 8, runSpacing: 8, children: _allGrades.map((grade) { - final isSelected = _selectedGrades.contains(grade); + final isSelected = + _selectedGrades.contains(grade); return FilterChip( 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, backgroundColor: TeacherTheme.surfaceCard, checkmarkColor: Colors.black, - side: BorderSide(color: isSelected ? TeacherTheme.emeraldPrimary : TeacherTheme.surfaceBorder), + side: BorderSide( + color: isSelected + ? TeacherTheme.emeraldPrimary + : TeacherTheme.surfaceBorder), onSelected: (selected) { setState(() { if (selected) { @@ -319,7 +365,10 @@ class _TeacherAuthScreenState extends State { const Text( 'ุฑู‚ู… ุงู„ู‡ุงุชู ู„ู„ุฏุฎูˆู„ ูˆุงู„ุชุญู‚ู‚ (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), _buildTextField( @@ -335,15 +384,23 @@ class _TeacherAuthScreenState extends State { style: ElevatedButton.styleFrom( backgroundColor: TeacherTheme.emeraldPrimary, foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + padding: + const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14)), elevation: 0, ), child: isLoading - ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.black, strokeWidth: 2)) + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.black, strokeWidth: 2)) : const Text( 'ุฅุฑุณุงู„ ุฑู…ุฒ ุงู„ุชุญู‚ู‚ ุนุจุฑ ุงู„ูˆุงุชุณุงุจ ๐Ÿ“ฒ', - style: TextStyle(fontSize: 14, fontWeight: FontWeight.w900), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w900), ), ), ] else ...[ @@ -353,25 +410,34 @@ class _TeacherAuthScreenState extends State { decoration: BoxDecoration( color: const Color(0xFF0C241D), 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( children: [ Row( mainAxisAlignment: MainAxisAlignment.center, 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), 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), 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, ), ], @@ -381,7 +447,10 @@ class _TeacherAuthScreenState extends State { const Text( 'ุฑู…ุฒ ุงู„ุชุญู‚ู‚ ุงู„ู…ุนุชู…ุฏ (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), _buildOtpField(controller: _otpController), @@ -392,15 +461,23 @@ class _TeacherAuthScreenState extends State { style: ElevatedButton.styleFrom( backgroundColor: TeacherTheme.emeraldPrimary, foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(vertical: 16), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), + padding: + const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14)), elevation: 0, ), child: isLoading - ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(color: Colors.black, strokeWidth: 2)) + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + color: Colors.black, strokeWidth: 2)) : const Text( 'ุชุฃูƒูŠุฏ ุงู„ุฏุฎูˆู„ ู„ู…ู„ู ุงู„ู…ุนู„ู… โœจ', - style: TextStyle(fontSize: 14, fontWeight: FontWeight.w900), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w900), ), ), const SizedBox(height: 14), @@ -410,16 +487,31 @@ class _TeacherAuthScreenState extends State { children: [ TextButton.icon( onPressed: isLoading ? null : _onSendOtp, - icon: const Icon(CupertinoIcons.refresh_circled, size: 16, color: Color(0xFF25D366)), - label: const Text('ุฅุนุงุฏุฉ ุฅุฑุณุงู„ ุงู„ุฑู…ุฒ ๐Ÿ“ฒ', style: TextStyle(color: Color(0xFF25D366), fontSize: 12, fontWeight: FontWeight.bold)), + icon: const Icon( + CupertinoIcons.refresh_circled, + size: 16, + color: Color(0xFF25D366)), + label: const Text('ุฅุนุงุฏุฉ ุฅุฑุณุงู„ ุงู„ุฑู…ุฒ ๐Ÿ“ฒ', + style: TextStyle( + color: Color(0xFF25D366), + fontSize: 12, + fontWeight: FontWeight.bold)), ), TextButton.icon( onPressed: () { setState(() => _isEnteringOtp = false); - context.read().resetToPhone(); + context + .read() + .resetToPhone(); }, - icon: const Icon(CupertinoIcons.pencil_ellipsis_rectangle, size: 16, color: Color(0xFF94A3B8)), - label: const Text('ุชุนุฏูŠู„ ุฑู‚ู… ุงู„ู‡ุงุชู โœ๏ธ', style: TextStyle(color: Color(0xFF94A3B8), fontSize: 12)), + icon: const Icon( + 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 { child: TextField( controller: controller, 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( prefixIcon: Icon(icon, color: TeacherTheme.emeraldPrimary, size: 18), hintText: hint, hintStyle: const TextStyle(color: Color(0xFF64748B), fontSize: 12.5), 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 { decoration: BoxDecoration( color: TeacherTheme.surfaceCard, 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( color: Color(0x2210B981), @@ -493,7 +588,8 @@ class _TeacherAuthScreenState extends State { ), decoration: const InputDecoration( counterText: '', - prefixIcon: Icon(CupertinoIcons.lock_shield_fill, color: TeacherTheme.emeraldPrimary, size: 22), + prefixIcon: Icon(CupertinoIcons.lock_shield_fill, + color: TeacherTheme.emeraldPrimary, size: 22), hintText: 'โ€ข โ€ข โ€ข โ€ข โ€ข โ€ข', hintStyle: TextStyle( color: Color(0xFF475569), diff --git a/apps/teacher_app/lib/presentation/screens/tabs/teacher_assignments_qna_tab.dart b/apps/teacher_app/lib/presentation/screens/tabs/teacher_assignments_qna_tab.dart index 05b6944..5d1121a 100644 --- a/apps/teacher_app/lib/presentation/screens/tabs/teacher_assignments_qna_tab.dart +++ b/apps/teacher_app/lib/presentation/screens/tabs/teacher_assignments_qna_tab.dart @@ -21,13 +21,16 @@ class TeacherAssignmentsQnATab extends StatefulWidget { const TeacherAssignmentsQnATab({super.key}); @override - State createState() => _TeacherAssignmentsQnATabState(); + State createState() => + _TeacherAssignmentsQnATabState(); } class _TeacherAssignmentsQnATabState extends State { - final TextEditingController _broadcastTitleCtrl = TextEditingController(text: 'ุชูˆุถูŠุญ ุจูŠุฏุงุบูˆุฌูŠ: ู…ุณุงุฑ ุงู„ุดุญู†ุฉ ููŠ ุงู„ูˆุนุงุก ุงู„ู…ุบู†ุงุทูŠุณูŠ'); + final TextEditingController _broadcastTitleCtrl = TextEditingController( + text: 'ุชูˆุถูŠุญ ุจูŠุฏุงุบูˆุฌูŠ: ู…ุณุงุฑ ุงู„ุดุญู†ุฉ ููŠ ุงู„ูˆุนุงุก ุงู„ู…ุบู†ุงุทูŠุณูŠ'); final TextEditingController _broadcastMessageCtrl = TextEditingController( - text: 'ุฃุนุฒุงุฆูŠ ุทู„ุจุฉ ุงู„ุตู ุงู„ุนุงุดุฑ: ุงู†ุชุจู‡ูˆุง ุฅู„ู‰ ุฃู† ู‚ูˆุฉ ู„ูˆุฑู†ุชุฒ ุชูƒูˆู† ุฏุงุฆู…ุงู‹ ุนู…ูˆุฏูŠุฉ ุนู„ู‰ ูƒู„ ู…ู† ู…ุชุฌู‡ ุงู„ุณุฑุนุฉ ูˆู…ุชุฌู‡ ุงู„ู…ุฌุงู„ุŒ ู„ุฐู„ูƒ ู„ุง ุชุจุฐู„ ุดุบู„ุงู‹ ูˆู„ุง ุชุบูŠู‘ุฑ ุงู„ุทุงู‚ุฉ ุงู„ุญุฑูƒูŠุฉุŒ ุจู„ ุชููˆุฌู‘ู‡ ุงู„ุฌุณูŠู… ููŠ ู…ุณุงุฑ ุญู„ุฒูˆู†ูŠ ู…ุญุตูˆุฑ ุฏุงุฎู„ ุงู„ูˆุนุงุก.', + text: + 'ุฃุนุฒุงุฆูŠ ุทู„ุจุฉ ุงู„ุตู ุงู„ุนุงุดุฑ: ุงู†ุชุจู‡ูˆุง ุฅู„ู‰ ุฃู† ู‚ูˆุฉ ู„ูˆุฑู†ุชุฒ ุชูƒูˆู† ุฏุงุฆู…ุงู‹ ุนู…ูˆุฏูŠุฉ ุนู„ู‰ ูƒู„ ู…ู† ู…ุชุฌู‡ ุงู„ุณุฑุนุฉ ูˆู…ุชุฌู‡ ุงู„ู…ุฌุงู„ุŒ ู„ุฐู„ูƒ ู„ุง ุชุจุฐู„ ุดุบู„ุงู‹ ูˆู„ุง ุชุบูŠู‘ุฑ ุงู„ุทุงู‚ุฉ ุงู„ุญุฑูƒูŠุฉุŒ ุจู„ ุชููˆุฌู‘ู‡ ุงู„ุฌุณูŠู… ููŠ ู…ุณุงุฑ ุญู„ุฒูˆู†ูŠ ู…ุญุตูˆุฑ ุฏุงุฎู„ ุงู„ูˆุนุงุก.', ); String _selectedBroadcastGrade = 'ุงู„ุตู ุงู„ุนุงุดุฑ ุงู„ุฃุณุงุณูŠ'; @@ -68,7 +71,8 @@ class _TeacherAssignmentsQnATabState extends State { className: 'ุงู„ุตู ุงู„ุนุงุดุฑ ุงู„ุฃุณุงุณูŠ (ุดุนุจุฉ ุฃ ูˆุจ)', dueDate: 'ุงู„ุฎู…ูŠุณ ุงู„ู‚ุงุฏู… ุงู„ุณุงุนุฉ 08:00 ู…', ); - SaqelToast.showSuccess(context, 'ุชู… ุฅุณู†ุงุฏ ูˆุฑู‚ุฉ ุงู„ุนู…ู„ ูˆุฅุดุนุงุฑ ุงู„ุทู„ุจุฉ ุจู†ุฌุงุญ ๐Ÿš€'); + SaqelToast.showSuccess( + context, 'ุชู… ุฅุณู†ุงุฏ ูˆุฑู‚ุฉ ุงู„ุนู…ู„ ูˆุฅุดุนุงุฑ ุงู„ุทู„ุจุฉ ุจู†ุฌุงุญ ๐Ÿš€'); }, child: const Text('ุฅุณู†ุงุฏ ู„ู„ุทู„ุจุฉ ุงู„ุขู†'), ), @@ -95,7 +99,8 @@ class _TeacherAssignmentsQnATabState extends State { decoration: const BoxDecoration( color: TeacherTheme.surfaceDark, 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( mainAxisSize: MainAxisSize.min, @@ -110,7 +115,8 @@ class _TeacherAssignmentsQnATabState extends State { color: TeacherTheme.emeraldPrimary.withOpacity(0.15), 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 Expanded( @@ -119,12 +125,16 @@ class _TeacherAssignmentsQnATabState extends State { children: [ Text( 'ุจุซ ู…ู‚ุทุน ุตูˆุชูŠ ูˆุฅุดุนุงุฑ ููˆุฑูŠ ู„ู„ุดุนุจุฉ ๐ŸŽ™๏ธ', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.w900, fontSize: 15), + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w900, + fontSize: 15), ), SizedBox(height: 2), Text( 'ูŠุตู„ ุงู„ุชุณุฌูŠู„ ุงู„ุตูˆุชูŠ ุนุจุฑ ุฎุงุฏู… ุงู„ูˆูŠุจ ุณูˆูƒุช (0ms) ู„ูƒุงูุฉ ุงู„ุทู„ุจุฉ ุงู„ู…ุดุชุฑูƒูŠู†', - style: TextStyle(color: Color(0xFF94A3B8), fontSize: 11), + style: + TextStyle(color: Color(0xFF94A3B8), fontSize: 11), ), ], ), @@ -133,18 +143,28 @@ class _TeacherAssignmentsQnATabState extends State { ), 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), _buildInputContainer( child: TextField( controller: _broadcastTitleCtrl, 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 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), _buildInputContainer( child: DropdownButtonHideUnderline( @@ -152,51 +172,79 @@ class _TeacherAssignmentsQnATabState extends State { value: _selectedBroadcastGrade, isExpanded: true, 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 [ - DropdownMenuItem(value: 'ุงู„ุตู ุงู„ุนุงุดุฑ ุงู„ุฃุณุงุณูŠ', 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))), + DropdownMenuItem( + value: 'ุงู„ุตู ุงู„ุนุงุดุฑ ุงู„ุฃุณุงุณูŠ', + 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) { - if (val != null) setState(() => _selectedBroadcastGrade = val); + if (val != null) + setState(() => _selectedBroadcastGrade = val); }, ), ), ), 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), _buildInputContainer( child: TextField( controller: _broadcastMessageCtrl, maxLines: 4, - style: const TextStyle(color: Colors.white, fontSize: 12.5, height: 1.4), - decoration: const InputDecoration(border: InputBorder.none, hintText: 'ุงูƒุชุจ ุงู„ุชูˆุฌูŠู‡ ุงู„ู…ูุงู‡ูŠู…ูŠ ู„ู„ุดุนุจุฉ...'), + style: const TextStyle( + color: Colors.white, fontSize: 12.5, height: 1.4), + decoration: const InputDecoration( + border: InputBorder.none, + hintText: 'ุงูƒุชุจ ุงู„ุชูˆุฌูŠู‡ ุงู„ู…ูุงู‡ูŠู…ูŠ ู„ู„ุดุนุจุฉ...'), ), ), const SizedBox(height: 16), // Voice Recording Strip Simulation Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + padding: + const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( color: const Color(0xFF0C241D), 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( children: [ - Icon(CupertinoIcons.mic_fill, color: TeacherTheme.emeraldPrimary, size: 18), + Icon(CupertinoIcons.mic_fill, + color: TeacherTheme.emeraldPrimary, size: 18), SizedBox(width: 10), Expanded( child: Text( 'ู…ู‚ุทุน ุตูˆุชูŠ ู…ุณุฌู„ ุจู†ู‚ุงุก ุนุงู„ูŠ: 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 { ElevatedButton.icon( onPressed: () async { Navigator.of(ctx).pop(); - final success = await context.read().broadcastAudioAnnouncement( + final success = await context + .read() + .broadcastAudioAnnouncement( title: _broadcastTitleCtrl.text.trim(), explanationText: _broadcastMessageCtrl.text.trim(), gradeLevel: _selectedBroadcastGrade, @@ -219,12 +269,15 @@ class _TeacherAssignmentsQnATabState extends State { } }, 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( backgroundColor: TeacherTheme.emeraldPrimary, foregroundColor: Colors.black, 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 { decoration: const BoxDecoration( color: TeacherTheme.surfaceCard, 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), child: Column( mainAxisSize: MainAxisSize.min, 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), Text( 'ุชุณุฌูŠู„ ุฑุฏ ุตูˆุชูŠ ุณู‚ุฑุงุทูŠ ู„ู„ุทุงู„ุจุฉ ($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 Text( @@ -267,7 +325,8 @@ class _TeacherAssignmentsQnATabState extends State { onPressed: () { context.read().sendVoiceReply( doubtId, - voiceText: 'ุฑุฏ ุงู„ุฃุณุชุงุฐ ุตูˆุชูŠุงู‹ ุนู„ู‰ ุงุณุชูุณุงุฑ ุงู„ุทุงู„ุจุฉ $studentName ุญูˆู„ ุงู„ูˆุนุงุก ุงู„ู…ุบู†ุงุทูŠุณูŠ ูˆุญุตุฑ ุงู„ุจู„ุงุฒู…ุง.', + voiceText: + 'ุฑุฏ ุงู„ุฃุณุชุงุฐ ุตูˆุชูŠุงู‹ ุนู„ู‰ ุงุณุชูุณุงุฑ ุงู„ุทุงู„ุจุฉ $studentName ุญูˆู„ ุงู„ูˆุนุงุก ุงู„ู…ุบู†ุงุทูŠุณูŠ ูˆุญุตุฑ ุงู„ุจู„ุงุฒู…ุง.', ); Navigator.of(ctx).pop(); SaqelToast.showSuccess( @@ -282,7 +341,8 @@ class _TeacherAssignmentsQnATabState extends State { backgroundColor: TeacherTheme.emeraldPrimary, foregroundColor: Colors.black, 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 { return BlocBuilder( builder: (context, state) { if (state.isLoading) { - return const Center(child: CupertinoActivityIndicator(color: TeacherTheme.emeraldPrimary)); + return const Center( + child: CupertinoActivityIndicator( + color: TeacherTheme.emeraldPrimary)); } return SingleChildScrollView( @@ -315,7 +377,8 @@ class _TeacherAssignmentsQnATabState extends State { end: Alignment.bottomRight, ), 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( color: Color(0x3310B981), @@ -334,7 +397,8 @@ class _TeacherAssignmentsQnATabState extends State { shape: BoxShape.circle, 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 Expanded( @@ -345,16 +409,21 @@ class _TeacherAssignmentsQnATabState extends State { children: [ 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), - 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), Text( 'ุจุซ ู…ู‚ุทุน ุตูˆุชูŠ ุญูŠ ูˆุดุฑุญ ู…ุจุงุดุฑ ู„ุฌู…ูŠุน ุทู„ุจุฉ ุงู„ุดุนุจุฉ ุงู„ู…ุดุชุฑูƒูŠู† (0ms)', - style: TextStyle(color: Color(0xFF94A3B8), fontSize: 11), + style: TextStyle( + color: Color(0xFF94A3B8), fontSize: 11), ), ], ), @@ -364,10 +433,14 @@ class _TeacherAssignmentsQnATabState extends State { style: ElevatedButton.styleFrom( backgroundColor: TeacherTheme.emeraldPrimary, foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + padding: const EdgeInsets.symmetric( + 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 { children: [ const Row( children: [ - Icon(CupertinoIcons.doc_text_fill, color: TeacherTheme.emeraldPrimary, size: 16), + Icon(CupertinoIcons.doc_text_fill, + color: TeacherTheme.emeraldPrimary, size: 16), SizedBox(width: 6), Text( 'ุฃูˆุฑุงู‚ ุงู„ุนู…ู„ ูˆุงู„ูˆุงุฌุจุงุช ุงู„ูˆุฒุงุฑูŠุฉ ๐Ÿ“', - style: TextStyle(fontSize: 14, fontWeight: FontWeight.w800, color: Colors.white), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w800, + color: Colors.white), ), ], ), TextButton.icon( onPressed: _dispatchNewHomework, - icon: const Icon(CupertinoIcons.plus_circle_fill, size: 15, color: TeacherTheme.emeraldPrimary), - label: const Text('ุฅุณู†ุงุฏ ูˆุงุฌุจ ุฌุฏูŠุฏ', style: TextStyle(color: TeacherTheme.emeraldPrimary, fontSize: 12, fontWeight: FontWeight.bold)), + icon: const Icon(CupertinoIcons.plus_circle_fill, + 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 { // Student Inquiries / Real Chat Header const Row( 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), 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 { decoration: BoxDecoration( color: TeacherTheme.surfaceDark, 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( crossAxisAlignment: CrossAxisAlignment.start, @@ -455,13 +544,18 @@ class _TeacherAssignmentsQnATabState extends State { Expanded( child: Text( 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( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), 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), ), child: Text( @@ -469,7 +563,9 @@ class _TeacherAssignmentsQnATabState extends State { style: TextStyle( fontSize: 10.5, 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 { const SizedBox(height: 6), Row( 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(), - 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 { color: TeacherTheme.surfaceDark, borderRadius: BorderRadius.circular(14), 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( @@ -517,7 +621,10 @@ class _TeacherAssignmentsQnATabState extends State { child: Center( child: Text( 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 { Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(doubt.studentName, 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.studentName, + 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), @@ -543,7 +658,8 @@ class _TeacherAssignmentsQnATabState extends State { ), child: Text( 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), @@ -552,7 +668,8 @@ class _TeacherAssignmentsQnATabState extends State { children: [ if (doubt.replied) Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: TeacherTheme.emeraldPrimary.withOpacity(0.15), borderRadius: BorderRadius.circular(6), @@ -560,22 +677,32 @@ class _TeacherAssignmentsQnATabState extends State { child: const Row( mainAxisSize: MainAxisSize.min, 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), - Text('ุชู… ุงู„ุฑุฏ ุจุตูˆุช ู…ุณุฌู„ ๐ŸŽง', style: TextStyle(color: TeacherTheme.emeraldPrimary, fontSize: 11, fontWeight: FontWeight.bold)), + Text('ุชู… ุงู„ุฑุฏ ุจุตูˆุช ู…ุณุฌู„ ๐ŸŽง', + style: TextStyle( + color: TeacherTheme.emeraldPrimary, + fontSize: 11, + fontWeight: FontWeight.bold)), ], ), ) else ElevatedButton.icon( - onPressed: () => _recordVoiceReply(doubt.id, doubt.studentName), + onPressed: () => + _recordVoiceReply(doubt.id, doubt.studentName), 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( backgroundColor: TeacherTheme.emeraldPrimary, foregroundColor: Colors.black, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8)), ), ), ], diff --git a/apps/teacher_app/lib/presentation/screens/tabs/teacher_monetization_tab.dart b/apps/teacher_app/lib/presentation/screens/tabs/teacher_monetization_tab.dart index 397a750..6aa74e1 100644 --- a/apps/teacher_app/lib/presentation/screens/tabs/teacher_monetization_tab.dart +++ b/apps/teacher_app/lib/presentation/screens/tabs/teacher_monetization_tab.dart @@ -31,9 +31,10 @@ class _TeacherMonetizationTabState extends State { context.read().loadMonetization(); } - void _showCliqPayoutDialog(BuildContext ctx, double availableBalance, String defaultAlias) { + void _showCliqPayoutDialog( + BuildContext ctx, double availableBalance, String defaultAlias) { final TextEditingController cliqController = TextEditingController( - text: defaultAlias.isNotEmpty ? defaultAlias : '0798583052@CLIQ', + text: defaultAlias, ); final TextEditingController amountController = TextEditingController( text: availableBalance > 0 ? availableBalance.toInt().toString() : '50', @@ -62,17 +63,22 @@ class _TeacherMonetizationTabState extends State { children: [ const Row( children: [ - Icon(CupertinoIcons.bolt_fill, color: TeacherTheme.emeraldPrimary, size: 22), + Icon(CupertinoIcons.bolt_fill, + color: TeacherTheme.emeraldPrimary, size: 22), SizedBox(width: 8), Text( 'ุทู„ุจ ุณุญุจ ููˆุฑูŠ ุนุจุฑ ูƒู„ูŠูƒ (CliQ)', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800, color: Colors.white), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: Colors.white), ), ], ), IconButton( onPressed: () => Navigator.pop(bCtx), - icon: const Icon(CupertinoIcons.xmark_circle_fill, color: Color(0xFF64748B)), + icon: const Icon(CupertinoIcons.xmark_circle_fill, + color: Color(0xFF64748B)), ), ], ), @@ -87,11 +93,15 @@ class _TeacherMonetizationTabState extends State { style: const TextStyle(color: Colors.white, fontSize: 13), decoration: InputDecoration( labelText: 'ู…ุนุฑู‘ู ูƒู„ูŠูƒ (CliQ Alias) ุฃูˆ ุฑู‚ู… ุงู„ู‡ุงุชู', - labelStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12), + labelStyle: + const TextStyle(color: Color(0xFF94A3B8), fontSize: 12), filled: true, fillColor: const Color(0xFF1E293B), - prefixIcon: const Icon(CupertinoIcons.person_crop_circle, color: TeacherTheme.emeraldPrimary, size: 20), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), + prefixIcon: const Icon(CupertinoIcons.person_crop_circle, + color: TeacherTheme.emeraldPrimary, size: 20), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none), ), ), const SizedBox(height: 12), @@ -101,11 +111,15 @@ class _TeacherMonetizationTabState extends State { style: const TextStyle(color: Colors.white, fontSize: 13), decoration: InputDecoration( labelText: 'ุงู„ู…ุจู„ุบ ุงู„ู…ุทู„ูˆุจ ุณุญุจู‡ (ุฏูŠู†ุงุฑ ุฃุฑุฏู†ูŠ)', - labelStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12), + labelStyle: + const TextStyle(color: Color(0xFF94A3B8), fontSize: 12), filled: true, fillColor: const Color(0xFF1E293B), - prefixIcon: const Icon(CupertinoIcons.money_dollar, color: TeacherTheme.emeraldPrimary, size: 20), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), + prefixIcon: const Icon(CupertinoIcons.money_dollar, + color: TeacherTheme.emeraldPrimary, size: 20), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none), ), ), const SizedBox(height: 18), @@ -121,18 +135,21 @@ class _TeacherMonetizationTabState extends State { Navigator.pop(bCtx); ScaffoldMessenger.of(ctx).showSnackBar( SnackBar( - content: Text('ุชู… ุฅุฏุฑุงุฌ ุทู„ุจ ุงู„ุณุญุจ ุจู…ุจู„ุบ ${amountController.text} ุฏ.ุฃ ููŠ ุทุงุจูˆุฑ ูƒู„ูŠูƒ ุจู†ุฌุงุญ โšก'), + content: Text( + 'ุชู… ุฅุฏุฑุงุฌ ุทู„ุจ ุงู„ุณุญุจ ุจู…ุจู„ุบ ${amountController.text} ุฏ.ุฃ ููŠ ุทุงุจูˆุฑ ูƒู„ูŠูƒ ุจู†ุฌุงุญ โšก'), backgroundColor: TeacherTheme.emeraldPrimary, ), ); }, icon: const Icon(CupertinoIcons.paperplane_fill, size: 16), - label: const Text('ุชุฃูƒูŠุฏ ูˆุฅุฑุณุงู„ ู„ุทุงุจูˆุฑ ุงู„ุณุญุจ ุงู„ููˆุฑูŠ (CliQ Queue)'), + label: + const Text('ุชุฃูƒูŠุฏ ูˆุฅุฑุณุงู„ ู„ุทุงุจูˆุฑ ุงู„ุณุญุจ ุงู„ููˆุฑูŠ (CliQ Queue)'), style: ElevatedButton.styleFrom( backgroundColor: TeacherTheme.emeraldPrimary, foregroundColor: Colors.black, 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 { } void _showIbanPayoutDialog(BuildContext ctx, double availableBalance) { - final TextEditingController ibanController = TextEditingController(text: 'JO94MEPA0000000000000000000000'); + final TextEditingController ibanController = + TextEditingController(text: 'JO94MEPA0000000000000000000000'); final TextEditingController amountController = TextEditingController( text: availableBalance > 0 ? availableBalance.toInt().toString() : '100', ); @@ -170,17 +188,22 @@ class _TeacherMonetizationTabState extends State { children: [ const Row( children: [ - Icon(CupertinoIcons.building_2_fill, color: TeacherTheme.cyberCyan, size: 22), + Icon(CupertinoIcons.building_2_fill, + color: TeacherTheme.cyberCyan, size: 22), SizedBox(width: 8), Text( 'ุทู„ุจ ุชุญูˆูŠู„ ุจู†ูƒูŠ ุฑุณู…ูŠ (IBAN Transfer)', - style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800, color: Colors.white), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w800, + color: Colors.white), ), ], ), IconButton( onPressed: () => Navigator.pop(bCtx), - icon: const Icon(CupertinoIcons.xmark_circle_fill, color: Color(0xFF64748B)), + icon: const Icon(CupertinoIcons.xmark_circle_fill, + color: Color(0xFF64748B)), ), ], ), @@ -195,11 +218,15 @@ class _TeacherMonetizationTabState extends State { style: const TextStyle(color: Colors.white, fontSize: 13), decoration: InputDecoration( labelText: 'ุฑู‚ู… ุงู„ุญุณุงุจ ุงู„ู…ุตุฑููŠ ุงู„ุฏูˆู„ูŠ (IBAN)', - labelStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12), + labelStyle: + const TextStyle(color: Color(0xFF94A3B8), fontSize: 12), filled: true, fillColor: const Color(0xFF1E293B), - prefixIcon: const Icon(CupertinoIcons.creditcard, color: TeacherTheme.cyberCyan, size: 20), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), + prefixIcon: const Icon(CupertinoIcons.creditcard, + color: TeacherTheme.cyberCyan, size: 20), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none), ), ), const SizedBox(height: 12), @@ -209,11 +236,15 @@ class _TeacherMonetizationTabState extends State { style: const TextStyle(color: Colors.white, fontSize: 13), decoration: InputDecoration( labelText: 'ุงู„ู…ุจู„ุบ ุงู„ู…ุทู„ูˆุจ ุชุญูˆูŠู„ู‡ (ุฏูŠู†ุงุฑ ุฃุฑุฏู†ูŠ)', - labelStyle: const TextStyle(color: Color(0xFF94A3B8), fontSize: 12), + labelStyle: + const TextStyle(color: Color(0xFF94A3B8), fontSize: 12), filled: true, fillColor: const Color(0xFF1E293B), - prefixIcon: const Icon(CupertinoIcons.money_dollar, color: TeacherTheme.cyberCyan, size: 20), - border: OutlineInputBorder(borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none), + prefixIcon: const Icon(CupertinoIcons.money_dollar, + color: TeacherTheme.cyberCyan, size: 20), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide.none), ), ), const SizedBox(height: 18), @@ -229,7 +260,8 @@ class _TeacherMonetizationTabState extends State { Navigator.pop(bCtx); ScaffoldMessenger.of(ctx).showSnackBar( SnackBar( - content: Text('ุชู… ุชุณุฌูŠู„ ุฃู…ุฑ ุงู„ุชุญูˆูŠู„ ุงู„ู…ุตุฑููŠ ุจู…ุจู„ุบ ${amountController.text} ุฏ.ุฃ ุจู†ุฌุงุญ ๐Ÿฆ'), + content: Text( + 'ุชู… ุชุณุฌูŠู„ ุฃู…ุฑ ุงู„ุชุญูˆูŠู„ ุงู„ู…ุตุฑููŠ ุจู…ุจู„ุบ ${amountController.text} ุฏ.ุฃ ุจู†ุฌุงุญ ๐Ÿฆ'), backgroundColor: TeacherTheme.cyberCyan, ), ); @@ -240,7 +272,8 @@ class _TeacherMonetizationTabState extends State { backgroundColor: TeacherTheme.cyberCyan, foregroundColor: Colors.black, 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 { children: [ Text( 'ุชุตู†ูŠู ู‚ุงุนุฏุฉ ุงู„ุทู„ุจุฉ (ุงู„ู…ุคุณุณูŠ ู…ู‚ุงุจู„ ุงู„ุฎุงุฑุฌูŠ) ๐Ÿ‘ฅ', - style: TextStyle(fontSize: 13.5, fontWeight: FontWeight.w800, color: Colors.white), + style: TextStyle( + fontSize: 13.5, + fontWeight: FontWeight.w800, + color: Colors.white), ), Text( 'ุชุญุฏูŠุซ ุงู„ูƒุดูˆูุงุช: ุงู„ูŠูˆู…', - style: TextStyle(fontSize: 11, color: Color(0xFF64748B)), + style: + TextStyle(fontSize: 11, color: Color(0xFF64748B)), ), ], ), @@ -288,7 +325,9 @@ class _TeacherMonetizationTabState extends State { decoration: BoxDecoration( color: const Color(0xFF064E3B).withOpacity(0.3), borderRadius: BorderRadius.circular(12), - border: Border.all(color: TeacherTheme.emeraldPrimary.withOpacity(0.3)), + border: Border.all( + color: + TeacherTheme.emeraldPrimary.withOpacity(0.3)), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -298,18 +337,26 @@ class _TeacherMonetizationTabState extends State { children: [ 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), Text( 'ู…ุดู…ูˆู„ูˆู† ุจุฑุณู… (0.00 ุฏ.ุฃ) ุถู…ู† ุงู„ุนู‚ุฏ ุงู„ู…ุคุณุณูŠ', - style: TextStyle(fontSize: 11, color: TeacherTheme.emeraldLight), + style: TextStyle( + fontSize: 11, + color: TeacherTheme.emeraldLight), ), ], ), Text( '${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 { decoration: BoxDecoration( color: const Color(0xFF0369A1).withOpacity(0.2), 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( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -330,18 +378,26 @@ class _TeacherMonetizationTabState extends State { children: [ 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), Text( 'ู…ุดุชุฑูƒูˆู† ุนุจุฑ ู†ุธุงู… ุงู„ุฏูุน ุงู„ููˆุฑูŠ ูƒู„ูŠูƒ (CliQ)', - style: TextStyle(fontSize: 11, color: TeacherTheme.cyberCyan), + style: TextStyle( + fontSize: 11, + color: TeacherTheme.cyberCyan), ), ], ), Text( '${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 { end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(20), - border: Border.all(color: TeacherTheme.emeraldPrimary.withOpacity(0.4)), + border: Border.all( + color: TeacherTheme.emeraldPrimary.withOpacity(0.4)), boxShadow: [ BoxShadow( color: TeacherTheme.emeraldPrimary.withOpacity(0.15), @@ -385,14 +442,18 @@ class _TeacherMonetizationTabState extends State { ), ), Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 3), decoration: BoxDecoration( color: Colors.black.withOpacity(0.3), borderRadius: BorderRadius.circular(6), ), child: const Text( '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 { const SizedBox(height: 4), Text( 'ุฅุฌู…ุงู„ูŠ ุงู„ู…ุจุงู„ุบ ุงู„ู…ุณุญูˆุจุฉ ุณุงุจู‚ุงู‹: ${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), @@ -420,32 +482,43 @@ class _TeacherMonetizationTabState extends State { Expanded( child: ElevatedButton.icon( 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( 'ุณุญุจ ููˆุฑูŠ ุนุจุฑ ูƒู„ูŠูƒ (CliQ)', - style: TextStyle(fontSize: 12, fontWeight: FontWeight.w800), + style: TextStyle( + fontSize: 12, fontWeight: FontWeight.w800), ), style: ElevatedButton.styleFrom( backgroundColor: TeacherTheme.emeraldPrimary, foregroundColor: Colors.black, padding: const EdgeInsets.symmetric(vertical: 12), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), ), ), ), const SizedBox(width: 8), OutlinedButton.icon( onPressed: () { - _showIbanPayoutDialog(context, state.availableBalance); + _showIbanPayoutDialog( + context, state.availableBalance); }, - icon: const Icon(CupertinoIcons.building_2_fill, size: 16, color: Colors.white), - label: const Text('ุชุญูˆูŠู„ ุจู†ูƒูŠ', style: TextStyle(fontSize: 12, color: Colors.white)), + icon: const Icon(CupertinoIcons.building_2_fill, + size: 16, color: Colors.white), + label: const Text('ุชุญูˆูŠู„ ุจู†ูƒูŠ', + style: + TextStyle(fontSize: 12, color: Colors.white)), style: OutlinedButton.styleFrom( - side: BorderSide(color: Colors.white.withOpacity(0.3)), - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 12), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + side: BorderSide( + color: Colors.white.withOpacity(0.3)), + padding: const EdgeInsets.symmetric( + vertical: 12, horizontal: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10)), ), ), ], @@ -495,33 +568,22 @@ class _TeacherMonetizationTabState extends State { TeacherTheme.cyberCyan, ), - const Divider(color: TeacherTheme.surfaceBorder, height: 24), + const Divider( + color: TeacherTheme.surfaceBorder, height: 24), - // Subscribers Slider Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( - 'ู…ุญุงูƒูŠ ุงู„ู…ุดุชุฑูƒูŠู† ุฎุงุฑุฌ ุงู„ุซู‚ุงูุฉ:', - style: TextStyle(fontSize: 12, color: Color(0xFF94A3B8)), - ), - Text( - '${state.studentSubscribers.toInt()} ุทุงู„ุจ ู…ุดุชุฑูƒ', - style: const TextStyle(fontSize: 13.5, fontWeight: FontWeight.w800, color: TeacherTheme.emeraldLight), - ), + const Text('ุงู„ู…ุดุชุฑูƒูˆู† ุงู„ูุนู„ูŠูˆู†:', + style: TextStyle( + fontSize: 12, color: Color(0xFF94A3B8))), + Text('${state.studentSubscribers.toInt()} ุทุงู„ุจ ู…ุดุชุฑูƒ', + style: const TextStyle( + fontSize: 13.5, + 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().updateSubscribers(val); - }, - ), ], ), ), @@ -530,19 +592,26 @@ class _TeacherMonetizationTabState extends State { // Active Courses Selling 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), if (state.courses.isNotEmpty) - ...state.courses.map((course) => Padding( - padding: const EdgeInsets.only(bottom: 8), - child: _courseEarningCard( - title: course.title, - students: course.militaryStudents + course.externalSubscribers, - revenue: '${(course.priceJod * (course.externalSubscribers > 0 ? course.externalSubscribers : 1) * 0.55).toStringAsFixed(0)} ุฏ.ุฃ', - ), - )).toList() + ...state.courses + .map((course) => Padding( + padding: const EdgeInsets.only(bottom: 8), + child: _courseEarningCard( + title: course.title, + students: course.militaryStudents + + course.externalSubscribers, + revenue: + '${(course.priceJod * (course.externalSubscribers > 0 ? course.externalSubscribers : 1) * 0.55).toStringAsFixed(0)} ุฏ.ุฃ', + ), + )) + .toList() else ...[ _courseEarningCard( title: 'ุงู„ููŠุฒูŠุงุก ูˆุงู„ุนู„ูˆู… ุงู„ุชุทุจูŠู‚ูŠุฉ โ€” ุงู„ุตู ุงู„ุนุงุดุฑ ุงู„ุฃุณุงุณูŠ', @@ -556,11 +625,15 @@ class _TeacherMonetizationTabState extends State { const SizedBox(height: 18), const Row( 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), Text( 'ุณุฌู„ ุนู…ู„ูŠุงุช ูˆุณุญูˆุจุงุช ูƒู„ูŠูƒ (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 { color: TeacherTheme.surfaceCard, borderRadius: BorderRadius.circular(12), border: Border.all( - color: isDone ? TeacherTheme.emeraldPrimary.withOpacity(0.3) : TeacherTheme.surfaceBorder, + color: isDone + ? TeacherTheme.emeraldPrimary.withOpacity(0.3) + : TeacherTheme.surfaceBorder, ), ), child: Row( @@ -592,9 +667,13 @@ class _TeacherMonetizationTabState extends State { Row( children: [ Icon( - isDone ? CupertinoIcons.checkmark_circle_fill : CupertinoIcons.clock_fill, + isDone + ? CupertinoIcons.checkmark_circle_fill + : CupertinoIcons.clock_fill, size: 20, - color: isDone ? TeacherTheme.emeraldPrimary : TeacherTheme.royalGold, + color: isDone + ? TeacherTheme.emeraldPrimary + : TeacherTheme.royalGold, ), const SizedBox(width: 10), Column( @@ -602,12 +681,16 @@ class _TeacherMonetizationTabState extends State { children: [ Text( 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), Text( 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 { style: TextStyle( fontSize: 13.5, fontWeight: FontWeight.w900, - color: isDone ? TeacherTheme.emeraldPrimary : TeacherTheme.royalGold, + color: isDone + ? TeacherTheme.emeraldPrimary + : TeacherTheme.royalGold, ), ), Text( isDone ? 'ุชู… ุงู„ุชุญูˆูŠู„ ุจู†ุฌุงุญ' : 'ููŠ ุทุงุจูˆุฑ ุงู„ู…ู‚ุงุตุฉ โšก', style: TextStyle( fontSize: 10, - color: isDone ? TeacherTheme.emeraldLight : TeacherTheme.royalGold, + color: isDone + ? TeacherTheme.emeraldLight + : TeacherTheme.royalGold, ), ), ], @@ -644,12 +731,19 @@ class _TeacherMonetizationTabState extends State { children: [ Row( 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), - 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 { child: Column( crossAxisAlignment: CrossAxisAlignment.start, 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), - Text('$students ุทุงู„ุจ ู…ุดุชุฑูƒ ยท ุฑุณูˆู… 20 ุฏ.ุฃ', style: const TextStyle(fontSize: 11, color: Color(0xFF94A3B8))), + Text('$students ุทุงู„ุจ ู…ุดุชุฑูƒ ยท ุฑุณูˆู… 20 ุฏ.ุฃ', + style: const TextStyle( + fontSize: 11, color: Color(0xFF94A3B8))), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ - Text(revenue, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w900, color: TeacherTheme.emeraldPrimary)), - const Text('ุตุงููŠ ุฃุฑุจุงุญูƒ', style: TextStyle(fontSize: 10, color: Color(0xFF64748B))), + Text(revenue, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w900, + color: TeacherTheme.emeraldPrimary)), + const Text('ุตุงููŠ ุฃุฑุจุงุญูƒ', + style: TextStyle(fontSize: 10, color: Color(0xFF64748B))), ], ), ], diff --git a/apps/teacher_app/lib/presentation/screens/tabs/teacher_reputation_scorecard_tab.dart b/apps/teacher_app/lib/presentation/screens/tabs/teacher_reputation_scorecard_tab.dart index 911cb89..2b49401 100644 --- a/apps/teacher_app/lib/presentation/screens/tabs/teacher_reputation_scorecard_tab.dart +++ b/apps/teacher_app/lib/presentation/screens/tabs/teacher_reputation_scorecard_tab.dart @@ -20,22 +20,24 @@ class TeacherReputationScorecardTab extends StatefulWidget { const TeacherReputationScorecardTab({super.key}); @override - State createState() => _TeacherReputationScorecardTabState(); + State createState() => + _TeacherReputationScorecardTabState(); } -class _TeacherReputationScorecardTabState extends State { +class _TeacherReputationScorecardTabState + extends State { final TeacherRepository _repository = TeacherRepository(); - bool _isLoading = true; - int _studentsEnrolled = 83; - double _compositeMerit = 96.5; - double _starRating = 4.92; - double _successRate = 98.2; - double _curriculumAlignment = 0.96; - double _socraticInteraction = 0.89; - double _audioClarity = 0.94; - double _cognitiveFocus = 0.92; - String _reputationTier = 'ู…ุนู„ู… ู†ุฎุจูˆูŠ ู…ุนุชู…ุฏ ๐Ÿ’Ž'; - String _aiGuidance = 'ุชูˆุฌูŠู‡ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ ุงู„ุฃุณุจูˆุนูŠ: ู†ุณุจุฉ ุงู„ุงู„ุชุฒุงู… ุงู„ูˆุฒุงุฑูŠ ู…ู…ุชุงุฒุฉ (96%). ูŠููˆุตู‰ ุจุฅุถุงูุฉ ูˆู‚ูุฉ ุณู‚ุฑุงุทูŠุฉ ุงุณุชู†ุชุงุฌูŠุฉ ููŠ ุงู„ุฏู‚ูŠู‚ุฉ 14 ู…ู† ุงู„ุฏุฑุณ ุงู„ู‚ุงุฏู… ู„ุชุนุฒูŠุฒ ุชูุงุนู„ ุงู„ุทู„ุจุฉ.'; + int _studentsEnrolled = 0; + double _compositeMerit = 0; + double _starRating = 0; + double _successRate = 0; + double _curriculumAlignment = 0; + double _socraticInteraction = 0; + double _audioClarity = 0; + double _cognitiveFocus = 0; + String _reputationTier = 'ุจุงู†ุชุธุงุฑ ุจูŠุงู†ุงุช ุงู„ุฃุฏุงุก'; + String _aiGuidance = + 'ุณุชุธู‡ุฑ ุงู„ุชูˆุตูŠุฉ ุจุนุฏ ุชูˆูุฑ ุญุตุต ู…ุญู„ู„ุฉ ูˆุชูุงุนู„ ูุนู„ูŠ ู…ู† ุงู„ุทู„ุจุฉ.'; @override void initState() { @@ -48,28 +50,38 @@ class _TeacherReputationScorecardTabState extends State _isLoading = false); + // Keep the honest zero-state when no reputation data is available. } } @override Widget build(BuildContext context) { final authState = context.watch().state; - final teacherName = (authState is TeacherAuthenticated) ? authState.profile.name : 'ุงู„ู…ู‡ู†ุฏุณ ุญู…ุฒุฉ ุงู„ุบูˆูŠุฑูŠูŠู†'; + final teacherName = + (authState is TeacherAuthenticated) ? authState.profile.name : 'ุงู„ู…ุนู„ู…'; return RefreshIndicator( onRefresh: _loadReputation, @@ -91,14 +103,16 @@ class _TeacherReputationScorecardTabState extends State createState() => _TeacherStudioUploadTabState(); } +class _CurriculumSelectors extends StatelessWidget { + final TeacherStudioState state; + final VoidCallback onLessonChanged; + const _CurriculumSelectors( + {required this.state, required this.onLessonChanged}); + + Map _map(dynamic value) => + value is Map ? Map.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().selectGrade(v)), + _drop( + 'ุงู„ู…ุจุญุซ', + state.subjectKey, + subjects.entries.map((e) => MapEntry(e.key, _name(e.value, e.key))), + (v) => context.read().selectSubject(v)), + _drop( + 'ุงู„ูุตู„', + state.semesterKey, + semesters.entries + .map((e) => MapEntry(e.key, _name(e.value, e.key))), + (v) => context.read().selectSemester(v)), + _drop( + 'ุงู„ูˆุญุฏุฉ', + state.unitKey, + units.entries.map((e) => MapEntry(e.key, _name(e.value, e.key))), + (v) => context.read().selectUnit(v)), + _drop( + 'ุงู„ุฏุฑุณ', + state.lessonKey, + lessons.whereType().map((e) => MapEntry( + e['id']?.toString() ?? '', e['title']?.toString() ?? '')), (v) { + context.read().selectLesson(v); + onLessonChanged(); + }), + ], + ); + } + + Widget _drop( + String label, + String value, + Iterable> entries, + ValueChanged changed) { + final items = entries.where((e) => e.key.isNotEmpty).toList(); + return SizedBox( + width: 260, + child: DropdownButtonFormField( + 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 { late final TextEditingController _titleController; @@ -33,6 +130,13 @@ class _TeacherStudioUploadTabState extends State { _titleController = TextEditingController( text: context.read().state.lessonTitle, ); + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!mounted) return; + await context.read().loadCurriculum(); + if (mounted) + _titleController.text = + context.read().state.lessonTitle; + }); } @override @@ -42,21 +146,23 @@ class _TeacherStudioUploadTabState extends State { } Future _pickVideo(BuildContext ctx) async { - final result = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: const ['mp4', 'mov', 'webm'], - withData: kIsWeb, - ); - if (!mounted || result == null || result.files.isEmpty) return; - - final file = result.files.single; - ctx.read().selectVideoFile( + try { + final result = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: const ['mp4', 'mov', 'webm'], + withData: kIsWeb); + if (!mounted || result == null || result.files.isEmpty) return; + final file = result.files.single; + ctx.read().selectVideoFile( file.name, file.size / (1024 * 1024), ctx.read().state.durationMinutes, path: file.path, - bytes: file.bytes, - ); + bytes: file.bytes); + } catch (e) { + if (mounted) + ctx.read().reportError('ุชุนุฐุฑ ูุชุญ ุงู„ู…ู„ู: $e'); + } } @override @@ -80,14 +186,16 @@ class _TeacherStudioUploadTabState extends State { end: Alignment.bottomRight, ), 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( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ - Icon(CupertinoIcons.sparkles, color: TeacherTheme.emeraldLight, size: 20), + Icon(CupertinoIcons.sparkles, + color: TeacherTheme.emeraldLight, size: 20), SizedBox(width: 8), Text( 'ุจูˆุงุจุฉ ุฌูˆุฏุฉ ุญุตุต ุงู„ุฃุณุชูˆุฏูŠูˆ ุงู„ุฑู‚ู…ูŠุฉ (ุตูŽู‚ูู„ 2.0)', @@ -135,19 +243,35 @@ class _TeacherStudioUploadTabState extends State { const SizedBox(height: 8), TextField( controller: _titleController, - style: const TextStyle(color: Colors.white, fontSize: 13.5), - onChanged: (val) => context.read().updateLessonTitle(val), + style: + const TextStyle(color: Colors.white, fontSize: 13.5), + onChanged: (val) => context + .read() + .updateLessonTitle(val), decoration: InputDecoration( filled: true, fillColor: const Color(0xFF161F30), border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide(color: Color(0xFF334155)), + borderSide: + const BorderSide(color: Color(0xFF334155)), ), ), ), const SizedBox(height: 16), + _CurriculumSelectors( + state: state, + onLessonChanged: () { + final title = context + .read() + .state + .lessonTitle; + _titleController.text = title; + }, + ), + const SizedBox(height: 16), + // Video Attachment Dropzone Card const Text( 'ู…ู„ู ููŠุฏูŠูˆ ุงู„ุญุตุฉ ู„ู„ุดุฑุญ ุงู„ุฑู‚ู…ูŠ (1080p):', @@ -164,7 +288,9 @@ class _TeacherStudioUploadTabState extends State { color: const Color(0xFF161F30), borderRadius: BorderRadius.circular(12), 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, ), ), @@ -174,12 +300,17 @@ class _TeacherStudioUploadTabState extends State { width: 44, height: 44, 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), ), child: Icon( CupertinoIcons.videocam_fill, - color: state.selectedFileName != null ? TeacherTheme.emeraldLight : const Color(0xFF94A3B8), + color: state.selectedFileName != null + ? TeacherTheme.emeraldLight + : const Color(0xFF94A3B8), size: 24, ), ), @@ -189,29 +320,44 @@ class _TeacherStudioUploadTabState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - state.selectedFileName ?? 'ู„ู… ูŠุชู… ุงุฎุชูŠุงุฑ ู…ู„ู ููŠุฏูŠูˆ ุจุนุฏ', - style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13), + state.selectedFileName ?? + 'ู„ู… ูŠุชู… ุงุฎุชูŠุงุฑ ู…ู„ู ููŠุฏูŠูˆ ุจุนุฏ', + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 13), ), const SizedBox(height: 2), Text( state.selectedFileName != null - ? 'ุงู„ุญุฌู…: ${state.selectedFileSizeMb ?? 48.2} MB โ€ข ุงู„ู…ุฏุฉ ุงู„ู…ุณุชุฎุฑุฌุฉ: ${state.durationMinutes.toStringAsFixed(1)} ุฏู‚ูŠู‚ุฉ' + ? 'ุงู„ุญุฌู…: ${state.selectedFileSizeMb?.toStringAsFixed(2)} MB โ€ข ุฃุฏุฎู„ ุงู„ู…ุฏุฉ ุงู„ูุนู„ูŠุฉ ุฃุฏู†ุงู‡' : 'ุงู„ุตูŠุบ ุงู„ู…ุฏุนูˆู…ุฉ: MP4, MOV, WebM (ุฌูˆุฏุฉ 1080p)', - style: const TextStyle(color: Color(0xFF94A3B8), fontSize: 11), + style: const TextStyle( + color: Color(0xFF94A3B8), fontSize: 11), ), ], ), ), ElevatedButton.icon( onPressed: () => _pickVideo(context), - icon: const Icon(CupertinoIcons.folder_badge_plus, size: 15), - label: Text(state.selectedFileName != null ? 'ุชุบูŠูŠุฑ ุงู„ู…ู„ู' : 'ุงุฎุชูŠุงุฑ ู…ู„ู ๐Ÿ“'), + icon: const Icon(CupertinoIcons.folder_badge_plus, + size: 15), + label: Text(state.selectedFileName != null + ? 'ุชุบูŠูŠุฑ ุงู„ู…ู„ู' + : 'ุงุฎุชูŠุงุฑ ู…ู„ู ๐Ÿ“'), style: ElevatedButton.styleFrom( - backgroundColor: state.selectedFileName != null ? const Color(0xFF1E293B) : TeacherTheme.emeraldPrimary, - foregroundColor: state.selectedFileName != null ? Colors.white : Colors.black, - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), - textStyle: const TextStyle(fontSize: 11.5, fontWeight: FontWeight.bold), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + backgroundColor: state.selectedFileName != null + ? const Color(0xFF1E293B) + : TeacherTheme.emeraldPrimary, + foregroundColor: state.selectedFileName != null + ? Colors.white + : Colors.black, + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 8), + textStyle: const TextStyle( + fontSize: 11.5, fontWeight: FontWeight.bold), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8)), ), ), ], @@ -232,7 +378,8 @@ class _TeacherStudioUploadTabState extends State { ), ), Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 4), decoration: BoxDecoration( color: isDurationGood ? TeacherTheme.emeraldPrimary.withOpacity(0.2) @@ -244,7 +391,9 @@ class _TeacherStudioUploadTabState extends State { style: TextStyle( fontSize: 13, fontWeight: FontWeight.w900, - color: isDurationGood ? TeacherTheme.emeraldLight : TeacherTheme.crimsonRed, + color: isDurationGood + ? TeacherTheme.emeraldLight + : TeacherTheme.crimsonRed, ), ), ), @@ -255,7 +404,9 @@ class _TeacherStudioUploadTabState extends State { min: 5.0, max: 45.0, divisions: 40, - activeColor: isDurationGood ? TeacherTheme.emeraldPrimary : TeacherTheme.crimsonRed, + activeColor: isDurationGood + ? TeacherTheme.emeraldPrimary + : TeacherTheme.crimsonRed, inactiveColor: TeacherTheme.surfaceBorder, onChanged: (val) { context.read().updateDuration(val); @@ -283,7 +434,9 @@ class _TeacherStudioUploadTabState extends State { ? CupertinoIcons.check_mark_circled_solid : CupertinoIcons.exclamationmark_triangle_fill, size: 18, - color: isDurationGood ? TeacherTheme.emeraldLight : TeacherTheme.crimsonRed, + color: isDurationGood + ? TeacherTheme.emeraldLight + : TeacherTheme.crimsonRed, ), const SizedBox(width: 8), Expanded( @@ -293,7 +446,9 @@ class _TeacherStudioUploadTabState extends State { : 'ุชุญุฐูŠุฑ ุฅุฏุฑุงูƒูŠ: ุงู„ุดุฑุญ ูŠุชุฌุงูˆุฒ 25 ุฏู‚ูŠู‚ุฉ! ูŠู†ุฎูุถ ุงุณุชูŠุนุงุจ ุงู„ุทู„ุจุฉ ุชุฏุฑูŠุฌูŠุงู‹ุŒ ูŠุฑุฌู‰ ุงุฎุชุตุงุฑ ุงู„ู…ู‚ุทุน ุฃูˆ ุชู‚ุณูŠู…ู‡.', style: TextStyle( 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 { onPressed: state.isAuditing ? null : () { - context.read().runQualityGate(); + context + .read() + .runQualityGate(); }, icon: state.isAuditing ? const SizedBox( width: 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( state.isAuditing ? 'ุฌุงุฑูŠ ุงู„ุชุฏู‚ูŠู‚ ุงู„ุชุฑุจูˆูŠ ุนุจุฑ ู…ุญุฑูƒ ุตูŽู‚ูู„...' : 'ูุญุต ุงู„ุญุตุฉ ุนุจุฑ ุจูˆุงุจุฉ ุงู„ุฌูˆุฏุฉ ุงู„ุฐูƒูŠุฉ (ุนุชุจุฉ 85%) โšก', - style: const TextStyle(fontSize: 13.5, fontWeight: FontWeight.w800), + style: const TextStyle( + fontSize: 13.5, fontWeight: FontWeight.w800), ), style: ElevatedButton.styleFrom( backgroundColor: TeacherTheme.emeraldPrimary, @@ -331,13 +491,22 @@ class _TeacherStudioUploadTabState extends State { ), ), ), + 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), // 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 { ); } - Widget _buildAuditResultCard(BuildContext context, TeacherStudioState state, TeacherLessonAuditModel res) { + Widget _buildAuditResultCard(BuildContext context, TeacherStudioState state, + TeacherLessonAuditModel res) { final int score = res.qualityScore; final bool isApproved = score >= 85; @@ -355,7 +525,9 @@ class _TeacherStudioUploadTabState extends State { color: TeacherTheme.surfaceCard, borderRadius: BorderRadius.circular(16), border: Border.all( - color: isApproved ? TeacherTheme.emeraldPrimary : TeacherTheme.crimsonRed, + color: isApproved + ? TeacherTheme.emeraldPrimary + : TeacherTheme.crimsonRed, width: 1.5, ), ), @@ -368,23 +540,32 @@ class _TeacherStudioUploadTabState extends State { Row( children: [ Icon( - isApproved ? CupertinoIcons.checkmark_seal_fill : CupertinoIcons.xmark_seal_fill, - color: isApproved ? TeacherTheme.emeraldPrimary : TeacherTheme.crimsonRed, + isApproved + ? CupertinoIcons.checkmark_seal_fill + : CupertinoIcons.xmark_seal_fill, + color: isApproved + ? TeacherTheme.emeraldPrimary + : TeacherTheme.crimsonRed, size: 22, ), const SizedBox(width: 8), Text( - isApproved ? 'ุฌุงู‡ุฒุฉ ู„ุฑูุน ุงู„ู…ู„ู ูˆุงู„ุชุญู„ูŠู„' : 'ุจูŠุงู†ุงุช ุงู„ุญุตุฉ ุจุญุงุฌุฉ ู„ู„ู…ุฑุงุฌุนุฉ', + isApproved + ? 'ุฌุงู‡ุฒุฉ ู„ุฑูุน ุงู„ู…ู„ู ูˆุงู„ุชุญู„ูŠู„' + : 'ุจูŠุงู†ุงุช ุงู„ุญุตุฉ ุจุญุงุฌุฉ ู„ู„ู…ุฑุงุฌุนุฉ', style: TextStyle( fontSize: 15, fontWeight: FontWeight.w900, - color: isApproved ? TeacherTheme.emeraldPrimary : TeacherTheme.crimsonRed, + color: isApproved + ? TeacherTheme.emeraldPrimary + : TeacherTheme.crimsonRed, ), ), ], ), Container( - padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + padding: + const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: isApproved ? TeacherTheme.emeraldPrimary.withOpacity(0.2) @@ -396,7 +577,9 @@ class _TeacherStudioUploadTabState extends State { style: TextStyle( fontSize: 13, fontWeight: FontWeight.w900, - color: isApproved ? TeacherTheme.emeraldLight : TeacherTheme.crimsonRed, + color: isApproved + ? TeacherTheme.emeraldLight + : TeacherTheme.crimsonRed, ), ), ), @@ -419,7 +602,8 @@ class _TeacherStudioUploadTabState extends State { const SizedBox(height: 6), _rowMetric('ู†ู‚ุงุก ุงู„ุตูˆุช ูˆู…ุฎุงุฑุฌ ุงู„ุญุฑูˆู:', res.audioClarity), const SizedBox(height: 6), - _rowMetric('ุงู„ููˆุงุตู„ ุงู„ุณู‚ุฑุงุทูŠุฉ ุงู„ุชูุงุนู„ูŠุฉ:', '${res.socraticStopsCount} ู…ุญุทุงุช ุชููƒูŠุฑูŠุฉ ุฅู„ุฒุงู…ูŠุฉ'), + _rowMetric('ุงู„ููˆุงุตู„ ุงู„ุณู‚ุฑุงุทูŠุฉ ุงู„ุชูุงุนู„ูŠุฉ:', + '${res.socraticStopsCount} ู…ุญุทุงุช ุชููƒูŠุฑูŠุฉ ุฅู„ุฒุงู…ูŠุฉ'), const SizedBox(height: 16), if (isApproved) ...[ @@ -430,11 +614,19 @@ class _TeacherStudioUploadTabState extends State { child: const Row( mainAxisAlignment: MainAxisAlignment.center, 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), Text( 'ุฌุงุฑูŠ ุฑูุน ุงู„ููŠุฏูŠูˆ ูˆุจุฏุก ู…ุนุงู„ุฌุฉ 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 { ), child: Row( 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), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - state.uploadSuccessMessage ?? 'ุชู… ุฑูุน ูˆู†ุดุฑ ุงู„ุญุตุฉ ุจู†ุฌุงุญ ููŠ ุงู„ู…ู†ู‡ุงุฌ ูˆุณูˆู‚ ุตูŽู‚ูู„! ๐Ÿš€', - style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w900), + state.uploadSuccessMessage ?? + 'ุชู… ุฑูุน ูˆู†ุดุฑ ุงู„ุญุตุฉ ุจู†ุฌุงุญ ููŠ ุงู„ู…ู†ู‡ุงุฌ ูˆุณูˆู‚ ุตูŽู‚ูู„! ๐Ÿš€', + style: const TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.w900), ), const SizedBox(height: 2), const Text( 'ุงู„ุญุตุฉ ู…ุชุงุญุฉ ุงู„ุขู† ู„ุทู„ุจุฉ ุงู„ุตู ุงู„ุนุงุดุฑ ูˆุณูˆู‚ ุงู„ุชุณูŠูŠู„ ุงู„ุชุฌุงุฑูŠ ๐ŸŽ–๏ธ', - style: TextStyle(color: TeacherTheme.emeraldLight, fontSize: 11), + style: TextStyle( + color: TeacherTheme.emeraldLight, fontSize: 11), ), ], ), @@ -476,12 +674,15 @@ class _TeacherStudioUploadTabState extends State { context.read().uploadAndPublishLesson(); }, 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( backgroundColor: TeacherTheme.emeraldDark, foregroundColor: Colors.white, minimumSize: const Size(double.infinity, 46), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12)), elevation: 0, ), ), @@ -495,8 +696,13 @@ class _TeacherStudioUploadTabState extends State { return Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF94A3B8))), - Text('$value', style: const TextStyle(fontSize: 12.5, fontWeight: FontWeight.w700, color: Color(0xFFCBD5E1))), + Text(label, + style: const TextStyle(fontSize: 12, color: Color(0xFF94A3B8))), + Text('$value', + style: const TextStyle( + fontSize: 12.5, + fontWeight: FontWeight.w700, + color: Color(0xFFCBD5E1))), ], ); } diff --git a/apps/teacher_app/lib/presentation/screens/teacher_main_shell.dart b/apps/teacher_app/lib/presentation/screens/teacher_main_shell.dart index ebc0090..00eaa81 100644 --- a/apps/teacher_app/lib/presentation/screens/teacher_main_shell.dart +++ b/apps/teacher_app/lib/presentation/screens/teacher_main_shell.dart @@ -32,7 +32,8 @@ class TeacherMainShell extends StatefulWidget { class _TeacherMainShellState extends State { int _currentTabIndex = 0; - void _openEditProfileDialog(BuildContext context, TeacherProfileModel profile) { + void _openEditProfileDialog( + BuildContext context, TeacherProfileModel profile) { final nameCtrl = TextEditingController(text: profile.name); final subjectCtrl = TextEditingController(text: profile.specialization); final schoolCtrl = TextEditingController(text: profile.schoolName); @@ -64,7 +65,9 @@ class _TeacherMainShellState extends State { decoration: const BoxDecoration( color: TeacherTheme.surfaceDark, 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: Column( @@ -73,37 +76,56 @@ class _TeacherMainShellState extends State { children: [ Row( 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 Text( 'ุชุญุฏูŠุซ ุจูŠุงู†ุงุช ุงู„ู…ุนู„ู… ูˆุงู„ุตููˆู ูˆุงู„ู…ุจุงุญุซ ๐Ÿ“‹', - style: TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w800), + style: TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.w800), ), const Spacer(), IconButton( 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 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), _buildInputBox(controller: nameCtrl, hint: 'ุงู„ุงุณู… ุงู„ูƒุงู…ู„'), 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), - _buildInputBox(controller: subjectCtrl, hint: 'ุงู„ู…ุงุฏุฉ (ู…ุซุงู„: ุงู„ููŠุฒูŠุงุก)'), + _buildInputBox( + controller: subjectCtrl, hint: 'ุงู„ู…ุงุฏุฉ (ู…ุซุงู„: ุงู„ููŠุฒูŠุงุก)'), 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), - _buildInputBox(controller: schoolCtrl, hint: 'ุงู„ู…ุฏุฑุณุฉ ุงู„ุชุงุจุน ู„ู‡ุง'), + _buildInputBox( + controller: schoolCtrl, hint: 'ุงู„ู…ุฏุฑุณุฉ ุงู„ุชุงุจุน ู„ู‡ุง'), const SizedBox(height: 14), - - 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: 8), Wrap( spacing: 8, @@ -112,11 +134,18 @@ class _TeacherMainShellState extends State { final isSelected = selectedGrades.contains(grade); return FilterChip( 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, backgroundColor: TeacherTheme.surfaceCard, checkmarkColor: Colors.black, - side: BorderSide(color: isSelected ? TeacherTheme.emeraldPrimary : TeacherTheme.surfaceBorder), + side: BorderSide( + color: isSelected + ? TeacherTheme.emeraldPrimary + : TeacherTheme.surfaceBorder), onSelected: (selected) { setModalState(() { if (selected) { @@ -132,7 +161,6 @@ class _TeacherMainShellState extends State { }).toList(), ), const SizedBox(height: 20), - ElevatedButton( onPressed: () { context.read().updateProfile( @@ -143,15 +171,19 @@ class _TeacherMainShellState extends State { bio: bioCtrl.text.trim(), ); Navigator.of(ctx).pop(); - SaqelToast.showSuccess(context, 'ุชู… ุชุญุฏูŠุซ ุจูŠุงู†ุงุช ุงู„ู…ุนู„ู… ูˆุงู„ุตููˆู ุจู†ุฌุงุญ โœจ'); + SaqelToast.showSuccess( + context, 'ุชู… ุชุญุฏูŠุซ ุจูŠุงู†ุงุช ุงู„ู…ุนู„ู… ูˆุงู„ุตููˆู ุจู†ุฌุงุญ โœจ'); }, style: ElevatedButton.styleFrom( backgroundColor: TeacherTheme.emeraldPrimary, foregroundColor: Colors.black, 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 { ); } - Widget _buildInputBox({required TextEditingController controller, required String hint}) { + Widget _buildInputBox( + {required TextEditingController controller, required String hint}) { return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 2), decoration: BoxDecoration( @@ -173,7 +206,10 @@ class _TeacherMainShellState extends State { child: TextField( controller: controller, 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 { textDirection: TextDirection.rtl, child: CupertinoAlertDialog( title: const Text('ุชุณุฌูŠู„ ุงู„ุฎุฑูˆุฌ ู…ู† ุงุณุชูˆุฏูŠูˆ ุงู„ู…ุนู„ู… ๐Ÿšช'), - content: const Text('ู‡ู„ ุฃู†ุช ู…ุชุฃูƒุฏ ู…ู† ุฑุบุจุชูƒ ููŠ ุฅู†ู‡ุงุก ุฌู„ุณุฉ ุงู„ุนู…ู„ ุงู„ุญุงู„ูŠุฉุŸ'), + content: + const Text('ู‡ู„ ุฃู†ุช ู…ุชุฃูƒุฏ ู…ู† ุฑุบุจุชูƒ ููŠ ุฅู†ู‡ุงุก ุฌู„ุณุฉ ุงู„ุนู…ู„ ุงู„ุญุงู„ูŠุฉุŸ'), actions: [ CupertinoDialogAction( onPressed: () => Navigator.of(ctx).pop(), @@ -224,7 +261,9 @@ class _TeacherMainShellState extends State { 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( textDirection: TextDirection.rtl, @@ -242,7 +281,10 @@ class _TeacherMainShellState extends State { height: 38, decoration: BoxDecoration( gradient: const LinearGradient( - colors: [TeacherTheme.emeraldPrimary, TeacherTheme.emeraldDark], + colors: [ + TeacherTheme.emeraldPrimary, + TeacherTheme.emeraldDark + ], ), borderRadius: BorderRadius.circular(10), ), @@ -276,12 +318,14 @@ class _TeacherMainShellState extends State { ), ), 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( '${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, ), ], @@ -294,13 +338,15 @@ class _TeacherMainShellState extends State { // Edit Profile Button IconButton( 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), ), // Logout Button IconButton( 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), ), ], @@ -317,7 +363,8 @@ class _TeacherMainShellState extends State { bottomNavigationBar: Container( decoration: const BoxDecoration( color: TeacherTheme.surfaceDark, - border: Border(top: BorderSide(color: TeacherTheme.surfaceBorder)), + border: + Border(top: BorderSide(color: TeacherTheme.surfaceBorder)), ), child: BottomNavigationBar( currentIndex: _currentTabIndex, @@ -325,7 +372,8 @@ class _TeacherMainShellState extends State { backgroundColor: Colors.transparent, selectedItemColor: TeacherTheme.emeraldPrimary, 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), type: BottomNavigationBarType.fixed, elevation: 0, diff --git a/apps/teacher_app/macos/Runner/DebugProfile.entitlements b/apps/teacher_app/macos/Runner/DebugProfile.entitlements index 3ba6c12..9158f3e 100644 --- a/apps/teacher_app/macos/Runner/DebugProfile.entitlements +++ b/apps/teacher_app/macos/Runner/DebugProfile.entitlements @@ -10,5 +10,7 @@ com.apple.security.network.server + com.apple.security.files.user-selected.read-write + diff --git a/apps/teacher_app/macos/Runner/Release.entitlements b/apps/teacher_app/macos/Runner/Release.entitlements index 7a2230d..c1a22b8 100644 --- a/apps/teacher_app/macos/Runner/Release.entitlements +++ b/apps/teacher_app/macos/Runner/Release.entitlements @@ -8,5 +8,7 @@ com.apple.security.network.server + com.apple.security.files.user-selected.read-write + diff --git a/backend/app/Controllers/VideoController.php b/backend/app/Controllers/VideoController.php index 484099a..8772218 100644 --- a/backend/app/Controllers/VideoController.php +++ b/backend/app/Controllers/VideoController.php @@ -42,6 +42,7 @@ class VideoController $courseId = (int)($request->getBody()['course_id'] ?? $_POST['course_id'] ?? 0); $title = trim((string)($request->getBody()['title'] ?? $_POST['title'] ?? '')); $seqOrder = (int)($request->getBody()['sequence_order'] ?? $_POST['sequence_order'] ?? 1); + $curriculumKey = trim((string)($request->getBody()['curriculum_key'] ?? $_POST['curriculum_key'] ?? '')); if (empty($title)) { $response->status(400)->json([ @@ -64,13 +65,19 @@ class VideoController return; } } 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) { $courseId = (int)$existingCourse['id']; $course = $existingCourse; } else { - $subjectName = trim((string)($request->getBody()['subject'] ?? $_POST['subject'] ?? '')); - $gradeLevel = trim((string)($request->getBody()['grade_level'] ?? $_POST['grade_level'] ?? '')); $subject = Database::selectOne( "SELECT id FROM subjects WHERE name_ar = ? OR name_en = ? ORDER BY id LIMIT 1", [$subjectName, $subjectName] @@ -121,11 +128,12 @@ class VideoController // Insert lesson record with HLS references $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) - VALUES (?, ?, ?, 'api_upload', ?, '', ?, ?, ?, ?, 0, 'ready')", + "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')", [ $courseId, $title, + $curriculumKey !== '' ? $curriculumKey : null, $seqOrder, $uploadResult['video_uuid'], $uploadResult['local_path'], diff --git a/backend/app/Services/AiVideoAnalyzerService.php b/backend/app/Services/AiVideoAnalyzerService.php index b94c010..40ee466 100644 --- a/backend/app/Services/AiVideoAnalyzerService.php +++ b/backend/app/Services/AiVideoAnalyzerService.php @@ -78,7 +78,7 @@ class AiVideoAnalyzerService $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 ุบูŠุฑ ุตุงู„ุญุฉ'); $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; } catch (\Throwable $e) { return ['decision' => 'needs_manual_review', 'reason' => 'ุชุนุฐุฑ ุฅูƒู…ุงู„ ุชุญู„ูŠู„ Geminiุ› ู„ู… ูŠุชู… ุฑูุน ุงู„ููŠุฏูŠูˆ']; diff --git a/backend/app/Services/TeacherRatingService.php b/backend/app/Services/TeacherRatingService.php index 43d4d63..5805682 100644 --- a/backend/app/Services/TeacherRatingService.php +++ b/backend/app/Services/TeacherRatingService.php @@ -52,19 +52,19 @@ class TeacherRatingService Database::query(" CREATE TABLE teacher_performance_metrics ( teacher_id BIGINT UNSIGNED PRIMARY KEY, - avg_response_minutes INT DEFAULT 4, - response_rate_percentage DECIMAL(5, 2) DEFAULT 98.50, + avg_response_minutes INT DEFAULT 0, + response_rate_percentage DECIMAL(5, 2) DEFAULT 0, active_queue_count INT UNSIGNED DEFAULT 0, total_students_enrolled INT DEFAULT 0, total_reviews_count INT DEFAULT 0, - raw_avg_rating DECIMAL(3, 2) DEFAULT 5.00, - weighted_student_rating DECIMAL(3, 2) DEFAULT 5.00, - ai_engagement_score DECIMAL(5, 2) DEFAULT 96.00, - sla_speed_score DECIMAL(5, 2) DEFAULT 98.00, - mastery_impact_score DECIMAL(5, 2) DEFAULT 94.00, - composite_merit_score DECIMAL(5, 2) DEFAULT 96.50, - star_equivalent DECIMAL(3, 2) DEFAULT 4.90, - reputation_tier VARCHAR(100) DEFAULT 'ู…ุนู„ู… ู†ุฎุจูˆูŠ ู…ุนุชู…ุฏ ๐Ÿ’Ž', + raw_avg_rating DECIMAL(3, 2) DEFAULT 0, + weighted_student_rating DECIMAL(3, 2) DEFAULT 0, + ai_engagement_score DECIMAL(5, 2) DEFAULT 0, + sla_speed_score DECIMAL(5, 2) DEFAULT 0, + mastery_impact_score DECIMAL(5, 2) DEFAULT 0, + composite_merit_score DECIMAL(5, 2) DEFAULT 0, + star_equivalent DECIMAL(3, 2) DEFAULT 0, + reputation_tier VARCHAR(100) DEFAULT 'ุจุงู†ุชุธุงุฑ ุจูŠุงู†ุงุช ุงู„ุฃุฏุงุก', 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; @@ -308,10 +308,6 @@ class TeacherRatingService ] ); - if ($studentsCount <= 0) { - $studentsCount = 83; - } - return [ 'teacher_id' => $teacherId, 'composite_merit_score' => $compositeScore, @@ -327,12 +323,12 @@ class TeacherRatingService 'sla_speed_score' => $slaComponent, 'mastery_impact_score' => $masteryComponent, 'ai_engagement_score' => $aiEngagementScore, - 'success_rate_percentage' => 98.2, - 'curriculum_alignment_pct' => 96.0, - 'socratic_interaction_pct' => 89.0, - 'audio_clarity_pct' => 94.0, - 'cognitive_focus_pct' => 92.0, - 'ai_recommendation' => 'ุชูˆุฌูŠู‡ ุงู„ุฐูƒุงุก ุงู„ุงุตุทู†ุงุนูŠ ุงู„ุฃุณุจูˆุนูŠ: ู†ุณุจุฉ ุงู„ุงู„ุชุฒุงู… ุงู„ูˆุฒุงุฑูŠ ู…ู…ุชุงุฒุฉ (96%). ูŠููˆุตู‰ ุจุฅุถุงูุฉ ูˆู‚ูุฉ ุณู‚ุฑุงุทูŠุฉ ุงุณุชู†ุชุงุฌูŠุฉ ููŠ ุงู„ุฏู‚ูŠู‚ุฉ 14 ู…ู† ุงู„ุฏุฑุณ ุงู„ู‚ุงุฏู… ู„ุชุนุฒูŠุฒ ุชูุงุนู„ ุงู„ุทู„ุจุฉ.' + 'success_rate_percentage' => 0.0, + 'curriculum_alignment_pct' => 0.0, + 'socratic_interaction_pct' => 0.0, + 'audio_clarity_pct' => 0.0, + 'cognitive_focus_pct' => 0.0, + 'ai_recommendation' => 'ุณุชุธู‡ุฑ ุงู„ุชูˆุตูŠุฉ ุจุนุฏ ุชูˆูุฑ ุญุตุต ู…ุญู„ู„ุฉ ูˆุชูุงุนู„ ูุนู„ูŠ ู…ู† ุงู„ุทู„ุจุฉ.' ]; } @@ -419,7 +415,7 @@ class TeacherRatingService } } catch (\Throwable $e) {} - return 94.50; + return 0.0; } /** @@ -431,11 +427,11 @@ class TeacherRatingService $teachers = Database::select( "SELECT t.id, t.full_name, t.specialization, t.bio, - COALESCE(m.avg_response_minutes, 3) as avg_response_minutes, - COALESCE(m.response_rate_percentage, 99.0) as response_rate_percentage, - COALESCE(m.composite_merit_score, 96.50) as composite_merit_score, - COALESCE(m.star_equivalent, 4.90) as star_equivalent, - COALESCE(m.reputation_tier, 'ู…ุนู„ู… ู†ุฎุจูˆูŠ ู…ุนุชู…ุฏ ๐Ÿ’Ž') as reputation_tier, + COALESCE(m.avg_response_minutes, 0) as avg_response_minutes, + COALESCE(m.response_rate_percentage, 0) as response_rate_percentage, + COALESCE(m.composite_merit_score, 0) as composite_merit_score, + COALESCE(m.star_equivalent, 0) as star_equivalent, + 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 FROM teachers t LEFT JOIN teacher_performance_metrics m ON t.id = m.teacher_id