Update Saqel Platform: 2026-09-08 18:55:21
This commit is contained in:
@@ -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<String, String>.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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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 = {};
|
||||
if (response.bodyBytes.isNotEmpty) {
|
||||
final decoded = json.decode(utf8.decode(response.bodyBytes));
|
||||
if (decoded is Map) data = Map<String, dynamic>.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<String, dynamic>.from(data['data'] as Map? ?? const {});
|
||||
final token = payload['token']?.toString() ?? '';
|
||||
@@ -79,10 +100,12 @@ class DirectorateApiService {
|
||||
|
||||
static Future<bool> 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<Map<String, dynamic>> 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<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(
|
||||
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<int>? 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<bool> 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<bool> 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<int>? 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<String, dynamic>.from(data['data'] as Map? ?? const {});
|
||||
@@ -194,21 +231,26 @@ class DirectorateApiService {
|
||||
static Future<Map<String, dynamic>> evaluateExamSessionIntegrity({
|
||||
List<Map<String, dynamic>> 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<String, dynamic>.from(data['data'] as Map? ?? const {});
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> 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<Map<String, dynamic>> 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<Map<String, dynamic>> 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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user