Update Saqel Platform: 2026-09-08 13:43:36

This commit is contained in:
Hamza-Ayed
2026-09-08 13:43:36 +03:00
parent 0f424062fe
commit bb37b31cab
76 changed files with 3617 additions and 3059 deletions
@@ -1,459 +1,226 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../models/directorate_models.dart';
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 = 'http://127.0.0.1:8000';
static const String baseUrl = String.fromEnvironment(
'SAQEL_API_BASE_URL',
defaultValue: 'https://saqel.intaleqapp.com',
);
/// Fetch Macro Directorate Dashboard (لوحة القائد العام لمدارس الثقافة العسكرية)
static Future<Map<String, dynamic>> fetchDirectorateDashboard() async {
try {
final response = await http
.get(Uri.parse('$baseUrl/api/directorate/dashboard'))
.timeout(const Duration(seconds: 4));
static final StorageService _storage = StorageService();
if (response.statusCode == 200) {
final data = json.decode(utf8.decode(response.bodyBytes));
return data;
}
} catch (_) {
// Fallback for offline / demo mode
static Future<Map<String, String>> _headers() async {
final token = await _storage.getToken();
if (token == null || token.isEmpty) {
throw StateError('جلسة الإدارة غير متوفرة. يرجى تسجيل الدخول.');
}
// High-fidelity fallback
return {
'status': 'success',
'directorate': {
'code': 'MC-JOR',
'name': 'مديرية التربية والتعليم والثقافة العسكرية',
'type': 'military_culture',
'commander_name': 'مدير التعليم والثقافة العسكرية',
'total_schools': 43,
'total_students': 19350,
'total_teachers': 812,
'annual_contract_value': 20000.00,
'recorded_lessons_month': 1420,
'compliance_rate': 94.5,
'ai_average_score': 91.8,
'active_unified_exams': 2,
},
'schools': [
{
'id': 1,
'code': 'SCH-MC-01',
'name': 'مدرسة الشهيد فيصل الثاني الثانوية العسكرية',
'governorate': 'العاصمة',
'weight_tier': 'tier_c_large',
'student_count': 980,
'teacher_count': 48,
'compliance': 98.0,
'status': 'excellent',
'annual_fee': 900.00,
},
{
'id': 2,
'code': 'SCH-MC-02',
'name': 'مدرسة الملك حسين الثانوية العسكرية',
'governorate': 'الزرقاء',
'weight_tier': 'tier_c_large',
'student_count': 890,
'teacher_count': 42,
'compliance': 95.5,
'status': 'excellent',
'annual_fee': 900.00,
},
{
'id': 3,
'code': 'SCH-MC-03',
'name': 'مدرسة صرح الشهيد العسكرية',
'governorate': 'إربد',
'weight_tier': 'tier_b_medium',
'student_count': 460,
'teacher_count': 24,
'compliance': 92.0,
'status': 'good',
'annual_fee': 450.00,
},
{
'id': 4,
'code': 'SCH-MC-04',
'name': 'مدرسة البادية الشمالية الثانوية العسكرية',
'governorate': 'المفرق',
'weight_tier': 'tier_a_small',
'student_count': 150,
'teacher_count': 12,
'compliance': 88.0,
'status': 'warning_under_quota',
'annual_fee': 250.00,
},
{
'id': 5,
'code': 'SCH-MC-05',
'name': 'مدرسة القويرة الثانوية العسكرية',
'governorate': 'العقبة',
'weight_tier': 'tier_a_small',
'student_count': 135,
'teacher_count': 11,
'compliance': 91.0,
'status': 'good',
'annual_fee': 250.00,
},
{
'id': 6,
'code': 'SCH-MC-06',
'name': 'مدرسة الكرك الثانوية العسكرية',
'governorate': 'الكرك',
'weight_tier': 'tier_b_medium',
'student_count': 420,
'teacher_count': 22,
'compliance': 94.0,
'status': 'good',
'annual_fee': 450.00,
},
],
'anomalies': [
{
'id': 'ANM-01',
'type': 'speed_impossible',
'school_name': 'مدرسة البادية الشمالية العسكرية',
'subject': 'الرياضيات العلمي',
'description': 'حل 18 طالباً لمسألة تفاضل في 14 ثانية (تحت التدقيق الإشرافي)',
'severity': 'medium',
'timestamp': 'منذ ساعتين',
}
],
'radar': {
'top_teachers': [
{'name': 'أحمد المجالي', 'school': 'الشهيد فيصل الثاني', 'subject': 'الفيزياء', 'score': 97.4, 'certified_badge': true},
{'name': 'خلدون بني هاني', 'school': 'صرح الشهيد إربد', 'subject': 'الرياضيات', 'score': 96.8, 'certified_badge': true},
{'name': 'طارق الحنيطي', 'school': 'الملك حسين الزرقاء', 'subject': 'الكيمياء', 'score': 95.9, 'certified_badge': true},
],
'needing_support': [
{'name': 'معلم لغة عربية', 'school': 'مدرسة القويرة', 'issue': 'ضعف الأسئلة السقراطية وتجاوز زمن الشرح 35 دقيقة', 'score': 74.2},
]
}
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
'X-Device-Fingerprint': 'saqel_admin_flutter',
};
}
/// Fetch School Principal Dashboard (لوحة مدير المدرسة والمشرف الميداني)
static Future<Map<String, dynamic>> fetchSchoolDashboard({int schoolId = 1}) async {
try {
final response = await http
.get(Uri.parse('$baseUrl/api/supervisor/school-dashboard?school_id=$schoolId'))
.timeout(const Duration(seconds: 4));
if (response.statusCode == 200) {
final data = json.decode(utf8.decode(response.bodyBytes));
return data;
}
} catch (_) {
// Fallback
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);
}
return {
'status': 'success',
'school': {
'id': schoolId,
'name': 'مدرسة الشهيد فيصل الثاني الثانوية العسكرية',
'governorate': 'العاصمة - عمان',
'student_count': 980,
'teacher_count': 48,
'weight_tier': 'tier_c_large',
'director_name': 'المقدم الركن / مدير المدرسة',
'biweekly_cycle': 'الدورة الرابعة (الفصل الأول)',
'cycle_progress': '42 من 48 حصة مسجلة (87.5%)',
},
'teachers': [
{
'id': 101,
'name': 'أحمد المجالي',
'subject': 'الفيزياء',
'grade': 'العاشر + الأول ثانوي',
'quota_status': 'completed',
'last_lesson': 'تطبيقات قوانين نيوتن الثالث والمصاعد',
'ai_score': 97.4,
'recorded_at': 'منذ 3 أيام',
},
{
'id': 102,
'name': 'عمر الحباشنة',
'subject': 'الرياضيات العلمي',
'grade': 'التوجيهي 2008',
'quota_status': 'pending',
'last_lesson': 'تطبيقات القيم القصوى والمعدلات المرتبطة',
'ai_score': 94.0,
'recorded_at': 'منذ 12 يوماً',
},
{
'id': 103,
'name': 'سليمان الطراونة',
'subject': 'الكيمياء',
'grade': 'الأول ثانوي',
'quota_status': 'completed',
'last_lesson': 'سرعة التفاعلات ونظرية التصادم',
'ai_score': 92.5,
'recorded_at': 'أمس',
},
{
'id': 104,
'name': 'محمد الغويري',
'subject': 'اللغة الإنجليزية',
'grade': 'العاشر',
'quota_status': 'completed',
'last_lesson': 'Conditional Sentences (Type 2 & 3)',
'ai_score': 96.0,
'recorded_at': 'منذ 5 أيام',
},
],
'active_exams': [
{
'id': 'EX-MC-2026-01',
'title': 'الامتحان الموحد التجريبي - الرياضيات العلمي',
'subject': 'الرياضيات العلمي',
'grade_level': 'التوجيهي 2008',
'scheduled_time': 'اليوم - 09:00 صباحاً (موعد موحد لكافة المدارس)',
'duration_minutes': 60,
'pushed_to_lab': true,
'status': 'in_progress',
'forms': ['نموذج أ', 'نموذج ب'],
'anti_cheating': {
'kiosk_mode_locked': true,
'dynamic_numbers': true,
'smart_sampling': 'ثانيتان كل دقيقة (حجم الإجمالي 16.5 ميجابايت)',
}
}
]
};
if (response.statusCode < 200 || response.statusCode >= 300) {
throw StateError(
data['message']?.toString() ?? 'فشل طلب الخادم (${response.statusCode}).',
);
}
return data;
}
/// Submit a Video Lesson for AI Pedagogical Audit (تسجيل حصة مرئية)
static Future<RecordedLessonResult> recordLesson({
required String teacherName,
required String subject,
required String lessonTitle,
required double fileSizeMb,
required int durationMinutes,
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
.post(
Uri.parse('$baseUrl/api/supervisor/record-lesson'),
headers: {'Content-Type': 'application/json'},
body: json.encode({
'teacher_name': teacherName,
'subject': subject,
'lesson_title': lessonTitle,
'file_size_mb': fileSizeMb,
'duration_minutes': durationMinutes,
}),
)
.timeout(const Duration(seconds: 4));
if (response.statusCode == 200) {
final data = json.decode(utf8.decode(response.bodyBytes));
return RecordedLessonResult.fromJson(data['data']);
}
final response = await http.get(
Uri.parse('$baseUrl/api/auth/me'),
headers: await _headers(),
).timeout(const Duration(seconds: 15));
_decode(response);
return true;
} catch (_) {
// Fallback
return false;
}
}
// Fallback simulated result
return RecordedLessonResult(
lessonUuid: 'LREC-${DateTime.now().millisecondsSinceEpoch}',
teacherName: teacherName,
subject: subject,
lessonTitle: lessonTitle,
fileSizeMb: fileSizeMb,
durationMinutes: durationMinutes,
aiAlignmentScore: 95.5,
teacherTalkRatio: '62%',
status: 'approved_official',
reportSummary:
'التزام تام بالمنهاج الوزاري، تفاعل ممتاز من الطلاب، وطرح سليم للأسئلة السقراطية.',
socraticCheckpoints: [
{
'timestamp': '14:20',
'question': 'ما الفرق بين القصور الذاتي والكتلة التثاقلية؟',
'objective': 'الربط بمنهاج الفيزياء - نتاجات الوحدة الثانية'
},
{
'timestamp': '29:10',
'question': 'إذا انقطع حبل المصعد، ما القوة العمودية المؤثرة على الشخص؟',
'objective': 'تفكير استنتاجي سقراطي'
}
],
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 {}),
);
}
/// Push Exam to School Computer Labs (إرسال الامتحان لمختبرات الحاسوب)
static Future<bool> pushExamToLab(String examId) async {
try {
final response = await http
.post(
Uri.parse('$baseUrl/api/supervisor/exam/push-to-lab'),
headers: {'Content-Type': 'application/json'},
body: json.encode({'exam_id': examId}),
)
.timeout(const Duration(seconds: 3));
return response.statusCode == 200;
} catch (_) {
return true; // Simulate success in offline mode
}
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';
}
/// Upload Smart Sampled Panoramic Surveillance (رفع العينة البانورامية المقتضبة)
static Future<bool> uploadPanoramicSample({
required double fileSizeMb,
required int durationSeconds,
required int sessionId,
String? filePath,
List<int>? fileBytes,
required String fileName,
}) async {
try {
final response = await http
.post(
Uri.parse('$baseUrl/api/supervisor/exam/upload-panoramic'),
headers: {'Content-Type': 'application/json'},
body: json.encode({
'file_size_mb': fileSizeMb,
'duration_seconds': durationSeconds,
}),
)
.timeout(const Duration(seconds: 3));
return response.statusCode == 200;
} catch (_) {
return true;
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';
}
/// Fetch Dual-Form Standardized Exam (نموذج أ ونموذج ب)
static Future<Map<String, dynamic>> fetchDualForms({
String subject = 'الفيزياء',
String gradeLevel = 'الأول ثانوي',
String gradeLevel = 'الصف العاشر الأساسي',
}) async {
try {
final uri = Uri.parse('$baseUrl/api/unified-exams/dual-forms').replace(
queryParameters: {'subject': subject, 'grade_level': gradeLevel},
);
final response = await http.get(uri).timeout(const Duration(seconds: 3));
if (response.statusCode == 200) {
final decoded = json.decode(utf8.decode(response.bodyBytes));
return decoded['data'] ?? {};
}
} catch (_) {}
return {
'exam_uuid': 'sim-exam-dual-01',
'subject': subject,
'grade_level': gradeLevel,
'forms': {
'form_a': {
'form_code': 'FORM_A_ALPHA',
'barcode': 'SAQEL-EXAM-A-SIM01',
'total_score': 100,
'objective_score': 70,
'written_steps_score': 30,
},
'form_b': {
'form_code': 'FORM_B_BETA',
'barcode': 'SAQEL-EXAM-B-SIM01',
'total_score': 100,
'objective_score': 70,
'written_steps_score': 30,
},
}
};
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 {});
}
/// Evaluate Exam Session Integrity & Detect Statistical Anomalies
static Future<Map<String, dynamic>> evaluateExamSessionIntegrity() async {
try {
final response = await http
.post(
Uri.parse('$baseUrl/api/unified-exams/evaluate-integrity'),
headers: {'Content-Type': 'application/json'},
body: json.encode({}),
)
.timeout(const Duration(seconds: 3));
if (response.statusCode == 200) {
final decoded = json.decode(utf8.decode(response.bodyBytes));
return decoded['data'] ?? {};
}
} catch (_) {}
return {
'status': 'success',
'anomalies_detected': 3,
'integrity_score': 75,
'anomalies': [
{
'type': 'impossible_speed',
'severity': 'critical',
'title': 'مؤشر السرعة المستحيلة (Impossible Speed)',
'student_name': 'سيف الدين خالد الرواشدة',
'seat_number': 'قاعة 1 — مقعد 04',
'details': 'أنهى الطالب الامتحان في 195 ثانية فقط بمعدل 12 ثانية لكل مسألة تفاضل وحصل على 95%.',
'recommended_action': 'استعراض التسجيل البانورامي للقاعة في الدقيقة 02:40 والتحقق من جهاز الطالب.'
},
{
'type': 'error_clustering',
'severity': 'critical',
'title': 'تكتل الأخطاء المتطابقة (Identical Error Clustering)',
'student_name': 'عمر أحمد الحباشنة و فيصل محمود الخريشا',
'seat_number': 'مقعد 05 و مقعد 06',
'details': 'تطابق غريب في اختيار نفس الخيار الخاطئ النادر في 3 مسائل حسابية معقدة بين مقاعد متجاورة.',
'recommended_action': 'الرجوع فوراً للقطات الكاميرا البانورامية للمقاعد المذكورة.'
}
]
};
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 {});
}
/// Dispatch Monthly Parent Reports via WhatsApp / Nabeh Gateway
static Future<Map<String, dynamic>> dispatchParentReports({int schoolId = 1}) async {
try {
final response = await http
.post(
Uri.parse('$baseUrl/api/parent-reports/dispatch'),
headers: {'Content-Type': 'application/json'},
body: json.encode({'school_id': schoolId}),
)
.timeout(const Duration(seconds: 4));
if (response.statusCode == 200) {
return json.decode(utf8.decode(response.bodyBytes));
}
} catch (_) {}
return {
'status': 'success',
'message': 'تمت جدولة وبث 450 تقرير شهري لأولياء أمور طلبة المدرسة بنجاح عبر بوابة نبيه 📲',
'total_dispatched': 450,
'delivery_rate': '100%',
};
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);
}
/// Import School Roster with 10-digit National ID and AES-256-GCM Encryption
static Future<Map<String, dynamic>> importSchoolRoster({int schoolId = 1}) async {
try {
final response = await http
.post(
Uri.parse('$baseUrl/api/school-roster/import'),
headers: {'Content-Type': 'application/json'},
body: json.encode({'school_id': schoolId}),
)
.timeout(const Duration(seconds: 4));
if (response.statusCode == 200) {
return json.decode(utf8.decode(response.bodyBytes));
}
} catch (_) {}
return {
'status': 'success',
'total_received': 5,
'imported_count': 5,
'failed_count': 0,
'encryption_info': 'تم تشفير جميع الأرقام الوطنية بنجاح عبر خوارزمية AES-256-GCM السيادية ومؤشر HMAC الأعمى.',
};
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);
}
}