json(['status'=>'success','data'=>Database::select('SELECT * FROM directorates ORDER BY id DESC')]); } public function saveDirectorate(Request $request, Response $response): void { $b=$request->getBody(); $id=(int)($b['id']??0); $name=trim((string)($b['name']??'')); $code=trim((string)($b['code']??'')); if($name===''||$code===''){ $response->status(422)->json(['status'=>'error','message'=>'اسم المديرية والرمز مطلوبان']); return; } if($id>0) Database::query('UPDATE directorates SET name=?,code=?,type=?,commander_name=?,phone=?,is_active=? WHERE id=?',[$name,$code,$b['type']??'ministry',$b['commander_name']??null,$b['phone']??null,(int)($b['is_active']??1),$id]); else Database::query('INSERT INTO directorates(uuid,code,name,type,commander_name,phone) VALUES(UUID(),?,?,?,?,?)',[$code,$name,$b['type']??'ministry',$b['commander_name']??null,$b['phone']??null]); $response->json(['status'=>'success']); } public function toggleDirectorate(Request $request, Response $response): void { $b=$request->getBody(); Database::query("UPDATE directorates SET is_active=IF(is_active=1,0,1) WHERE id=?",[(int)($b['id']??0)]); $response->json(['status'=>'success']); } public function schools(Request $request, Response $response): void { $response->json(['status' => 'success', 'data' => Database::select( "SELECT s.*, d.name AS directorate_name FROM schools s LEFT JOIN directorates d ON d.id=s.directorate_id ORDER BY s.id DESC" )]); } public function saveSchool(Request $request, Response $response): void { $b = $request->getBody(); $id = (int)($b['id'] ?? 0); $name = trim((string)($b['name'] ?? '')); $code = trim((string)($b['code'] ?? '')); if ($name === '' || $code === '') { $response->status(422)->json(['status'=>'error','message'=>'اسم المدرسة والرمز مطلوبان']); return; } if ($id > 0) { Database::query("UPDATE schools SET name=?, code=?, type=?, directorate_id=?, director_name=?, phone=?, city=?, governorate=?, latitude=?, longitude=? WHERE id=?", [$name,$code,$b['type']??'center',$b['directorate_id']?:null,$b['director_name']??null,$b['phone']??null,$b['city']??'Amman',$b['governorate']??'العاصمة',$b['latitude']??null,$b['longitude']??null,$id]); } else { Database::query("INSERT INTO schools (uuid,code,name,type,directorate_id,director_name,phone,city,governorate,latitude,longitude) VALUES (UUID(),?,?,?,?,?,?,?,?,?,?)", [$code,$name,$b['type']??'center',$b['directorate_id']?:null,$b['director_name']??null,$b['phone']??null,$b['city']??'Amman',$b['governorate']??'العاصمة',$b['latitude']??null,$b['longitude']??null]); $id=(int)Database::getInstance()->lastInsertId(); } $response->json(['status'=>'success','data'=>['id'=>$id]]); } public function toggleSchool(Request $request, Response $response): void { $b=$request->getBody(); Database::query("UPDATE schools SET is_active=IF(is_active=1,0,1) WHERE id=?",[(int)($b['id']??0)]); $response->json(['status'=>'success']); } public function staff(Request $request, Response $response): void { $response->json(['status'=>'success','data'=>Database::select("SELECT sa.id,sa.uuid,sa.identity_id,sa.full_name,sa.role,sa.school_id,sa.directorate_id,sa.status,ai.phone_hash FROM staff_accounts sa JOIN auth_identities ai ON ai.id=sa.identity_id ORDER BY sa.id DESC")]); } public function toggleStaff(Request $request, Response $response): void { $b=$request->getBody(); Database::query("UPDATE staff_accounts SET status=IF(status='active','suspended','active') WHERE id=?",[(int)($b['id']??0)]); $response->json(['status'=>'success']); } public function saveStaff(Request $request, Response $response): void { $b=$request->getBody(); $id=(int)($b['id']??0); $role=(string)($b['role']??''); $name=trim((string)($b['full_name']??'')); $phone=preg_replace('/\D+/','',(string)($b['phone']??'')); if($name===''||!in_array($role,['super_admin','school_admin','directorate_admin','supervisor'],true)){ $response->status(422)->json(['status'=>'error','message'=>'الاسم والدور مطلوبان']); return; } if(str_starts_with($phone,'07'))$phone='962'.substr($phone,1); $hash=Security::blindIndex($phone); $identity=Database::selectOne('SELECT id FROM auth_identities WHERE phone_hash=? LIMIT 1',[$hash]); if(!$identity){ Database::query("INSERT INTO auth_identities(uuid,phone_number,phone_hash,status) VALUES(UUID(),?,?, 'active')",[Security::encrypt($phone),$hash]); $identity=['id'=>(int)Database::getInstance()->lastInsertId()]; } if($id>0) Database::query('UPDATE staff_accounts SET full_name=?,role=?,school_id=?,directorate_id=?,status=\'active\' WHERE id=?',[$name,$role,$b['school_id']?:null,$b['directorate_id']?:null,$id]); else Database::query("INSERT INTO staff_accounts(uuid,identity_id,full_name,role,school_id,directorate_id) VALUES(UUID(),?,?,?,?,?)",[$identity['id'],$name,$role,$b['school_id']?:null,$b['directorate_id']?:null]); $response->json(['status'=>'success']); } public function overview(Request $request, Response $response): void { $counts = [ 'total_directorates' => $this->count('directorates'), 'total_schools' => $this->count('schools'), 'total_students' => $this->count('students'), 'total_teachers' => $this->count('teachers'), ]; $response->json([ 'status' => 'success', 'data' => array_merge($counts, [ // Payment settlement and infrastructure monitoring are not // connected yet. Null is intentional: zero would be a claim. 'gross_margin_percent' => null, 'treasury_balance_jod' => null, 'total_cliq_inflow_jod' => null, 'pending_payouts_count' => null, 'r2_cost_savings_jod' => null, 'uptime_percent' => null, 'measurement_notes' => [ 'financial_metrics' => 'غير متاحة حتى ربط مزود الدفع ودفتر التسوية.', 'r2_cost_savings_jod' => 'يتطلب ربط Cloudflare Billing Analytics', 'uptime_percent' => 'يتطلب ربط مزود مراقبة خارجي', ], ]), ]); } public function payouts(Request $request, Response $response): void { CliqPaymentService::ensureSchema(); $items = Database::select( "SELECT pq.id, t.full_name AS teacher_name, pq.teacher_cliq_alias AS cliq_alias, pq.amount_jod, pq.created_at AS requested_at, pq.status FROM payout_queue pq JOIN teachers t ON t.id = pq.teacher_id ORDER BY pq.id DESC LIMIT 100" ); $response->json(['status' => 'success', 'data' => $items]); } public function aiNodes(Request $request, Response $response): void { $configured = trim((string)getenv('SAQEL_AI_NODES_JSON')); $nodes = $configured !== '' ? json_decode($configured, true) : []; if (!is_array($nodes)) { $response->status(500)->json(['status' => 'error', 'message' => 'إعداد عقد الذكاء الاصطناعي غير صالح']); return; } $response->json(['status' => 'success', 'data' => array_values($nodes)]); } public function securityAlerts(Request $request, Response $response): void { // No synthetic alerts: an audit-event pipeline will populate this endpoint. $response->json(['status' => 'success', 'data' => [], 'measurement_status' => 'unavailable']); } public function publicationBundles(Request $request, Response $response): void { $bundles = Database::select( "SELECT pb.id, pb.uuid, pb.bundle_version, pb.status, pb.published_at, pb.created_at, cl.title AS lesson_title, cl.curriculum_edition, s.name AS subject_name, g.name AS grade_name FROM publication_bundles pb JOIN curriculum_lessons cl ON cl.id = pb.curriculum_lesson_id JOIN curriculum_units cu ON cu.id = cl.unit_id JOIN curriculum_courses cc ON cc.id = cu.course_id JOIN subjects s ON s.id = cc.subject_id JOIN grade_levels g ON g.id = cc.grade_level_id ORDER BY pb.id DESC LIMIT 100" ); $submissions = Database::select( "SELECT vv.id AS version_id, vv.uuid AS version_uuid, vv.version_number, vv.status, vv.created_at, t.full_name AS teacher_name, cl.title AS lesson_title FROM video_versions vv JOIN teacher_submissions ts ON ts.id = vv.teacher_submission_id JOIN teachers t ON t.id = ts.teacher_id JOIN curriculum_lessons cl ON cl.id = ts.curriculum_lesson_id ORDER BY vv.id DESC LIMIT 100" ); $response->json([ 'status' => 'success', 'data' => [ 'bundles' => $bundles, 'submissions' => $submissions, ], ]); } public function reviewSubmission(Request $request, Response $response): void { $body = $request->getBody(); $versionUuid = trim((string)($body['version_uuid'] ?? '')); $decision = (string)($body['decision'] ?? ''); // 'approved' or 'rejected' if (!in_array($decision, ['approved', 'rejected'], true) || $versionUuid === '') { $response->status(422)->json(['status' => 'error', 'message' => 'بيانات مراجعة النسخة غير صالحة']); return; } $version = Database::selectOne( "SELECT vv.id, vv.teacher_submission_id FROM video_versions vv WHERE vv.uuid = ? LIMIT 1", [$versionUuid] ); if (!$version) { $response->status(404)->json(['status' => 'error', 'message' => 'نسخة الفيديو غير موجودة']); return; } $status = $decision === 'approved' ? 'published' : 'rejected'; Database::query("UPDATE video_versions SET status = ?, reviewed_at = NOW() WHERE id = ?", [$status, (int)$version['id']]); if ($decision === 'approved') { Database::query( "UPDATE teacher_submissions SET status = 'published', current_published_video_version_id = ? WHERE id = ?", [(int)$version['id'], (int)$version['teacher_submission_id']] ); } $response->json(['status' => 'success', 'message' => $decision === 'approved' ? 'تم اعتماد ونشر النسخة بنجاح' : 'تم رفض النسخة']); } private function count(string $table): int { $row = Database::selectOne("SELECT COUNT(*) AS count_value FROM `{$table}`"); return (int)($row['count_value'] ?? 0); } }