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
@@ -2,451 +2,288 @@
namespace App\Controllers;
use App\Core\Database;
use App\Core\Request;
use App\Core\Response;
use App\Core\Database;
use App\Core\Security;
use App\Services\VideoService;
class DirectorateSupervisorController
{
/**
* Get Directorate Macro Dashboard (لوحة القائد لمديرية الثقافة العسكرية)
* GET /api/directorate/dashboard
*/
public function getDirectorateDashboard(Request $request, Response $response): void
{
$directorateCode = $request->getQueryParams()['directorate_code'] ?? 'MC-JOR';
// Fetch or default Military Culture Directorate
$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, // % of bi-weekly video quotas met
'ai_average_score' => 91.8,
'active_unified_exams' => 2,
];
// Sample list of 43 schools categorized by weight tiers across Jordan
$sampleSchools = [
[
'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,
],
];
// Macro Alerts & Anomaly indicators
$anomalies = [
[
'id' => 'ANM-01',
'type' => 'speed_impossible',
'school_name' => 'مدرسة البادية الشمالية العسكرية',
'subject' => 'الرياضيات العلمي',
'description' => 'حل 18 طالباً لمسألة تفاضل في 14 ثانية (تحت التدقيق الإشرافي)',
'severity' => 'medium',
'timestamp' => date('Y-m-d H:i', strtotime('-2 hours')),
]
];
$response->json([
'status' => 'success',
'directorate' => $directorate,
'schools' => $sampleSchools,
'anomalies' => $anomalies,
'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],
]
]
]);
}
/**
* Get School Principal / Field Supervisor Dashboard (لوحة مدير المدرسة والمشرف)
* GET /api/supervisor/school-dashboard
*/
public function getSchoolDashboard(Request $request, Response $response): void
{
$schoolId = (int)($request->getQueryParams()['school_id'] ?? 1);
$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', // 1 lesson every 2 weeks completed
'last_lesson' => 'تطبيقات قوانين نيوتن الثالث والمصاعد',
'ai_score' => 97.4,
'recorded_at' => 'منذ 3 أيام',
],
[
'id' => 102,
'name' => 'عمر الحباشنة',
'subject' => 'الرياضيات العلمي',
'grade' => 'التوجيهي 2008',
'quota_status' => 'pending', // Needs recording this week
'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 أيام',
],
];
$activeExams = [
[
'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 ميجابايت)',
]
]
];
$response->json([
'status' => 'success',
'school' => $school,
'teachers' => $teachers,
'active_exams' => $activeExams
]);
}
/**
* Record Classroom Lesson via Supervisor App
* POST /api/supervisor/record-lesson
*/
public function recordLesson(Request $request, Response $response): void
{
$body = $request->getBody();
$teacherName = $body['teacher_name'] ?? 'معلم معتمد';
$subject = $body['subject'] ?? 'الفيزياء';
$lessonTitle = $body['lesson_title'] ?? 'حصة صفية ميدانية';
$fileSizeMb = (float)($body['file_size_mb'] ?? 340.0);
$durationMin = (int)($body['duration_minutes'] ?? 45);
// Quality check constraint: Maximum 400 MB
if ($fileSizeMb > 400.0) {
$response->status(400)->json([
'status' => 'error',
'message' => 'حجم الفيديو يتجاوز السقف المعتمد (400 ميجابايت). يرجى التحقق من ترميز الضغط 720p.'
]);
$directorateId = $this->resolveDirectorateId($request);
if (!$directorateId) {
$response->status(403)->json(['status' => 'error', 'message' => 'لا توجد مديرية ضمن نطاق صلاحيات الحساب']);
return;
}
// Simulate AI pedagogical audit
$aiScore = mt_rand(88, 98);
$teacherTalkRatio = mt_rand(55, 68); // healthy balance between 50-70%
$socraticCheckpoints = [
[
'timestamp' => '14:20',
'question' => 'لماذا تختلف قوة التجاذب الكتلي عند الاقتراب من سطح الأرض؟',
'objective' => 'الربط بالمنهاج الوزاري - نتاجات الوحدة الثانية'
],
[
'timestamp' => '28:45',
'question' => 'ما هو التفسير الفيزيائي لقصور الجسم الذاتي في حركة المصعد؟',
'objective' => 'إثارة التفكير الاستنتاجي السقراطي'
]
];
$directorate = Database::selectOne(
"SELECT d.id, d.code, d.name, d.type, d.commander_name, d.annual_contract_value,
(SELECT COUNT(*) FROM schools s WHERE s.directorate_id = d.id AND s.is_active = 1) AS total_schools,
(SELECT COUNT(*) FROM students st JOIN schools s ON s.id = st.school_id WHERE s.directorate_id = d.id) AS total_students,
(SELECT COUNT(*) FROM teachers t JOIN schools s ON s.id = t.school_id WHERE s.directorate_id = d.id) AS total_teachers,
(SELECT COUNT(*) FROM field_recorded_lessons f JOIN schools s ON s.id = f.school_id
WHERE s.directorate_id = d.id AND f.recorded_at >= DATE_FORMAT(NOW(), '%Y-%m-01')) AS recorded_lessons_month,
COALESCE((SELECT AVG(f.ai_alignment_score) FROM field_recorded_lessons f JOIN schools s ON s.id = f.school_id
WHERE s.directorate_id = d.id AND f.ai_alignment_score IS NOT NULL), 0) AS ai_average_score,
(SELECT COUNT(*) FROM unified_exams ue WHERE ue.directorate_id = d.id AND ue.status IN ('scheduled','in_progress')) AS active_unified_exams
FROM directorates d WHERE d.id = ? AND d.is_active = 1 LIMIT 1",
[$directorateId]
);
if (!$directorate) {
$response->status(404)->json(['status' => 'error', 'message' => 'المديرية غير موجودة']);
return;
}
$lessonUuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
$schools = Database::select(
"SELECT s.id, s.code, s.name, s.governorate, s.weight_tier, s.annual_fee,
(SELECT COUNT(*) FROM students st WHERE st.school_id = s.id) AS student_count,
(SELECT COUNT(*) FROM teachers t WHERE t.school_id = s.id) AS teacher_count,
(SELECT COUNT(DISTINCT f.teacher_id) FROM field_recorded_lessons f
WHERE f.school_id = s.id AND f.recorded_at >= DATE_SUB(NOW(), INTERVAL 14 DAY)) AS teachers_recorded
FROM schools s WHERE s.directorate_id = ? AND s.is_active = 1 ORDER BY s.name",
[$directorateId]
);
$totalCompliance = 0.0;
foreach ($schools as &$school) {
$teacherCount = (int)$school['teacher_count'];
$compliance = $teacherCount > 0 ? min(100, ((int)$school['teachers_recorded'] / $teacherCount) * 100) : 0.0;
$school['compliance'] = round($compliance, 2);
$school['status'] = $compliance >= 95 ? 'excellent' : ($compliance >= 80 ? 'good' : 'warning_under_quota');
unset($school['teachers_recorded']);
$totalCompliance += $compliance;
}
unset($school);
$directorate['compliance_rate'] = count($schools) > 0 ? round($totalCompliance / count($schools), 2) : 0.0;
$anomalies = Database::select(
"SELECT ess.id, 'exam_integrity' AS type, s.name AS school_name, ue.subject,
CONCAT('شذوذ سرعة: ', ess.speed_anomalies_count, '، تجمع أخطاء: ', ess.error_clustering_count) AS description,
'high' AS severity, ess.created_at AS timestamp
FROM exam_school_sessions ess
JOIN schools s ON s.id = ess.school_id
JOIN unified_exams ue ON ue.id = ess.unified_exam_id
WHERE s.directorate_id = ? AND ess.status = 'flagged_investigation'
ORDER BY ess.id DESC LIMIT 50",
[$directorateId]
);
$response->json([
'status' => 'success',
'message' => 'تم رفع الحصة بنجاح وإتمام التدقيق التربوي السيادي',
'data' => [
'lesson_uuid' => $lessonUuid,
'teacher_name' => $teacherName,
'subject' => $subject,
'lesson_title' => $lessonTitle,
'file_size_mb' => $fileSizeMb,
'duration_minutes' => $durationMin,
'ai_alignment_score' => $aiScore,
'teacher_talk_ratio' => $teacherTalkRatio . '%',
'status' => ($aiScore >= 85) ? 'approved_official' : 'under_supervisor_review',
'socratic_checkpoints' => $socraticCheckpoints,
'report_summary' => 'التزام تام بالخطة الفصلية، تسلسل منطقي في طرح المفاهيم، لغة تربوية سليمة خالية من المخالفات.'
]
'status' => 'success',
'directorate' => $directorate,
'schools' => $schools,
'anomalies' => $anomalies,
'radar' => ['top_teachers' => [], 'needing_support' => []],
]);
}
/**
* Push Unified Exam to School Computer Labs
* POST /api/supervisor/exam/push-to-lab
*/
public function getSchoolDashboard(Request $request, Response $response): void
{
$requestedId = (int)$request->getQuery('school_id', 0);
$schoolId = $this->resolveSchoolId($request, $requestedId);
if (!$schoolId) {
$response->status(403)->json(['status' => 'error', 'message' => 'المدرسة خارج نطاق صلاحيات الحساب']);
return;
}
$school = Database::selectOne(
"SELECT s.id, s.name, s.governorate, s.weight_tier, s.director_name,
(SELECT COUNT(*) FROM students st WHERE st.school_id = s.id) AS student_count,
(SELECT COUNT(*) FROM teachers t WHERE t.school_id = s.id) AS teacher_count
FROM schools s WHERE s.id = ? AND s.is_active = 1 LIMIT 1",
[$schoolId]
);
if (!$school) {
$response->status(404)->json(['status' => 'error', 'message' => 'المدرسة غير موجودة']);
return;
}
$teachers = Database::select(
"SELECT t.id, t.full_name AS name, t.specialization AS subject,
COALESCE(f.grade_level, '') AS grade,
CASE WHEN f.recorded_at >= DATE_SUB(NOW(), INTERVAL 14 DAY) THEN 'completed' ELSE 'pending' END AS quota_status,
COALESCE(f.lesson_title, '') AS last_lesson,
COALESCE(f.ai_alignment_score, 0) AS ai_score,
COALESCE(DATE_FORMAT(f.recorded_at, '%Y-%m-%d %H:%i'), '') AS recorded_at
FROM teachers t
LEFT JOIN field_recorded_lessons f ON f.id = (
SELECT f2.id FROM field_recorded_lessons f2 WHERE f2.teacher_id = t.id ORDER BY f2.recorded_at DESC LIMIT 1
)
WHERE t.school_id = ? ORDER BY t.full_name",
[$schoolId]
);
$activeExams = Database::select(
"SELECT ue.id, ess.id AS session_id, ue.title, ue.subject, ue.grade_level, ue.scheduled_at AS scheduled_time,
ue.duration_minutes, ess.pushed_to_lab, ess.status
FROM exam_school_sessions ess JOIN unified_exams ue ON ue.id = ess.unified_exam_id
WHERE ess.school_id = ? AND ue.status IN ('scheduled','in_progress') ORDER BY ue.scheduled_at",
[$schoolId]
);
$recorded = count(array_filter($teachers, fn($t) => $t['quota_status'] === 'completed'));
$school['biweekly_cycle'] = date('Y-m-d', strtotime('-14 days')) . ' — ' . date('Y-m-d');
$school['cycle_progress'] = $recorded . ' من ' . count($teachers) . ' معلم';
$response->json(['status' => 'success', 'school' => $school, 'teachers' => $teachers, 'active_exams' => $activeExams, 'computer_labs' => []]);
}
public function recordLesson(Request $request, Response $response): void
{
$body = $request->getBody();
$schoolId = $this->resolveSchoolId($request, (int)($body['school_id'] ?? 0));
$teacherId = (int)($body['teacher_id'] ?? 0);
$subject = trim((string)($body['subject'] ?? ''));
$grade = trim((string)($body['grade_level'] ?? ''));
$title = trim((string)($body['lesson_title'] ?? ''));
if (!$schoolId || !$teacherId || $subject === '' || $grade === '' || $title === '' || empty($_FILES['video'])) {
$response->status(422)->json(['status' => 'error', 'message' => 'المدرسة والمعلم والمبحث والصف والعنوان وملف الفيديو مطلوبة']);
return;
}
$teacher = Database::selectOne("SELECT id, full_name FROM teachers WHERE id = ? AND school_id = ? LIMIT 1", [$teacherId, $schoolId]);
if (!$teacher) {
$response->status(404)->json(['status' => 'error', 'message' => 'المعلم غير مرتبط بهذه المدرسة']);
return;
}
$file = $_FILES['video'];
if (($file['error'] ?? UPLOAD_ERR_NO_FILE) !== UPLOAD_ERR_OK || (int)$file['size'] > 400 * 1024 * 1024) {
$response->status(422)->json(['status' => 'error', 'message' => 'ملف الفيديو غير صالح أو يتجاوز 400 ميجابايت']);
return;
}
$uuid = $this->uuid();
$extension = strtolower(pathinfo((string)$file['name'], PATHINFO_EXTENSION));
if (!in_array($extension, ['mp4', 'mov', 'webm'], true)) {
$response->status(422)->json(['status' => 'error', 'message' => 'صيغة الفيديو غير مدعومة']);
return;
}
$videoUrl = VideoService::uploadToR2((string)$file['tmp_name'], "field-lessons/{$schoolId}/{$uuid}.{$extension}", (string)($file['type'] ?? 'video/mp4'));
if (!$videoUrl) {
$response->status(502)->json(['status' => 'error', 'message' => 'تعذر رفع الفيديو إلى Cloudflare R2']);
return;
}
Database::insert(
"INSERT INTO field_recorded_lessons
(uuid, school_id, teacher_id, subject, grade_level, lesson_title, video_path, file_size_mb, duration_minutes, status, recorded_by_name, recorded_by_role)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'pending_ai', ?, ?)",
[$uuid, $schoolId, $teacherId, $subject, $grade, $title, $videoUrl, round((int)$file['size'] / 1048576, 2), (int)($body['duration_minutes'] ?? 0), (string)($request->full_name ?? $request->uuid ?? 'staff'), $request->role === 'school_admin' ? 'principal' : 'supervisor']
);
$response->status(201)->json(['status' => 'success', 'message' => 'تم رفع الحصة وبانتظار التحليل الفعلي', 'data' => ['lesson_uuid' => $uuid, 'teacher_name' => $teacher['full_name'], 'subject' => $subject, 'lesson_title' => $title, 'file_size_mb' => round((int)$file['size'] / 1048576, 2), 'duration_minutes' => (int)($body['duration_minutes'] ?? 0), 'ai_alignment_score' => 0, 'teacher_talk_ratio' => '', 'status' => 'pending_ai', 'socratic_checkpoints' => [], 'report_summary' => 'بانتظار التحليل']]);
}
public function pushExamToLab(Request $request, Response $response): void
{
$body = $request->getBody();
$examId = $body['exam_id'] ?? 'EX-MC-2026-01';
$response->json([
'status' => 'success',
'message' => 'تم بث الامتحان الموحد لمحطات مختبر الحاسوب وتفعيل البيئة المقفلة (Kiosk Mode)',
'exam_id' => $examId,
'pushed_at' => date('Y-m-d H:i:s'),
'connected_nodes' => 32, // lab PCs
'forms_ready' => ['Form A', 'Form B'],
]);
$examId = (int)($body['exam_id'] ?? 0);
$schoolId = $this->resolveSchoolId($request, (int)($body['school_id'] ?? 0));
if (!$examId || !$schoolId) {
$response->status(422)->json(['status' => 'error', 'message' => 'معرف الامتحان والمدرسة مطلوبان']);
return;
}
$exam = Database::selectOne("SELECT id FROM unified_exams WHERE id = ? LIMIT 1", [$examId]);
if (!$exam) {
$response->status(404)->json(['status' => 'error', 'message' => 'الامتحان غير موجود']);
return;
}
Database::query("INSERT INTO exam_school_sessions (unified_exam_id, school_id, pushed_to_lab, pushed_to_lab_at) VALUES (?, ?, 1, NOW()) ON DUPLICATE KEY UPDATE pushed_to_lab = 1, pushed_to_lab_at = NOW()", [$examId, $schoolId]);
$response->json(['status' => 'success', 'message' => 'تم تفعيل الامتحان للمختبر المسجل', 'exam_id' => $examId, 'school_id' => $schoolId, 'pushed_at' => date('Y-m-d H:i:s')]);
}
/**
* Upload Smart Sampled Panoramic Surveillance Video
* POST /api/supervisor/exam/upload-panoramic-sample
*/
public function uploadPanoramicSample(Request $request, Response $response): void
{
$body = $request->getBody();
$fileSizeMb = (float)($body['file_size_mb'] ?? 16.5);
$durationSec = (int)($body['duration_seconds'] ?? 120);
$response->json([
'status' => 'success',
'message' => 'تم حفظ العينة البانورامية المقتضبة بنجاح وربطها بسجل الامتحان',
'file_size_mb' => $fileSizeMb,
'duration_seconds' => $durationSec,
'retention_policy' => 'حفظ محلي مشفر لمدة 14 يوماً حتى اعتماد النتائج',
'correlated_anomalies'=> 0
]);
$sessionId = (int)($body['session_id'] ?? 0);
$session = Database::selectOne("SELECT ess.id, ess.school_id FROM exam_school_sessions ess WHERE ess.id = ? LIMIT 1", [$sessionId]);
if (!$session || !$this->resolveSchoolId($request, (int)$session['school_id']) || empty($_FILES['video'])) {
$response->status(422)->json(['status' => 'error', 'message' => 'جلسة صحيحة وملف فيديو بانورامي مطلوبان']);
return;
}
$file = $_FILES['video'];
$url = VideoService::uploadToR2((string)$file['tmp_name'], "exam-samples/{$sessionId}/{$this->uuid()}.mp4", (string)($file['type'] ?? 'video/mp4'));
if (!$url) {
$response->status(502)->json(['status' => 'error', 'message' => 'تعذر رفع العينة إلى R2']);
return;
}
$size = round((int)$file['size'] / 1048576, 2);
Database::query("UPDATE exam_school_sessions SET panoramic_video_path = ?, panoramic_size_mb = ? WHERE id = ?", [$url, $size, $sessionId]);
$response->json(['status' => 'success', 'message' => 'تم حفظ العينة وربطها بالجلسة', 'file_size_mb' => $size]);
}
/**
* توليد الامتحان الموحد بنموذجين متوازيين (نموذج أ ونموذج ب)
* GET /api/unified-exams/dual-forms
*/
public function generateDualForms(Request $request, Response $response): void
{
$subject = (string)$request->getQuery('subject', 'الفيزياء');
$gradeLevel = (string)$request->getQuery('grade_level', 'الأول ثانوي');
$forms = \App\Services\UnifiedExamService::generateDualForms($subject, $gradeLevel);
$response->json([
'status' => 'success',
'data' => $forms
]);
$forms = \App\Services\UnifiedExamService::generateDualForms((string)$request->getQuery('subject', ''), (string)$request->getQuery('grade_level', ''));
$response->json(['status' => 'success', 'data' => $forms]);
}
/**
* كشف الشذوذ الإحصائي ومكافحة الغش في جلسة الامتحان الموحد
* POST /api/unified-exams/evaluate-integrity
*/
public function evaluateExamSessionIntegrity(Request $request, Response $response): void
{
$body = $request->getBody();
$submissions = $body['submissions'] ?? [];
if (empty($submissions)) {
// Default sample submissions demonstrating all 3 statistical anomalies
$submissions = [
[
'student_id' => 101,
'student_name' => 'سيف الدين خالد الرواشدة',
'seat_number' => 'قاعة 1 — مقعد 04',
'time_spent_seconds' => 195, // < 300s
'score' => 95,
'historical_average' => 50.0,
'answers' => [
1 => ['selected_option' => 0, 'is_correct' => true],
2 => ['selected_option' => 0, 'is_correct' => true],
]
],
[
'student_id' => 102,
'student_name' => 'عمر أحمد الحباشنة',
'seat_number' => 'قاعة 1 — مقعد 05',
'time_spent_seconds' => 720,
'score' => 92,
'historical_average' => 42.0, // Historical leap
'answers' => [
1 => ['selected_option' => 2, 'is_correct' => false], // identical error
2 => ['selected_option' => 1, 'is_correct' => false],
]
],
[
'student_id' => 103,
'student_name' => 'فيصل محمود الخريشا',
'seat_number' => 'قاعة 1 — مقعد 06',
'time_spent_seconds' => 740,
'score' => 88,
'historical_average' => 84.0,
'answers' => [
1 => ['selected_option' => 2, 'is_correct' => false], // identical error
2 => ['selected_option' => 1, 'is_correct' => false],
]
]
];
$submissions = $request->getBody()['submissions'] ?? [];
if (!is_array($submissions) || !$submissions) {
$response->status(422)->json(['status' => 'error', 'message' => 'لا يمكن تقييم النزاهة دون تسليمات امتحان حقيقية']);
return;
}
$result = \App\Services\UnifiedExamService::evaluateExamSessionIntegrity($submissions);
$response->json([
'status' => 'success',
'data' => $result
]);
$response->json(['status' => 'success', 'data' => \App\Services\UnifiedExamService::evaluateExamSessionIntegrity($submissions)]);
}
/**
* إرسال ومضات التقارير الشهرية لأولياء الأمور عبر الواتساب وبوابة نبيه
* POST /api/parent-reports/dispatch
*/
public function dispatchParentReports(Request $request, Response $response): void
{
$body = $request->getBody();
$studentId = !empty($body['student_id']) ? (int)$body['student_id'] : null;
$schoolId = !empty($body['school_id']) ? (int)$body['school_id'] : 1;
if ($studentId) {
$res = \App\Services\ParentReportService::dispatchReportToGuardian($studentId);
} else {
$res = \App\Services\ParentReportService::dispatchBatchReports($schoolId);
$schoolId = $this->resolveSchoolId($request, (int)($body['school_id'] ?? 0));
if (!$schoolId) {
$response->status(403)->json(['status' => 'error', 'message' => 'المدرسة خارج نطاق الصلاحية']);
return;
}
$response->json($res);
$studentId = (int)($body['student_id'] ?? 0);
if ($studentId) {
$student = Database::selectOne("SELECT id FROM students WHERE id = ? AND school_id = ? LIMIT 1", [$studentId, $schoolId]);
if (!$student) {
$response->status(404)->json(['status' => 'error', 'message' => 'الطالب غير موجود في المدرسة']);
return;
}
}
$result = $studentId ? \App\Services\ParentReportService::dispatchReportToGuardian($studentId) : \App\Services\ParentReportService::dispatchBatchReports($schoolId);
$response->json($result);
}
/**
* استيراد كشف المدرسة وتشفير الأرقام الوطنية (AES-256-GCM)
* POST /api/school-roster/import
*/
public function importSchoolRoster(Request $request, Response $response): void
{
$body = $request->getBody();
$schoolId = !empty($body['school_id']) ? (int)$body['school_id'] : 1;
$records = $body['records'] ?? [];
if (empty($records)) {
$records = \App\Services\SchoolRosterService::getSampleRosterData();
$schoolId = $this->resolveSchoolId($request, (int)($body['school_id'] ?? 0));
$records = $body['records'] ?? [];
if (!$schoolId || !is_array($records) || !$records) {
$response->status(422)->json(['status' => 'error', 'message' => 'المدرسة وكشف حقيقي غير فارغ مطلوبان']);
return;
}
$response->json(\App\Services\SchoolRosterService::importStudentRoster($schoolId, $records));
}
$result = \App\Services\SchoolRosterService::importStudentRoster($schoolId, $records);
private function resolveDirectorateId(Request $request): int
{
if ($request->role === 'super_admin') {
$requested = (int)$request->getQuery('directorate_id', 0);
if ($requested) return $requested;
}
if ($request->directorate_id) return (int)$request->directorate_id;
if ($request->school_id) {
$row = Database::selectOne("SELECT directorate_id FROM schools WHERE id = ? LIMIT 1", [$request->school_id]);
return (int)($row['directorate_id'] ?? 0);
}
return 0;
}
$response->json($result);
private function resolveSchoolId(Request $request, int $requested): int
{
if (in_array($request->role, ['school_admin', 'supervisor'], true)) {
return $request->school_id && (!$requested || $requested === $request->school_id) ? (int)$request->school_id : 0;
}
if ($request->role === 'directorate_admin') {
if (!$requested || !$request->directorate_id) return 0;
$row = Database::selectOne("SELECT id FROM schools WHERE id = ? AND directorate_id = ? LIMIT 1", [$requested, $request->directorate_id]);
return $row ? $requested : 0;
}
return $request->role === 'super_admin' ? $requested : 0;
}
private function uuid(): string
{
$data = random_bytes(16);
$data[6] = chr((ord($data[6]) & 0x0f) | 0x40);
$data[8] = chr((ord($data[8]) & 0x3f) | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
}
}