From 590326baa022faeb98e7730efc2575868fc96437 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Tue, 8 Sep 2026 18:55:21 +0300 Subject: [PATCH] Update Saqel Platform: 2026-09-08 18:55:21 --- apps/admin_app/lib/core/utils/app_logger.dart | 28 +++- .../lib/services/directorate_api_service.dart | 156 +++++++++++------- .../lib/core/utils/app_logger.dart | 21 +++ .../repositories/super_admin_repository.dart | 141 +++++++++++----- .../data/repositories/teacher_repository.dart | 26 ++- backend/app/Controllers/VideoController.php | 18 +- 6 files changed, 283 insertions(+), 107 deletions(-) create mode 100644 apps/super_admin_app/lib/core/utils/app_logger.dart diff --git a/apps/admin_app/lib/core/utils/app_logger.dart b/apps/admin_app/lib/core/utils/app_logger.dart index 88ad3d1..90d5926 100644 --- a/apps/admin_app/lib/core/utils/app_logger.dart +++ b/apps/admin_app/lib/core/utils/app_logger.dart @@ -21,21 +21,25 @@ class AppLogger { if (!kDebugMode) return; final buffer = StringBuffer(); - buffer.writeln('\n🌐 ─── [HTTP REQUEST (ADMIN)] ───────────────────────────────────'); + buffer.writeln( + '\n🌐 ─── [HTTP REQUEST (ADMIN)] ───────────────────────────────────'); 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 (ADMIN)] ───────────────────────'); + buffer.writeln( + '\n$emoji ─── [HTTP RESPONSE $statusCode (ADMIN)] ───────────────────────'); 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 = 'ADMIN_ERROR'}) { + static void error(String message, + {dynamic error, StackTrace? stackTrace, String tag = 'ADMIN_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/admin_app/lib/services/directorate_api_service.dart b/apps/admin_app/lib/services/directorate_api_service.dart index 195ce15..13db793 100644 --- a/apps/admin_app/lib/services/directorate_api_service.dart +++ b/apps/admin_app/lib/services/directorate_api_service.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import '../core/services/storage_service.dart'; +import '../core/utils/app_logger.dart'; import '../data/models/directorate_models.dart'; /// Authenticated API client for principals, supervisors, and directorates. @@ -29,15 +30,25 @@ class DirectorateApiService { } static Map _decode(http.Response response) { + AppLogger.response( + method: response.request?.method ?? 'HTTP', + uri: response.request?.url ?? Uri.parse(baseUrl), + statusCode: response.statusCode, + responseBody: response.body, + ); Map data = {}; if (response.bodyBytes.isNotEmpty) { final decoded = json.decode(utf8.decode(response.bodyBytes)); if (decoded is Map) data = Map.from(decoded); } if (response.statusCode < 200 || response.statusCode >= 300) { - throw StateError( - data['message']?.toString() ?? 'فؓل طلب الخادم (${response.statusCode}).', + final error = StateError( + data['message']?.toString() ?? + 'فؓل طلب الخادم (${response.statusCode}).', ); + AppLogger.error('Admin API request failed', + error: error, tag: 'ADMIN_API'); + throw error; } return data; } @@ -46,11 +57,16 @@ class DirectorateApiService { required String phoneNumber, required String role, }) async { - final response = await http.post( - Uri.parse('$baseUrl/api/auth/otp/request'), - headers: const {'Accept': 'application/json', 'Content-Type': 'application/json'}, - body: json.encode({'phone_number': phoneNumber, 'role': role}), - ).timeout(const Duration(seconds: 30)); + final response = await http + .post( + Uri.parse('$baseUrl/api/auth/otp/request'), + headers: const { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: json.encode({'phone_number': phoneNumber, 'role': role}), + ) + .timeout(const Duration(seconds: 30)); _decode(response); } @@ -59,16 +75,21 @@ class DirectorateApiService { required String otp, required String role, }) async { - final response = await http.post( - Uri.parse('$baseUrl/api/auth/otp/verify'), - headers: const {'Accept': 'application/json', 'Content-Type': 'application/json'}, - body: json.encode({ - 'phone_number': phoneNumber, - 'otp': otp, - 'role': role, - 'device_fingerprint': 'saqel_admin_flutter', - }), - ).timeout(const Duration(seconds: 30)); + final response = await http + .post( + Uri.parse('$baseUrl/api/auth/otp/verify'), + headers: const { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: json.encode({ + 'phone_number': phoneNumber, + 'otp': otp, + 'role': role, + 'device_fingerprint': 'saqel_admin_flutter', + }), + ) + .timeout(const Duration(seconds: 30)); final data = _decode(response); final payload = Map.from(data['data'] as Map? ?? const {}); final token = payload['token']?.toString() ?? ''; @@ -79,10 +100,12 @@ class DirectorateApiService { static Future hasValidSession() async { try { - final response = await http.get( - Uri.parse('$baseUrl/api/auth/me'), - headers: await _headers(), - ).timeout(const Duration(seconds: 15)); + final response = await http + .get( + Uri.parse('$baseUrl/api/auth/me'), + headers: await _headers(), + ) + .timeout(const Duration(seconds: 15)); _decode(response); return true; } catch (_) { @@ -91,18 +114,22 @@ class DirectorateApiService { } static Future> fetchDirectorateDashboard() async { - final response = await http.get( - Uri.parse('$baseUrl/api/directorate/dashboard'), - headers: await _headers(), - ).timeout(const Duration(seconds: 15)); + final response = await http + .get( + Uri.parse('$baseUrl/api/directorate/dashboard'), + headers: await _headers(), + ) + .timeout(const Duration(seconds: 15)); return _decode(response); } - static Future> fetchSchoolDashboard({int schoolId = 1}) async { + static Future> fetchSchoolDashboard( + {int schoolId = 1}) async { final uri = Uri.parse('$baseUrl/api/supervisor/school-dashboard').replace( queryParameters: {'school_id': '$schoolId'}, ); - final response = await http.get(uri, headers: await _headers()) + final response = await http + .get(uri, headers: await _headers()) .timeout(const Duration(seconds: 15)); return _decode(response); } @@ -118,7 +145,8 @@ class DirectorateApiService { List? fileBytes, required String fileName, }) async { - final request = http.MultipartRequest('POST', Uri.parse('$baseUrl/api/supervisor/record-lesson')); + final request = http.MultipartRequest( + 'POST', Uri.parse('$baseUrl/api/supervisor/record-lesson')); final headers = await _headers(); headers.remove('Content-Type'); request.headers.addAll(headers); @@ -131,9 +159,11 @@ class DirectorateApiService { 'duration_minutes': '$durationMinutes', }); if (fileBytes != null) { - request.files.add(http.MultipartFile.fromBytes('video', fileBytes, filename: fileName)); + request.files.add( + http.MultipartFile.fromBytes('video', fileBytes, filename: fileName)); } else if (filePath != null) { - request.files.add(await http.MultipartFile.fromPath('video', filePath, filename: fileName)); + request.files.add(await http.MultipartFile.fromPath('video', filePath, + filename: fileName)); } else { throw StateError('لم ŁŠŲŖŁ… اختيار ملف فيديو Ų­Ł‚ŁŠŁ‚ŁŠ.'); } @@ -145,12 +175,15 @@ class DirectorateApiService { ); } - static Future pushExamToLab({required int examId, required int schoolId}) async { - final response = await http.post( - Uri.parse('$baseUrl/api/supervisor/exam/push-to-lab'), - headers: await _headers(), - body: json.encode({'exam_id': examId, 'school_id': schoolId}), - ).timeout(const Duration(seconds: 30)); + static Future pushExamToLab( + {required int examId, required int schoolId}) async { + final response = await http + .post( + Uri.parse('$baseUrl/api/supervisor/exam/push-to-lab'), + headers: await _headers(), + body: json.encode({'exam_id': examId, 'school_id': schoolId}), + ) + .timeout(const Duration(seconds: 30)); return _decode(response)['status'] == 'success'; } @@ -160,15 +193,18 @@ class DirectorateApiService { List? fileBytes, required String fileName, }) async { - final request = http.MultipartRequest('POST', Uri.parse('$baseUrl/api/supervisor/exam/upload-panoramic')); + final request = http.MultipartRequest( + 'POST', Uri.parse('$baseUrl/api/supervisor/exam/upload-panoramic')); final headers = await _headers(); headers.remove('Content-Type'); request.headers.addAll(headers); request.fields['session_id'] = '$sessionId'; if (fileBytes != null) { - request.files.add(http.MultipartFile.fromBytes('video', fileBytes, filename: fileName)); + request.files.add( + http.MultipartFile.fromBytes('video', fileBytes, filename: fileName)); } else if (filePath != null) { - request.files.add(await http.MultipartFile.fromPath('video', filePath, filename: fileName)); + request.files.add(await http.MultipartFile.fromPath('video', filePath, + filename: fileName)); } else { throw StateError('لم ŁŠŲŖŁ… اختيار Ų¹ŁŠŁ†Ų© فيديو Ų­Ł‚ŁŠŁ‚ŁŠŲ©.'); } @@ -185,7 +221,8 @@ class DirectorateApiService { final uri = Uri.parse('$baseUrl/api/unified-exams/dual-forms').replace( queryParameters: {'subject': subject, 'grade_level': gradeLevel}, ); - final response = await http.get(uri, headers: await _headers()) + final response = await http + .get(uri, headers: await _headers()) .timeout(const Duration(seconds: 30)); final data = _decode(response); return Map.from(data['data'] as Map? ?? const {}); @@ -194,21 +231,26 @@ class DirectorateApiService { static Future> evaluateExamSessionIntegrity({ List> submissions = const [], }) async { - final response = await http.post( - Uri.parse('$baseUrl/api/unified-exams/evaluate-integrity'), - headers: await _headers(), - body: json.encode({'submissions': submissions}), - ).timeout(const Duration(seconds: 30)); + final response = await http + .post( + Uri.parse('$baseUrl/api/unified-exams/evaluate-integrity'), + headers: await _headers(), + body: json.encode({'submissions': submissions}), + ) + .timeout(const Duration(seconds: 30)); final data = _decode(response); return Map.from(data['data'] as Map? ?? const {}); } - static Future> dispatchParentReports({int schoolId = 1}) async { - final response = await http.post( - Uri.parse('$baseUrl/api/parent-reports/dispatch'), - headers: await _headers(), - body: json.encode({'school_id': schoolId}), - ).timeout(const Duration(minutes: 2)); + static Future> dispatchParentReports( + {int schoolId = 1}) async { + final response = await http + .post( + Uri.parse('$baseUrl/api/parent-reports/dispatch'), + headers: await _headers(), + body: json.encode({'school_id': schoolId}), + ) + .timeout(const Duration(minutes: 2)); return _decode(response); } @@ -216,11 +258,13 @@ class DirectorateApiService { int schoolId = 1, required List> records, }) async { - final response = await http.post( - Uri.parse('$baseUrl/api/school-roster/import'), - headers: await _headers(), - body: json.encode({'school_id': schoolId, 'records': records}), - ).timeout(const Duration(minutes: 2)); + final response = await http + .post( + Uri.parse('$baseUrl/api/school-roster/import'), + headers: await _headers(), + body: json.encode({'school_id': schoolId, 'records': records}), + ) + .timeout(const Duration(minutes: 2)); return _decode(response); } } diff --git a/apps/super_admin_app/lib/core/utils/app_logger.dart b/apps/super_admin_app/lib/core/utils/app_logger.dart new file mode 100644 index 0000000..b4cad25 --- /dev/null +++ b/apps/super_admin_app/lib/core/utils/app_logger.dart @@ -0,0 +1,21 @@ +import 'dart:convert'; +import 'package:flutter/foundation.dart'; + +/// Debug-only HTTP diagnostics. Tokens are never printed. +class AppLogger { + AppLogger._(); + + static void response( + {required String method, + required Uri uri, + required int statusCode, + dynamic body}) { + if (!kDebugMode) return; + debugPrint( + '[SUPER_ADMIN_HTTP] $method $uri -> $statusCode\\n${body is String ? body : jsonEncode(body)}'); + } + + static void error(String message, Object error) { + if (kDebugMode) debugPrint('[SUPER_ADMIN_ERROR] $message: $error'); + } +} diff --git a/apps/super_admin_app/lib/data/repositories/super_admin_repository.dart b/apps/super_admin_app/lib/data/repositories/super_admin_repository.dart index f480256..526974e 100644 --- a/apps/super_admin_app/lib/data/repositories/super_admin_repository.dart +++ b/apps/super_admin_app/lib/data/repositories/super_admin_repository.dart @@ -4,6 +4,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:http/http.dart' as http; import '../../core/config/app_config.dart'; +import '../../core/utils/app_logger.dart'; import '../models/super_admin_models.dart'; class SuperAdminRepository { @@ -12,7 +13,8 @@ class SuperAdminRepository { Future> _headers() async { final token = await _storage.read(key: _tokenKey); - if (token == null || token.isEmpty) throw StateError('جلسة Ų§Ł„Ų³ŁˆŲØŲ± أدمن غير Ł…ŲŖŁˆŁŲ±Ų©.'); + if (token == null || token.isEmpty) + throw StateError('جلسة Ų§Ł„Ų³ŁˆŲØŲ± أدمن غير Ł…ŲŖŁˆŁŲ±Ų©.'); return { 'Accept': 'application/json', 'Authorization': 'Bearer $token', @@ -21,34 +23,57 @@ class SuperAdminRepository { } Map _decode(http.Response response) { - final decoded = response.bodyBytes.isEmpty ? {} : json.decode(utf8.decode(response.bodyBytes)); - final data = decoded is Map ? Map.from(decoded) : {}; + AppLogger.response( + method: response.request?.method ?? 'HTTP', + uri: response.request?.url ?? Uri.parse(AppConfig.apiBaseUrl), + statusCode: response.statusCode, + body: response.body, + ); + final decoded = response.bodyBytes.isEmpty + ? {} + : json.decode(utf8.decode(response.bodyBytes)); + final data = decoded is Map + ? Map.from(decoded) + : {}; if (response.statusCode < 200 || response.statusCode >= 300) { - throw StateError(data['message']?.toString() ?? 'فؓل طلب الخادم (${response.statusCode}).'); + final error = StateError(data['message']?.toString() ?? + 'فؓل طلب الخادم (${response.statusCode}).'); + AppLogger.error('Super-admin API request failed', error); + throw error; } return data; } Future requestOtp(String phone) async { - final response = await http.post( - Uri.parse('${AppConfig.apiBaseUrl}/api/auth/otp/request'), - headers: const {'Accept': 'application/json', 'Content-Type': 'application/json'}, - body: json.encode({'phone_number': phone, 'role': 'super_admin'}), - ).timeout(const Duration(seconds: 30)); + final response = await http + .post( + Uri.parse('${AppConfig.apiBaseUrl}/api/auth/otp/request'), + headers: const { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: json.encode({'phone_number': phone, 'role': 'super_admin'}), + ) + .timeout(const Duration(seconds: 30)); _decode(response); } Future verifyOtp(String phone, String otp) async { - final response = await http.post( - Uri.parse('${AppConfig.apiBaseUrl}/api/auth/otp/verify'), - headers: const {'Accept': 'application/json', 'Content-Type': 'application/json'}, - body: json.encode({ - 'phone_number': phone, - 'otp': otp, - 'role': 'super_admin', - 'device_fingerprint': 'saqel_super_admin_flutter', - }), - ).timeout(const Duration(seconds: 30)); + final response = await http + .post( + Uri.parse('${AppConfig.apiBaseUrl}/api/auth/otp/verify'), + headers: const { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: json.encode({ + 'phone_number': phone, + 'otp': otp, + 'role': 'super_admin', + 'device_fingerprint': 'saqel_super_admin_flutter', + }), + ) + .timeout(const Duration(seconds: 30)); final body = _decode(response); final payload = Map.from(body['data'] as Map? ?? const {}); final token = payload['token']?.toString() ?? ''; @@ -58,10 +83,12 @@ class SuperAdminRepository { Future hasValidSession() async { try { - final response = await http.get( - Uri.parse('${AppConfig.apiBaseUrl}/api/auth/me'), - headers: await _headers(), - ).timeout(const Duration(seconds: 15)); + final response = await http + .get( + Uri.parse('${AppConfig.apiBaseUrl}/api/auth/me'), + headers: await _headers(), + ) + .timeout(const Duration(seconds: 15)); _decode(response); return true; } catch (_) { @@ -70,48 +97,82 @@ class SuperAdminRepository { } Future _get(String path) async { - final response = await http.get(Uri.parse('${AppConfig.apiBaseUrl}$path'), headers: await _headers()) + final response = await http + .get(Uri.parse('${AppConfig.apiBaseUrl}$path'), + headers: await _headers()) .timeout(const Duration(seconds: 20)); return _decode(response)['data']; } Future _post(String path, Map body) async { - final response = await http.post(Uri.parse('${AppConfig.apiBaseUrl}$path'), headers: { - ...await _headers(), 'Content-Type': 'application/json', - }, body: json.encode(body)).timeout(const Duration(seconds: 20)); + final response = await http + .post(Uri.parse('${AppConfig.apiBaseUrl}$path'), + headers: { + ...await _headers(), + 'Content-Type': 'application/json', + }, + body: json.encode(body)) + .timeout(const Duration(seconds: 20)); return _decode(response)['data']; } Future>> getSchools() async => - (await _get('/api/super-admin/schools') as List).map((e) => Map.from(e as Map)).toList(); + (await _get('/api/super-admin/schools') as List) + .map((e) => Map.from(e as Map)) + .toList(); Future>> getStaff() async => - (await _get('/api/super-admin/staff') as List).map((e) => Map.from(e as Map)).toList(); + (await _get('/api/super-admin/staff') as List) + .map((e) => Map.from(e as Map)) + .toList(); + + Future saveSchool(Map body) async { + await _post('/api/super-admin/schools/save', body); + } + + Future toggleSchool(int id) async { + await _post('/api/super-admin/schools/toggle', {'id': id}); + } + + Future saveStaff(Map body) async { + await _post('/api/super-admin/staff/save', body); + } + + Future toggleStaff(int id) async { + await _post('/api/super-admin/staff/toggle', {'id': id}); + } - Future saveSchool(Map body) async { await _post('/api/super-admin/schools/save', body); } - Future toggleSchool(int id) async { await _post('/api/super-admin/schools/toggle', {'id': id}); } - Future saveStaff(Map body) async { await _post('/api/super-admin/staff/save', body); } - Future toggleStaff(int id) async { await _post('/api/super-admin/staff/toggle', {'id': id}); } Future>> getDirectorates() async => - (await _get('/api/super-admin/directorates') as List).map((e) => Map.from(e as Map)).toList(); - Future saveDirectorate(Map body) async { await _post('/api/super-admin/directorates/save', body); } - Future toggleDirectorate(int id) async { await _post('/api/super-admin/directorates/toggle', {'id': id}); } + (await _get('/api/super-admin/directorates') as List) + .map((e) => Map.from(e as Map)) + .toList(); + Future saveDirectorate(Map body) async { + await _post('/api/super-admin/directorates/save', body); + } + + Future toggleDirectorate(int id) async { + await _post('/api/super-admin/directorates/toggle', {'id': id}); + } Future getMacroTelemetry() async => - MacroTelemetryModel.fromJson(Map.from(await _get('/api/super-admin/overview') as Map)); + MacroTelemetryModel.fromJson(Map.from( + await _get('/api/super-admin/overview') as Map)); Future> getAiClusterNodes() async => (await _get('/api/super-admin/ai-nodes') as List) - .map((item) => AiClusterNodeModel.fromJson(Map.from(item as Map))) + .map((item) => AiClusterNodeModel.fromJson( + Map.from(item as Map))) .toList(); Future> getPayoutQueue() async => (await _get('/api/super-admin/payouts') as List) - .map((item) => PayoutQueueItemModel.fromJson(Map.from(item as Map))) + .map((item) => PayoutQueueItemModel.fromJson( + Map.from(item as Map))) .toList(); Future> getSecurityAlerts() async => (await _get('/api/super-admin/security-alerts') as List) - .map((item) => SecurityIntegrityAlertModel.fromJson(Map.from(item as Map))) + .map((item) => SecurityIntegrityAlertModel.fromJson( + Map.from(item as Map))) .toList(); } diff --git a/apps/teacher_app/lib/data/repositories/teacher_repository.dart b/apps/teacher_app/lib/data/repositories/teacher_repository.dart index d8fac14..0d9f536 100644 --- a/apps/teacher_app/lib/data/repositories/teacher_repository.dart +++ b/apps/teacher_app/lib/data/repositories/teacher_repository.dart @@ -365,11 +365,13 @@ class TeacherRepository { String? filePath, Uint8List? fileBytes, }) async { + final stopwatch = Stopwatch()..start(); + final uploadUri = Uri.parse('$baseUrl${AppConfig.uploadVideoEndpoint}'); try { final headers = await _authHeaders(); final request = http.MultipartRequest( 'POST', - Uri.parse('$baseUrl${AppConfig.uploadVideoEndpoint}'), + uploadUri, ); request.headers.addAll(headers..remove('Content-Type')); request.fields.addAll({ @@ -380,6 +382,19 @@ class TeacherRepository { 'subject': subject, 'curriculum_key': curriculumKey, }); + AppLogger.request( + method: 'POST', + uri: uploadUri, + headers: request.headers, + body: { + 'title': title, + 'grade_level': gradeLevel, + 'subject': subject, + 'curriculum_key': curriculumKey, + 'file_name': fileName, + 'file_size_mb': fileSizeMb + }, + ); final resolvedName = fileName ?? 'lesson.mp4'; if (filePath != null && filePath.isNotEmpty) { @@ -401,6 +416,13 @@ class TeacherRepository { final streamed = await _client.send(request).timeout(const Duration(minutes: 10)); final response = await http.Response.fromStream(streamed); + AppLogger.response( + method: 'POST', + uri: uploadUri, + statusCode: response.statusCode, + responseBody: response.body, + duration: stopwatch.elapsed, + ); final decoded = json.decode(response.body) as Map; if (response.statusCode == 200 || response.statusCode == 201) @@ -410,6 +432,8 @@ class TeacherRepository { } on StateError { rethrow; } catch (error) { + AppLogger.error('Lesson upload request failed', + error: error, tag: 'TEACHER_REPO'); throw StateError('ŲŖŲ¹Ų°Ų± رفع الحصة: $error'); } } diff --git a/backend/app/Controllers/VideoController.php b/backend/app/Controllers/VideoController.php index 79e0c49..7b5efb9 100644 --- a/backend/app/Controllers/VideoController.php +++ b/backend/app/Controllers/VideoController.php @@ -92,7 +92,23 @@ class VideoController $subject = Database::selectOne( "SELECT id FROM subjects WHERE name = ? ORDER BY id LIMIT 1", [$subjectName] - ) ?: Database::selectOne("SELECT id FROM subjects ORDER BY id LIMIT 1"); + ); + if (!$subject && $subjectName !== '') { + // The curriculum tree is the source of truth. When its + // subject has not yet been seeded into SQL, create the + // minimal canonical subject record instead of attaching + // the teacher's lesson to an unrelated first subject. + $subjectCode = 'CURR-' . strtoupper(substr(hash('sha256', $subjectName), 0, 12)); + try { + $subjectId = (int)Database::insert( + "INSERT INTO subjects (name, code, stream, is_active) VALUES (?, ?, 'common', 1)", + [$subjectName, $subjectCode] + ); + $subject = ['id' => $subjectId]; + } catch (\Throwable $e) { + $subject = Database::selectOne("SELECT id FROM subjects WHERE name = ? LIMIT 1", [$subjectName]); + } + } if (!$subject) { $response->status(409)->json(['status' => 'error', 'message' => 'لا يوجد Ł…ŲØŲ­Ų« معرف لربط Ų§Ł„ŁŁŠŲÆŁŠŁˆ به']); return;