227 lines
8.2 KiB
Dart
227 lines
8.2 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import '../core/services/storage_service.dart';
|
|
import '../data/models/directorate_models.dart';
|
|
|
|
/// Authenticated API client for principals, supervisors, and directorates.
|
|
/// Operational data always comes from the server; connection errors stay errors.
|
|
class DirectorateApiService {
|
|
static const String baseUrl = String.fromEnvironment(
|
|
'SAQEL_API_BASE_URL',
|
|
defaultValue: 'https://saqel.intaleqapp.com',
|
|
);
|
|
|
|
static final StorageService _storage = StorageService();
|
|
|
|
static Future<Map<String, String>> _headers() async {
|
|
final token = await _storage.getToken();
|
|
if (token == null || token.isEmpty) {
|
|
throw StateError('جلسة الإدارة غير متوفرة. يرجى تسجيل الدخول.');
|
|
}
|
|
return {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
'Authorization': 'Bearer $token',
|
|
'X-Device-Fingerprint': 'saqel_admin_flutter',
|
|
};
|
|
}
|
|
|
|
static Map<String, dynamic> _decode(http.Response response) {
|
|
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}).',
|
|
);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
static Future<void> requestOtp({
|
|
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));
|
|
_decode(response);
|
|
}
|
|
|
|
static Future<Map<String, dynamic>> verifyOtp({
|
|
required String phoneNumber,
|
|
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 data = _decode(response);
|
|
final payload = Map<String, dynamic>.from(data['data'] as Map? ?? const {});
|
|
final token = payload['token']?.toString() ?? '';
|
|
if (token.isEmpty) throw StateError('لم يُرجع الخادم جلسة إدارية صالحة.');
|
|
await _storage.saveToken(token);
|
|
return payload;
|
|
}
|
|
|
|
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));
|
|
_decode(response);
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
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));
|
|
return _decode(response);
|
|
}
|
|
|
|
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())
|
|
.timeout(const Duration(seconds: 15));
|
|
return _decode(response);
|
|
}
|
|
|
|
static Future<RecordedLessonResult> recordLesson({
|
|
required int schoolId,
|
|
required int teacherId,
|
|
required String subject,
|
|
required String gradeLevel,
|
|
required String lessonTitle,
|
|
required int durationMinutes,
|
|
String? filePath,
|
|
List<int>? fileBytes,
|
|
required String fileName,
|
|
}) async {
|
|
final request = http.MultipartRequest('POST', Uri.parse('$baseUrl/api/supervisor/record-lesson'));
|
|
final headers = await _headers();
|
|
headers.remove('Content-Type');
|
|
request.headers.addAll(headers);
|
|
request.fields.addAll({
|
|
'school_id': '$schoolId',
|
|
'teacher_id': '$teacherId',
|
|
'subject': subject,
|
|
'grade_level': gradeLevel,
|
|
'lesson_title': lessonTitle,
|
|
'duration_minutes': '$durationMinutes',
|
|
});
|
|
if (fileBytes != null) {
|
|
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));
|
|
} else {
|
|
throw StateError('لم يتم اختيار ملف فيديو حقيقي.');
|
|
}
|
|
final streamed = await request.send().timeout(const Duration(minutes: 10));
|
|
final response = await http.Response.fromStream(streamed);
|
|
final data = _decode(response);
|
|
return RecordedLessonResult.fromJson(
|
|
Map<String, dynamic>.from(data['data'] as Map? ?? const {}),
|
|
);
|
|
}
|
|
|
|
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';
|
|
}
|
|
|
|
static Future<bool> uploadPanoramicSample({
|
|
required int sessionId,
|
|
String? filePath,
|
|
List<int>? fileBytes,
|
|
required String fileName,
|
|
}) async {
|
|
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));
|
|
} else if (filePath != null) {
|
|
request.files.add(await http.MultipartFile.fromPath('video', filePath, filename: fileName));
|
|
} else {
|
|
throw StateError('لم يتم اختيار عينة فيديو حقيقية.');
|
|
}
|
|
final response = await http.Response.fromStream(
|
|
await request.send().timeout(const Duration(minutes: 10)),
|
|
);
|
|
return _decode(response)['status'] == 'success';
|
|
}
|
|
|
|
static Future<Map<String, dynamic>> fetchDualForms({
|
|
String subject = 'الفيزياء',
|
|
String gradeLevel = 'الصف العاشر الأساسي',
|
|
}) async {
|
|
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())
|
|
.timeout(const Duration(seconds: 30));
|
|
final data = _decode(response);
|
|
return Map<String, dynamic>.from(data['data'] as Map? ?? const {});
|
|
}
|
|
|
|
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 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));
|
|
return _decode(response);
|
|
}
|
|
|
|
static Future<Map<String, dynamic>> importSchoolRoster({
|
|
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));
|
|
return _decode(response);
|
|
}
|
|
}
|