Update Saqel Platform: 2026-09-08 18:55:21
This commit is contained in:
@@ -21,21 +21,25 @@ class AppLogger {
|
|||||||
if (!kDebugMode) return;
|
if (!kDebugMode) return;
|
||||||
|
|
||||||
final buffer = StringBuffer();
|
final buffer = StringBuffer();
|
||||||
buffer.writeln('\n🌐 ─── [HTTP REQUEST (ADMIN)] ───────────────────────────────────');
|
buffer.writeln(
|
||||||
|
'\n🌐 ─── [HTTP REQUEST (ADMIN)] ───────────────────────────────────');
|
||||||
buffer.writeln('➡️ Method: $method');
|
buffer.writeln('➡️ Method: $method');
|
||||||
buffer.writeln('🔗 URL: $uri');
|
buffer.writeln('🔗 URL: $uri');
|
||||||
if (headers != null && headers.isNotEmpty) {
|
if (headers != null && headers.isNotEmpty) {
|
||||||
final safeHeaders = Map<String, String>.from(headers);
|
final safeHeaders = Map<String, String>.from(headers);
|
||||||
if (safeHeaders.containsKey('Authorization')) {
|
if (safeHeaders.containsKey('Authorization')) {
|
||||||
final auth = safeHeaders['Authorization']!;
|
final auth = safeHeaders['Authorization']!;
|
||||||
safeHeaders['Authorization'] = auth.length > 20 ? '${auth.substring(0, 15)}...' : 'Bearer [REDACTED]';
|
safeHeaders['Authorization'] = auth.length > 20
|
||||||
|
? '${auth.substring(0, 15)}...'
|
||||||
|
: 'Bearer [REDACTED]';
|
||||||
}
|
}
|
||||||
buffer.writeln('📋 Headers: ${jsonEncode(safeHeaders)}');
|
buffer.writeln('📋 Headers: ${jsonEncode(safeHeaders)}');
|
||||||
}
|
}
|
||||||
if (body != null) {
|
if (body != null) {
|
||||||
buffer.writeln('📦 Body: ${body is String ? body : jsonEncode(body)}');
|
buffer.writeln('📦 Body: ${body is String ? body : jsonEncode(body)}');
|
||||||
}
|
}
|
||||||
buffer.writeln('───────────────────────────────────────────────────────────────');
|
buffer.writeln(
|
||||||
|
'───────────────────────────────────────────────────────────────');
|
||||||
debugPrint(buffer.toString());
|
debugPrint(buffer.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,24 +56,29 @@ class AppLogger {
|
|||||||
final emoji = isSuccess ? '✅' : '⚠️';
|
final emoji = isSuccess ? '✅' : '⚠️';
|
||||||
final buffer = StringBuffer();
|
final buffer = StringBuffer();
|
||||||
|
|
||||||
buffer.writeln('\n$emoji ─── [HTTP RESPONSE $statusCode (ADMIN)] ───────────────────────');
|
buffer.writeln(
|
||||||
|
'\n$emoji ─── [HTTP RESPONSE $statusCode (ADMIN)] ───────────────────────');
|
||||||
buffer.writeln('⬅️ Method: $method');
|
buffer.writeln('⬅️ Method: $method');
|
||||||
buffer.writeln('🔗 URL: $uri');
|
buffer.writeln('🔗 URL: $uri');
|
||||||
if (duration != null) {
|
if (duration != null) {
|
||||||
buffer.writeln('⏱️ Time: ${duration.inMilliseconds}ms');
|
buffer.writeln('⏱️ Time: ${duration.inMilliseconds}ms');
|
||||||
}
|
}
|
||||||
if (responseBody != null) {
|
if (responseBody != null) {
|
||||||
buffer.writeln('📦 Data: ${responseBody is String ? responseBody : jsonEncode(responseBody)}');
|
buffer.writeln(
|
||||||
|
'📦 Data: ${responseBody is String ? responseBody : jsonEncode(responseBody)}');
|
||||||
}
|
}
|
||||||
buffer.writeln('───────────────────────────────────────────────────────────────');
|
buffer.writeln(
|
||||||
|
'───────────────────────────────────────────────────────────────');
|
||||||
debugPrint(buffer.toString());
|
debugPrint(buffer.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
static void error(String message, {dynamic error, StackTrace? stackTrace, String tag = 'ADMIN_ERROR'}) {
|
static void error(String message,
|
||||||
|
{dynamic error, StackTrace? stackTrace, String tag = 'ADMIN_ERROR'}) {
|
||||||
if (!kDebugMode) return;
|
if (!kDebugMode) return;
|
||||||
|
|
||||||
final buffer = StringBuffer();
|
final buffer = StringBuffer();
|
||||||
buffer.writeln('\n🚨 ─── [ERROR: $tag] ──────────────────────────────────────────');
|
buffer.writeln(
|
||||||
|
'\n🚨 ─── [ERROR: $tag] ──────────────────────────────────────────');
|
||||||
buffer.writeln('❌ Message: $message');
|
buffer.writeln('❌ Message: $message');
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
buffer.writeln('⚠️ Error: $error');
|
buffer.writeln('⚠️ Error: $error');
|
||||||
@@ -77,7 +86,8 @@ class AppLogger {
|
|||||||
if (stackTrace != null) {
|
if (stackTrace != null) {
|
||||||
buffer.writeln('📍 Trace: \n$stackTrace');
|
buffer.writeln('📍 Trace: \n$stackTrace');
|
||||||
}
|
}
|
||||||
buffer.writeln('───────────────────────────────────────────────────────────────');
|
buffer.writeln(
|
||||||
|
'───────────────────────────────────────────────────────────────');
|
||||||
debugPrint(buffer.toString());
|
debugPrint(buffer.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
|||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
import '../core/services/storage_service.dart';
|
import '../core/services/storage_service.dart';
|
||||||
|
import '../core/utils/app_logger.dart';
|
||||||
import '../data/models/directorate_models.dart';
|
import '../data/models/directorate_models.dart';
|
||||||
|
|
||||||
/// Authenticated API client for principals, supervisors, and directorates.
|
/// Authenticated API client for principals, supervisors, and directorates.
|
||||||
@@ -29,15 +30,25 @@ class DirectorateApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Map<String, dynamic> _decode(http.Response response) {
|
static Map<String, dynamic> _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<String, dynamic> data = {};
|
Map<String, dynamic> data = {};
|
||||||
if (response.bodyBytes.isNotEmpty) {
|
if (response.bodyBytes.isNotEmpty) {
|
||||||
final decoded = json.decode(utf8.decode(response.bodyBytes));
|
final decoded = json.decode(utf8.decode(response.bodyBytes));
|
||||||
if (decoded is Map) data = Map<String, dynamic>.from(decoded);
|
if (decoded is Map) data = Map<String, dynamic>.from(decoded);
|
||||||
}
|
}
|
||||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||||
throw StateError(
|
final error = StateError(
|
||||||
data['message']?.toString() ?? 'فشل طلب الخادم (${response.statusCode}).',
|
data['message']?.toString() ??
|
||||||
|
'فشل طلب الخادم (${response.statusCode}).',
|
||||||
);
|
);
|
||||||
|
AppLogger.error('Admin API request failed',
|
||||||
|
error: error, tag: 'ADMIN_API');
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -46,11 +57,16 @@ class DirectorateApiService {
|
|||||||
required String phoneNumber,
|
required String phoneNumber,
|
||||||
required String role,
|
required String role,
|
||||||
}) async {
|
}) async {
|
||||||
final response = await http.post(
|
final response = await http
|
||||||
|
.post(
|
||||||
Uri.parse('$baseUrl/api/auth/otp/request'),
|
Uri.parse('$baseUrl/api/auth/otp/request'),
|
||||||
headers: const {'Accept': 'application/json', 'Content-Type': 'application/json'},
|
headers: const {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
body: json.encode({'phone_number': phoneNumber, 'role': role}),
|
body: json.encode({'phone_number': phoneNumber, 'role': role}),
|
||||||
).timeout(const Duration(seconds: 30));
|
)
|
||||||
|
.timeout(const Duration(seconds: 30));
|
||||||
_decode(response);
|
_decode(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,16 +75,21 @@ class DirectorateApiService {
|
|||||||
required String otp,
|
required String otp,
|
||||||
required String role,
|
required String role,
|
||||||
}) async {
|
}) async {
|
||||||
final response = await http.post(
|
final response = await http
|
||||||
|
.post(
|
||||||
Uri.parse('$baseUrl/api/auth/otp/verify'),
|
Uri.parse('$baseUrl/api/auth/otp/verify'),
|
||||||
headers: const {'Accept': 'application/json', 'Content-Type': 'application/json'},
|
headers: const {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
body: json.encode({
|
body: json.encode({
|
||||||
'phone_number': phoneNumber,
|
'phone_number': phoneNumber,
|
||||||
'otp': otp,
|
'otp': otp,
|
||||||
'role': role,
|
'role': role,
|
||||||
'device_fingerprint': 'saqel_admin_flutter',
|
'device_fingerprint': 'saqel_admin_flutter',
|
||||||
}),
|
}),
|
||||||
).timeout(const Duration(seconds: 30));
|
)
|
||||||
|
.timeout(const Duration(seconds: 30));
|
||||||
final data = _decode(response);
|
final data = _decode(response);
|
||||||
final payload = Map<String, dynamic>.from(data['data'] as Map? ?? const {});
|
final payload = Map<String, dynamic>.from(data['data'] as Map? ?? const {});
|
||||||
final token = payload['token']?.toString() ?? '';
|
final token = payload['token']?.toString() ?? '';
|
||||||
@@ -79,10 +100,12 @@ class DirectorateApiService {
|
|||||||
|
|
||||||
static Future<bool> hasValidSession() async {
|
static Future<bool> hasValidSession() async {
|
||||||
try {
|
try {
|
||||||
final response = await http.get(
|
final response = await http
|
||||||
|
.get(
|
||||||
Uri.parse('$baseUrl/api/auth/me'),
|
Uri.parse('$baseUrl/api/auth/me'),
|
||||||
headers: await _headers(),
|
headers: await _headers(),
|
||||||
).timeout(const Duration(seconds: 15));
|
)
|
||||||
|
.timeout(const Duration(seconds: 15));
|
||||||
_decode(response);
|
_decode(response);
|
||||||
return true;
|
return true;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -91,18 +114,22 @@ class DirectorateApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static Future<Map<String, dynamic>> fetchDirectorateDashboard() async {
|
static Future<Map<String, dynamic>> fetchDirectorateDashboard() async {
|
||||||
final response = await http.get(
|
final response = await http
|
||||||
|
.get(
|
||||||
Uri.parse('$baseUrl/api/directorate/dashboard'),
|
Uri.parse('$baseUrl/api/directorate/dashboard'),
|
||||||
headers: await _headers(),
|
headers: await _headers(),
|
||||||
).timeout(const Duration(seconds: 15));
|
)
|
||||||
|
.timeout(const Duration(seconds: 15));
|
||||||
return _decode(response);
|
return _decode(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Map<String, dynamic>> fetchSchoolDashboard({int schoolId = 1}) async {
|
static Future<Map<String, dynamic>> fetchSchoolDashboard(
|
||||||
|
{int schoolId = 1}) async {
|
||||||
final uri = Uri.parse('$baseUrl/api/supervisor/school-dashboard').replace(
|
final uri = Uri.parse('$baseUrl/api/supervisor/school-dashboard').replace(
|
||||||
queryParameters: {'school_id': '$schoolId'},
|
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));
|
.timeout(const Duration(seconds: 15));
|
||||||
return _decode(response);
|
return _decode(response);
|
||||||
}
|
}
|
||||||
@@ -118,7 +145,8 @@ class DirectorateApiService {
|
|||||||
List<int>? fileBytes,
|
List<int>? fileBytes,
|
||||||
required String fileName,
|
required String fileName,
|
||||||
}) async {
|
}) 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();
|
final headers = await _headers();
|
||||||
headers.remove('Content-Type');
|
headers.remove('Content-Type');
|
||||||
request.headers.addAll(headers);
|
request.headers.addAll(headers);
|
||||||
@@ -131,9 +159,11 @@ class DirectorateApiService {
|
|||||||
'duration_minutes': '$durationMinutes',
|
'duration_minutes': '$durationMinutes',
|
||||||
});
|
});
|
||||||
if (fileBytes != null) {
|
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) {
|
} 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 {
|
} else {
|
||||||
throw StateError('لم يتم اختيار ملف فيديو حقيقي.');
|
throw StateError('لم يتم اختيار ملف فيديو حقيقي.');
|
||||||
}
|
}
|
||||||
@@ -145,12 +175,15 @@ class DirectorateApiService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> pushExamToLab({required int examId, required int schoolId}) async {
|
static Future<bool> pushExamToLab(
|
||||||
final response = await http.post(
|
{required int examId, required int schoolId}) async {
|
||||||
|
final response = await http
|
||||||
|
.post(
|
||||||
Uri.parse('$baseUrl/api/supervisor/exam/push-to-lab'),
|
Uri.parse('$baseUrl/api/supervisor/exam/push-to-lab'),
|
||||||
headers: await _headers(),
|
headers: await _headers(),
|
||||||
body: json.encode({'exam_id': examId, 'school_id': schoolId}),
|
body: json.encode({'exam_id': examId, 'school_id': schoolId}),
|
||||||
).timeout(const Duration(seconds: 30));
|
)
|
||||||
|
.timeout(const Duration(seconds: 30));
|
||||||
return _decode(response)['status'] == 'success';
|
return _decode(response)['status'] == 'success';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,15 +193,18 @@ class DirectorateApiService {
|
|||||||
List<int>? fileBytes,
|
List<int>? fileBytes,
|
||||||
required String fileName,
|
required String fileName,
|
||||||
}) async {
|
}) 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();
|
final headers = await _headers();
|
||||||
headers.remove('Content-Type');
|
headers.remove('Content-Type');
|
||||||
request.headers.addAll(headers);
|
request.headers.addAll(headers);
|
||||||
request.fields['session_id'] = '$sessionId';
|
request.fields['session_id'] = '$sessionId';
|
||||||
if (fileBytes != null) {
|
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) {
|
} 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 {
|
} else {
|
||||||
throw StateError('لم يتم اختيار عينة فيديو حقيقية.');
|
throw StateError('لم يتم اختيار عينة فيديو حقيقية.');
|
||||||
}
|
}
|
||||||
@@ -185,7 +221,8 @@ class DirectorateApiService {
|
|||||||
final uri = Uri.parse('$baseUrl/api/unified-exams/dual-forms').replace(
|
final uri = Uri.parse('$baseUrl/api/unified-exams/dual-forms').replace(
|
||||||
queryParameters: {'subject': subject, 'grade_level': gradeLevel},
|
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));
|
.timeout(const Duration(seconds: 30));
|
||||||
final data = _decode(response);
|
final data = _decode(response);
|
||||||
return Map<String, dynamic>.from(data['data'] as Map? ?? const {});
|
return Map<String, dynamic>.from(data['data'] as Map? ?? const {});
|
||||||
@@ -194,21 +231,26 @@ class DirectorateApiService {
|
|||||||
static Future<Map<String, dynamic>> evaluateExamSessionIntegrity({
|
static Future<Map<String, dynamic>> evaluateExamSessionIntegrity({
|
||||||
List<Map<String, dynamic>> submissions = const [],
|
List<Map<String, dynamic>> submissions = const [],
|
||||||
}) async {
|
}) async {
|
||||||
final response = await http.post(
|
final response = await http
|
||||||
|
.post(
|
||||||
Uri.parse('$baseUrl/api/unified-exams/evaluate-integrity'),
|
Uri.parse('$baseUrl/api/unified-exams/evaluate-integrity'),
|
||||||
headers: await _headers(),
|
headers: await _headers(),
|
||||||
body: json.encode({'submissions': submissions}),
|
body: json.encode({'submissions': submissions}),
|
||||||
).timeout(const Duration(seconds: 30));
|
)
|
||||||
|
.timeout(const Duration(seconds: 30));
|
||||||
final data = _decode(response);
|
final data = _decode(response);
|
||||||
return Map<String, dynamic>.from(data['data'] as Map? ?? const {});
|
return Map<String, dynamic>.from(data['data'] as Map? ?? const {});
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Map<String, dynamic>> dispatchParentReports({int schoolId = 1}) async {
|
static Future<Map<String, dynamic>> dispatchParentReports(
|
||||||
final response = await http.post(
|
{int schoolId = 1}) async {
|
||||||
|
final response = await http
|
||||||
|
.post(
|
||||||
Uri.parse('$baseUrl/api/parent-reports/dispatch'),
|
Uri.parse('$baseUrl/api/parent-reports/dispatch'),
|
||||||
headers: await _headers(),
|
headers: await _headers(),
|
||||||
body: json.encode({'school_id': schoolId}),
|
body: json.encode({'school_id': schoolId}),
|
||||||
).timeout(const Duration(minutes: 2));
|
)
|
||||||
|
.timeout(const Duration(minutes: 2));
|
||||||
return _decode(response);
|
return _decode(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,11 +258,13 @@ class DirectorateApiService {
|
|||||||
int schoolId = 1,
|
int schoolId = 1,
|
||||||
required List<Map<String, dynamic>> records,
|
required List<Map<String, dynamic>> records,
|
||||||
}) async {
|
}) async {
|
||||||
final response = await http.post(
|
final response = await http
|
||||||
|
.post(
|
||||||
Uri.parse('$baseUrl/api/school-roster/import'),
|
Uri.parse('$baseUrl/api/school-roster/import'),
|
||||||
headers: await _headers(),
|
headers: await _headers(),
|
||||||
body: json.encode({'school_id': schoolId, 'records': records}),
|
body: json.encode({'school_id': schoolId, 'records': records}),
|
||||||
).timeout(const Duration(minutes: 2));
|
)
|
||||||
|
.timeout(const Duration(minutes: 2));
|
||||||
return _decode(response);
|
return _decode(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
|||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
import '../../core/config/app_config.dart';
|
import '../../core/config/app_config.dart';
|
||||||
|
import '../../core/utils/app_logger.dart';
|
||||||
import '../models/super_admin_models.dart';
|
import '../models/super_admin_models.dart';
|
||||||
|
|
||||||
class SuperAdminRepository {
|
class SuperAdminRepository {
|
||||||
@@ -12,7 +13,8 @@ class SuperAdminRepository {
|
|||||||
|
|
||||||
Future<Map<String, String>> _headers() async {
|
Future<Map<String, String>> _headers() async {
|
||||||
final token = await _storage.read(key: _tokenKey);
|
final token = await _storage.read(key: _tokenKey);
|
||||||
if (token == null || token.isEmpty) throw StateError('جلسة السوبر أدمن غير متوفرة.');
|
if (token == null || token.isEmpty)
|
||||||
|
throw StateError('جلسة السوبر أدمن غير متوفرة.');
|
||||||
return {
|
return {
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Authorization': 'Bearer $token',
|
'Authorization': 'Bearer $token',
|
||||||
@@ -21,34 +23,57 @@ class SuperAdminRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _decode(http.Response response) {
|
Map<String, dynamic> _decode(http.Response response) {
|
||||||
final decoded = response.bodyBytes.isEmpty ? <String, dynamic>{} : json.decode(utf8.decode(response.bodyBytes));
|
AppLogger.response(
|
||||||
final data = decoded is Map ? Map<String, dynamic>.from(decoded) : <String, dynamic>{};
|
method: response.request?.method ?? 'HTTP',
|
||||||
|
uri: response.request?.url ?? Uri.parse(AppConfig.apiBaseUrl),
|
||||||
|
statusCode: response.statusCode,
|
||||||
|
body: response.body,
|
||||||
|
);
|
||||||
|
final decoded = response.bodyBytes.isEmpty
|
||||||
|
? <String, dynamic>{}
|
||||||
|
: json.decode(utf8.decode(response.bodyBytes));
|
||||||
|
final data = decoded is Map
|
||||||
|
? Map<String, dynamic>.from(decoded)
|
||||||
|
: <String, dynamic>{};
|
||||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
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;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> requestOtp(String phone) async {
|
Future<void> requestOtp(String phone) async {
|
||||||
final response = await http.post(
|
final response = await http
|
||||||
|
.post(
|
||||||
Uri.parse('${AppConfig.apiBaseUrl}/api/auth/otp/request'),
|
Uri.parse('${AppConfig.apiBaseUrl}/api/auth/otp/request'),
|
||||||
headers: const {'Accept': 'application/json', 'Content-Type': 'application/json'},
|
headers: const {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
body: json.encode({'phone_number': phone, 'role': 'super_admin'}),
|
body: json.encode({'phone_number': phone, 'role': 'super_admin'}),
|
||||||
).timeout(const Duration(seconds: 30));
|
)
|
||||||
|
.timeout(const Duration(seconds: 30));
|
||||||
_decode(response);
|
_decode(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> verifyOtp(String phone, String otp) async {
|
Future<void> verifyOtp(String phone, String otp) async {
|
||||||
final response = await http.post(
|
final response = await http
|
||||||
|
.post(
|
||||||
Uri.parse('${AppConfig.apiBaseUrl}/api/auth/otp/verify'),
|
Uri.parse('${AppConfig.apiBaseUrl}/api/auth/otp/verify'),
|
||||||
headers: const {'Accept': 'application/json', 'Content-Type': 'application/json'},
|
headers: const {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
body: json.encode({
|
body: json.encode({
|
||||||
'phone_number': phone,
|
'phone_number': phone,
|
||||||
'otp': otp,
|
'otp': otp,
|
||||||
'role': 'super_admin',
|
'role': 'super_admin',
|
||||||
'device_fingerprint': 'saqel_super_admin_flutter',
|
'device_fingerprint': 'saqel_super_admin_flutter',
|
||||||
}),
|
}),
|
||||||
).timeout(const Duration(seconds: 30));
|
)
|
||||||
|
.timeout(const Duration(seconds: 30));
|
||||||
final body = _decode(response);
|
final body = _decode(response);
|
||||||
final payload = Map<String, dynamic>.from(body['data'] as Map? ?? const {});
|
final payload = Map<String, dynamic>.from(body['data'] as Map? ?? const {});
|
||||||
final token = payload['token']?.toString() ?? '';
|
final token = payload['token']?.toString() ?? '';
|
||||||
@@ -58,10 +83,12 @@ class SuperAdminRepository {
|
|||||||
|
|
||||||
Future<bool> hasValidSession() async {
|
Future<bool> hasValidSession() async {
|
||||||
try {
|
try {
|
||||||
final response = await http.get(
|
final response = await http
|
||||||
|
.get(
|
||||||
Uri.parse('${AppConfig.apiBaseUrl}/api/auth/me'),
|
Uri.parse('${AppConfig.apiBaseUrl}/api/auth/me'),
|
||||||
headers: await _headers(),
|
headers: await _headers(),
|
||||||
).timeout(const Duration(seconds: 15));
|
)
|
||||||
|
.timeout(const Duration(seconds: 15));
|
||||||
_decode(response);
|
_decode(response);
|
||||||
return true;
|
return true;
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
@@ -70,48 +97,82 @@ class SuperAdminRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> _get(String path) async {
|
Future<dynamic> _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));
|
.timeout(const Duration(seconds: 20));
|
||||||
return _decode(response)['data'];
|
return _decode(response)['data'];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<dynamic> _post(String path, Map<String, dynamic> body) async {
|
Future<dynamic> _post(String path, Map<String, dynamic> body) async {
|
||||||
final response = await http.post(Uri.parse('${AppConfig.apiBaseUrl}$path'), headers: {
|
final response = await http
|
||||||
...await _headers(), 'Content-Type': 'application/json',
|
.post(Uri.parse('${AppConfig.apiBaseUrl}$path'),
|
||||||
}, body: json.encode(body)).timeout(const Duration(seconds: 20));
|
headers: {
|
||||||
|
...await _headers(),
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: json.encode(body))
|
||||||
|
.timeout(const Duration(seconds: 20));
|
||||||
return _decode(response)['data'];
|
return _decode(response)['data'];
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Map<String, dynamic>>> getSchools() async =>
|
Future<List<Map<String, dynamic>>> getSchools() async =>
|
||||||
(await _get('/api/super-admin/schools') as List).map((e) => Map<String, dynamic>.from(e as Map)).toList();
|
(await _get('/api/super-admin/schools') as List)
|
||||||
|
.map((e) => Map<String, dynamic>.from(e as Map))
|
||||||
|
.toList();
|
||||||
|
|
||||||
Future<List<Map<String, dynamic>>> getStaff() async =>
|
Future<List<Map<String, dynamic>>> getStaff() async =>
|
||||||
(await _get('/api/super-admin/staff') as List).map((e) => Map<String, dynamic>.from(e as Map)).toList();
|
(await _get('/api/super-admin/staff') as List)
|
||||||
|
.map((e) => Map<String, dynamic>.from(e as Map))
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
Future<void> saveSchool(Map<String, dynamic> body) async {
|
||||||
|
await _post('/api/super-admin/schools/save', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> toggleSchool(int id) async {
|
||||||
|
await _post('/api/super-admin/schools/toggle', {'id': id});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> saveStaff(Map<String, dynamic> body) async {
|
||||||
|
await _post('/api/super-admin/staff/save', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> toggleStaff(int id) async {
|
||||||
|
await _post('/api/super-admin/staff/toggle', {'id': id});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> saveSchool(Map<String, dynamic> body) async { await _post('/api/super-admin/schools/save', body); }
|
|
||||||
Future<void> toggleSchool(int id) async { await _post('/api/super-admin/schools/toggle', {'id': id}); }
|
|
||||||
Future<void> saveStaff(Map<String, dynamic> body) async { await _post('/api/super-admin/staff/save', body); }
|
|
||||||
Future<void> toggleStaff(int id) async { await _post('/api/super-admin/staff/toggle', {'id': id}); }
|
|
||||||
Future<List<Map<String, dynamic>>> getDirectorates() async =>
|
Future<List<Map<String, dynamic>>> getDirectorates() async =>
|
||||||
(await _get('/api/super-admin/directorates') as List).map((e) => Map<String, dynamic>.from(e as Map)).toList();
|
(await _get('/api/super-admin/directorates') as List)
|
||||||
Future<void> saveDirectorate(Map<String, dynamic> body) async { await _post('/api/super-admin/directorates/save', body); }
|
.map((e) => Map<String, dynamic>.from(e as Map))
|
||||||
Future<void> toggleDirectorate(int id) async { await _post('/api/super-admin/directorates/toggle', {'id': id}); }
|
.toList();
|
||||||
|
Future<void> saveDirectorate(Map<String, dynamic> body) async {
|
||||||
|
await _post('/api/super-admin/directorates/save', body);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> toggleDirectorate(int id) async {
|
||||||
|
await _post('/api/super-admin/directorates/toggle', {'id': id});
|
||||||
|
}
|
||||||
|
|
||||||
Future<MacroTelemetryModel> getMacroTelemetry() async =>
|
Future<MacroTelemetryModel> getMacroTelemetry() async =>
|
||||||
MacroTelemetryModel.fromJson(Map<String, dynamic>.from(await _get('/api/super-admin/overview') as Map));
|
MacroTelemetryModel.fromJson(Map<String, dynamic>.from(
|
||||||
|
await _get('/api/super-admin/overview') as Map));
|
||||||
|
|
||||||
Future<List<AiClusterNodeModel>> getAiClusterNodes() async =>
|
Future<List<AiClusterNodeModel>> getAiClusterNodes() async =>
|
||||||
(await _get('/api/super-admin/ai-nodes') as List)
|
(await _get('/api/super-admin/ai-nodes') as List)
|
||||||
.map((item) => AiClusterNodeModel.fromJson(Map<String, dynamic>.from(item as Map)))
|
.map((item) => AiClusterNodeModel.fromJson(
|
||||||
|
Map<String, dynamic>.from(item as Map)))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
Future<List<PayoutQueueItemModel>> getPayoutQueue() async =>
|
Future<List<PayoutQueueItemModel>> getPayoutQueue() async =>
|
||||||
(await _get('/api/super-admin/payouts') as List)
|
(await _get('/api/super-admin/payouts') as List)
|
||||||
.map((item) => PayoutQueueItemModel.fromJson(Map<String, dynamic>.from(item as Map)))
|
.map((item) => PayoutQueueItemModel.fromJson(
|
||||||
|
Map<String, dynamic>.from(item as Map)))
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
Future<List<SecurityIntegrityAlertModel>> getSecurityAlerts() async =>
|
Future<List<SecurityIntegrityAlertModel>> getSecurityAlerts() async =>
|
||||||
(await _get('/api/super-admin/security-alerts') as List)
|
(await _get('/api/super-admin/security-alerts') as List)
|
||||||
.map((item) => SecurityIntegrityAlertModel.fromJson(Map<String, dynamic>.from(item as Map)))
|
.map((item) => SecurityIntegrityAlertModel.fromJson(
|
||||||
|
Map<String, dynamic>.from(item as Map)))
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -365,11 +365,13 @@ class TeacherRepository {
|
|||||||
String? filePath,
|
String? filePath,
|
||||||
Uint8List? fileBytes,
|
Uint8List? fileBytes,
|
||||||
}) async {
|
}) async {
|
||||||
|
final stopwatch = Stopwatch()..start();
|
||||||
|
final uploadUri = Uri.parse('$baseUrl${AppConfig.uploadVideoEndpoint}');
|
||||||
try {
|
try {
|
||||||
final headers = await _authHeaders();
|
final headers = await _authHeaders();
|
||||||
final request = http.MultipartRequest(
|
final request = http.MultipartRequest(
|
||||||
'POST',
|
'POST',
|
||||||
Uri.parse('$baseUrl${AppConfig.uploadVideoEndpoint}'),
|
uploadUri,
|
||||||
);
|
);
|
||||||
request.headers.addAll(headers..remove('Content-Type'));
|
request.headers.addAll(headers..remove('Content-Type'));
|
||||||
request.fields.addAll({
|
request.fields.addAll({
|
||||||
@@ -380,6 +382,19 @@ class TeacherRepository {
|
|||||||
'subject': subject,
|
'subject': subject,
|
||||||
'curriculum_key': curriculumKey,
|
'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';
|
final resolvedName = fileName ?? 'lesson.mp4';
|
||||||
if (filePath != null && filePath.isNotEmpty) {
|
if (filePath != null && filePath.isNotEmpty) {
|
||||||
@@ -401,6 +416,13 @@ class TeacherRepository {
|
|||||||
final streamed =
|
final streamed =
|
||||||
await _client.send(request).timeout(const Duration(minutes: 10));
|
await _client.send(request).timeout(const Duration(minutes: 10));
|
||||||
final response = await http.Response.fromStream(streamed);
|
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<String, dynamic>;
|
final decoded = json.decode(response.body) as Map<String, dynamic>;
|
||||||
if (response.statusCode == 200 || response.statusCode == 201)
|
if (response.statusCode == 200 || response.statusCode == 201)
|
||||||
@@ -410,6 +432,8 @@ class TeacherRepository {
|
|||||||
} on StateError {
|
} on StateError {
|
||||||
rethrow;
|
rethrow;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
AppLogger.error('Lesson upload request failed',
|
||||||
|
error: error, tag: 'TEACHER_REPO');
|
||||||
throw StateError('تعذر رفع الحصة: $error');
|
throw StateError('تعذر رفع الحصة: $error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,23 @@ class VideoController
|
|||||||
$subject = Database::selectOne(
|
$subject = Database::selectOne(
|
||||||
"SELECT id FROM subjects WHERE name = ? ORDER BY id LIMIT 1",
|
"SELECT id FROM subjects WHERE name = ? ORDER BY id LIMIT 1",
|
||||||
[$subjectName]
|
[$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) {
|
if (!$subject) {
|
||||||
$response->status(409)->json(['status' => 'error', 'message' => 'لا يوجد مبحث معرف لربط الفيديو به']);
|
$response->status(409)->json(['status' => 'error', 'message' => 'لا يوجد مبحث معرف لربط الفيديو به']);
|
||||||
return;
|
return;
|
||||||
|
|||||||
Reference in New Issue
Block a user