status(400)->json([ 'status' => 'error', 'message' => 'يرجى اختيار ملف PDF صالح للمنهاج الوزاري.' ]); return; } $file = $_FILES['pdf_file']; $origName = $file['name']; $tmpPath = $file['tmp_name']; $taskId = uniqid('task_'); // Setup processing directory $processingDir = __DIR__ . '/../../storage/curriculum/processing'; if (!is_dir($processingDir)) { mkdir($processingDir, 0777, true); } $savedPdfPath = $processingDir . '/' . $taskId . '.pdf'; move_uploaded_file($tmpPath, $savedPdfPath); // Initialize Task State $taskState = [ 'task_id' => $taskId, 'status' => 'queued', 'progress' => 0, 'message' => 'تم استلام الملف وجاري تحويله للمعالجة المعمقة...', 'orig_name' => $origName ]; file_put_contents($processingDir . '/' . $taskId . '.json', json_encode($taskState, JSON_UNESCAPED_UNICODE)); // Prepare task paths $scriptPath = realpath(__DIR__ . '/../../scripts/curriculum_worker.php'); $logPath = $processingDir . '/' . $taskId . '.log'; // Send processing response immediately $response->json([ 'status' => 'processing', 'task_id' => $taskId, 'message' => 'تم استلام الكتاب، وجاري المعالجة المعمقة بواسطة الذكاء الاصطناعي. يرجى الانتظار...' ]); // Terminate request and flush buffers so the client receives the JSON immediately if (function_exists('fastcgi_finish_request')) { fastcgi_finish_request(); } else { // Fallback for non-FPM ob_end_flush(); flush(); } // Run worker completely in the background via CLI try { $cmd = "php " . escapeshellarg($scriptPath) . " " . escapeshellarg($taskId) . " > /dev/null 2>&1 &"; exec($cmd); // Log that we spawned it file_put_contents($logPath, "[System] Spawning background worker: $cmd\n", FILE_APPEND); } catch (\Exception $e) { $err = "[Exception] " . $e->getMessage(); file_put_contents($logPath, $err . "\n", FILE_APPEND); // Update state file to error $stateFile = $processingDir . '/' . $taskId . '.json'; if (file_exists($stateFile)) { $st = json_decode(file_get_contents($stateFile), true); $st['status'] = 'error'; $st['message'] = 'تعطل النظام أثناء التحليل (Crash). يرجى مراجعة سجل الأخطاء.'; file_put_contents($stateFile, json_encode($st, JSON_UNESCAPED_UNICODE)); } } // Ensure script stops here so it doesn't return anything else exit; } /** * Poll Upload Status */ public function getUploadStatus(Request $request, Response $response): void { $taskId = $request->getQueryParams()['task_id'] ?? ''; if (empty($taskId)) { $response->status(400)->json(['status' => 'error', 'message' => 'رقم المهمة مفقود']); return; } $processingDir = __DIR__ . '/../../storage/curriculum/processing'; $taskId = basename($taskId); if (!preg_match('/^task_[A-Za-z0-9_.-]+$|^video_[A-Za-z0-9_.-]+$/', $taskId)) { $response->status(400)->json(['status' => 'error', 'message' => 'رقم المهمة غير صالح']); return; } $stateFile = $processingDir . '/' . $taskId . '.json'; if (!file_exists($stateFile)) { $response->status(404)->json(['status' => 'error', 'message' => 'المهمة غير موجودة']); return; } $state = json_decode(file_get_contents($stateFile), true); $response->json($state); } /** * Get Upload Log */ public function getUploadLog(Request $request, Response $response): void { $taskId = $request->getQueryParams()['task_id'] ?? ''; if (empty($taskId)) { $response->status(400)->json(['status' => 'error', 'message' => 'رقم المهمة مفقود']); return; } $processingDir = __DIR__ . '/../../storage/curriculum/processing'; $logFile = $processingDir . '/' . $taskId . '.log'; if (!file_exists($logFile)) { $response->status(404)->json(['status' => 'error', 'message' => 'سجل الأخطاء غير موجود']); return; } $logContent = file_get_contents($logFile); $response->json(['status' => 'success', 'log' => $logContent]); } /** GET /api/curriculum/lessons/{lessonId}/english-package */ public function getPublishedEnglishPackage(Request $request, Response $response): void { try { $result=LearningPackageService::publishedEnglish(trim((string)$request->getParam('lessonId','')),(int)$request->user_id,$request->getHeader('x-national-id')); $response->status((int)$result['http_status'])->json(array_diff_key($result,['http_status'=>true])); } catch (\Throwable $e) { error_log('English package read failed: '.$e->getMessage()); $response->status(503)->json(['status'=>'unavailable','message'=>'تعذر قراءة حزمة الإنجليزية.']); } } /** * Get Complete Live Tree */ public function getTree(Request $request, Response $response): void { $tree = CurriculumService::getCurriculumTree(); // Manifest resource paths are intake metadata, not student-facing grants. // Until they are represented by approved assets in a published bundle, do // not expose them as available textbooks or worksheets. $removeUnpublishedResources = function (&$node) use (&$removeUnpublishedResources): void { if (!is_array($node)) { return; } if (isset($node['resources']) && is_array($node['resources'])) { foreach ($node['resources'] as &$resourceGroup) { if (is_array($resourceGroup) && array_key_exists('items', $resourceGroup)) { $resourceGroup['items'] = []; } } unset($resourceGroup); } foreach ($node as &$child) { $removeUnpublishedResources($child); } unset($child); }; $removeUnpublishedResources($tree); try { $approvedLessons = Database::select("SELECT id, uuid, source_manifest_path FROM curriculum_lessons WHERE source_status='approved'"); $byPath = []; $lessonMap = []; foreach ($approvedLessons as $row) { $p = (string)$row['source_manifest_path']; $byPath[$p] = [ 'curriculum_lesson_id' => (string)$row['uuid'], 'has_video' => false, ]; $lessonMap[(int)$row['id']] = $p; } if (!empty($lessonMap)) { try { $videoCounts = Database::select( "SELECT ts.curriculum_lesson_id, COUNT(vv.id) AS video_count FROM teacher_submissions ts JOIN video_versions vv ON vv.id = ts.current_published_video_version_id AND vv.status = 'published' WHERE ts.status = 'published' GROUP BY ts.curriculum_lesson_id" ); foreach ($videoCounts as $vc) { $lid = (int)$vc['curriculum_lesson_id']; if (isset($lessonMap[$lid]) && (int)$vc['video_count'] > 0) { $byPath[$lessonMap[$lid]]['has_video'] = true; } } } catch (\Throwable $ve) { // teacher_submissions or video_versions not yet populated or migrated; keep has_video = false } } foreach ($tree as $gKey => $grade) { if (!isset($tree[$gKey]['subjects']) || !is_array($tree[$gKey]['subjects'])) continue; foreach ($tree[$gKey]['subjects'] as $sKey => $subject) { if (!isset($tree[$gKey]['subjects'][$sKey]['semesters']) || !is_array($tree[$gKey]['subjects'][$sKey]['semesters'])) continue; foreach ($tree[$gKey]['subjects'][$sKey]['semesters'] as $semKey => $semester) { if (!isset($tree[$gKey]['subjects'][$sKey]['semesters'][$semKey]['units']) || !is_array($tree[$gKey]['subjects'][$sKey]['semesters'][$semKey]['units'])) continue; foreach ($tree[$gKey]['subjects'][$sKey]['semesters'][$semKey]['units'] as $uKey => $unit) { if (!isset($tree[$gKey]['subjects'][$sKey]['semesters'][$semKey]['units'][$uKey]['lessons']) || !is_array($tree[$gKey]['subjects'][$sKey]['semesters'][$semKey]['units'][$uKey]['lessons'])) continue; foreach ($tree[$gKey]['subjects'][$sKey]['semesters'][$semKey]['units'][$uKey]['lessons'] as $lKey => $lesson) { $path = (string)($lesson['file'] ?? ''); if (isset($byPath[$path])) { $tree[$gKey]['subjects'][$sKey]['semesters'][$semKey]['units'][$uKey]['lessons'][$lKey] = array_merge($lesson, $byPath[$path]); } } } } } } // The manifest describes intake files only. Student-visible resources // are rebuilt from approved, rights-cleared assets in a published // bundle, and expose an opaque asset UUID rather than a storage path. $resourceRows = Database::select( "SELECT cl.subject_key, a.uuid AS asset_id, a.asset_type, a.mime_type, pba.role, pba.sort_order FROM publication_bundles pb JOIN curriculum_lessons cl ON cl.id = pb.curriculum_lesson_id JOIN publication_bundle_assets pba ON pba.publication_bundle_id = pb.id JOIN content_assets a ON a.id = pba.content_asset_id WHERE pb.status = 'published' AND cl.source_status = 'approved' AND a.review_status = 'approved' AND a.rights_status = 'cleared' AND pba.role IN ('textbook', 'worksheet') ORDER BY cl.subject_key, pba.role, pba.sort_order, a.id" ); $resourcesBySubject = []; foreach ($resourceRows as $row) { $group = $row['role'] === 'textbook' ? 'textbooks' : 'worksheets'; $subjectKey = (string)$row['subject_key']; $resourcesBySubject[$subjectKey] ??= ['textbooks' => [], 'worksheets' => []]; $resourcesBySubject[$subjectKey][$group][] = [ 'asset_id' => (string)$row['asset_id'], 'asset_type' => (string)$row['asset_type'], 'mime_type' => (string)$row['mime_type'], 'type' => $group === 'textbooks' ? 'textbook' : 'worksheet', ]; } foreach ($tree as $gKey => $grade) { if (!isset($tree[$gKey]['subjects']) || !is_array($tree[$gKey]['subjects'])) continue; foreach ($tree[$gKey]['subjects'] as $sKey => $subject) { $subjectResources = $resourcesBySubject[(string)$sKey] ?? ['textbooks' => [], 'worksheets' => []]; foreach (['textbooks', 'worksheets'] as $group) { foreach ($subjectResources[$group] as $index => &$resource) { $resource['title'] = $group === 'textbooks' ? 'كتاب منشور ' . ($index + 1) : 'ورقة عمل منشورة ' . ($index + 1); } unset($resource); } $tree[$gKey]['subjects'][$sKey]['resources'] = [ 'textbooks' => ['items' => $subjectResources['textbooks']], 'worksheets' => ['items' => $subjectResources['worksheets']], ]; } } } catch (\Throwable $e) { error_log('Published curriculum tree enrichment unavailable: '.$e->getMessage()); } $response->json(['status'=>'success','data'=>$tree]); } /** * Get Single Lesson Markdown Content */ public function getLessonContent(Request $request, Response $response): void { $file = $request->getQueryParams()['file'] ?? ''; if (empty($file)) { $response->status(400)->json(['status' => 'error', 'message' => 'مسار الملف مطلوب']); return; } $content = CurriculumService::getLessonMarkdown($file); $assets = CurriculumService::getLessonAiAssets($file); $serverFullPath = realpath(__DIR__ . '/../../storage/curriculum') . '/' . ltrim($file, '/'); $response->json([ 'status' => 'success', 'file' => $file, 'server_full_path' => $serverFullPath, 'content' => $content, 'ai_assets' => $assets ]); } public function saveLessonContent(Request $request, Response $response): void { $body = $request->getBody(); $file = $body['file'] ?? ''; $content = $body['content'] ?? ''; $aiAssets = $body['ai_assets'] ?? null; if (empty($file) || empty($content)) { $response->status(400)->json(['status' => 'error', 'message' => 'بيانات الحفظ غير مكتملة']); return; } CurriculumService::saveLessonMarkdown($file, $content); if ($aiAssets !== null) { CurriculumService::saveLessonAiAssets($file, $aiAssets); } // Sync to MySQL Database Table `lessons` if connected try { $aiVideoUrl = $aiAssets['ai_video_url'] ?? null; $cheatSheet = $aiAssets['cheat_sheet'] ?? null; $socraticJson = isset($aiAssets['socratic_quiz']) ? json_encode($aiAssets['socratic_quiz'], JSON_UNESCAPED_UNICODE) : null; // Search lesson by matching filename or title $filename = basename($file, '.md'); \App\Core\Database::query( "UPDATE lessons SET markdown_content = ?, ai_video_url = COALESCE(?, ai_video_url), cheat_sheet_markdown = COALESCE(?, cheat_sheet_markdown), socratic_quiz_json = COALESCE(?, socratic_quiz_json) WHERE title LIKE ? OR markdown_content LIKE ?", [$content, $aiVideoUrl, $cheatSheet, $socraticJson, "%{$filename}%", "%{$file}%"] ); } catch (\Throwable $dbEx) { error_log("Curriculum DB save sync note: " . $dbEx->getMessage()); } $serverFullPath = realpath(__DIR__ . '/../../storage/curriculum') . '/' . ltrim($file, '/'); $response->json([ 'status' => 'success', 'message' => 'تم حفظ واعتماد محتوى الدرس في المنهاج وقاعدة البيانات بنجاح!', 'server_full_path' => $serverFullPath ]); } public function generateAiAssets(Request $request, Response $response): void { $body = $request->getBody(); $file = $body['file'] ?? ''; if (empty($file)) { $response->status(400)->json(['status' => 'error', 'message' => 'مسار الملف مطلوب']); return; } $content = CurriculumService::getLessonMarkdown($file); if (empty($content)) { $response->status(400)->json(['status' => 'error', 'message' => 'محتوى الدرس فارغ']); return; } // Generate Assets require_once __DIR__ . '/../Services/AiLessonEnhancerService.php'; $assets = \App\Services\AiLessonEnhancerService::generateFromText('الدرس', $content); if ($assets) { CurriculumService::saveLessonAiAssets($file, $assets); $response->json(['status' => 'success', 'ai_assets' => $assets]); } else { $response->status(500)->json(['status' => 'error', 'message' => 'فشل توليد المخرجات الذكية']); } } /** * Direct Video Upload to Platform Storage with Semantic Lesson Binding */ public function uploadLessonVideo(Request $request, Response $response): void { $file = $_POST['file'] ?? ''; if (empty($file)) { $response->status(400)->json(['status' => 'error', 'message' => 'مسار الدرس مطلوب']); return; } if (empty($_FILES['video_file']) || $_FILES['video_file']['error'] !== UPLOAD_ERR_OK) { $response->status(400)->json(['status' => 'error', 'message' => 'يرجى اختيار ملف فيديو صالح (MP4, WebM, MOV)']); return; } try { // Video transcoding and R2 upload can take longer than a web request. // Persist the upload and hand the heavy work to a CLI worker. $processingDir = __DIR__ . '/../../storage/curriculum/processing'; if (!is_dir($processingDir)) mkdir($processingDir, 0755, true); $taskId = uniqid('video_', true); $extension = strtolower(pathinfo($_FILES['video_file']['name'], PATHINFO_EXTENSION)) ?: 'mp4'; $savedVideoPath = $processingDir . '/' . $taskId . '.' . $extension; if (!move_uploaded_file($_FILES['video_file']['tmp_name'], $savedVideoPath)) { throw new \RuntimeException('تعذر حفظ ملف الفيديو المؤقت على السيرفر.'); } file_put_contents($processingDir . '/' . $taskId . '.json', json_encode([ 'task_id' => $taskId, 'status' => 'queued', 'progress' => 5, 'message' => 'تم استلام الفيديو، وسيبدأ التحويل والرفع السحابي الآن.', 'file' => $file, 'video_path' => $savedVideoPath, 'original_name' => $_FILES['video_file']['name'] ], JSON_UNESCAPED_UNICODE)); $response->status(202)->json(['status' => 'processing', 'task_id' => $taskId, 'message' => 'تم استلام الفيديو وجاري تحويله ورفعه إلى Cloudflare R2...']); if (function_exists('fastcgi_finish_request')) fastcgi_finish_request(); elseif (ob_get_level()) { ob_end_flush(); flush(); } $scriptPath = realpath(__DIR__ . '/../../scripts/video_upload_worker.php'); $logPath = $processingDir . '/' . $taskId . '.log'; exec('php ' . escapeshellarg($scriptPath) . ' ' . escapeshellarg($taskId) . ' >> ' . escapeshellarg($logPath) . ' 2>&1 &'); return; } catch (\Throwable $e) { $response->status(500)->json([ 'status' => 'error', 'message' => $e->getMessage() ]); } } /** * Generate & Refresh AI Question Bank for any Unit or Lesson (Triggered from Curriculum Studio) * POST /api/curriculum/generate-questions */ public function generateQuestionBank(Request $request, Response $response): void { $body = $request->getBody(); $unitPath = $body['unit_path'] ?? 'grade_10/math_10/semester_1/unit_01'; $courseId = (int)($body['course_id'] ?? 1); $count = (int)($body['count'] ?? 50); try { require_once __DIR__ . '/../Services/AiQuestionBankGeneratorService.php'; $result = \App\Services\AiQuestionBankGeneratorService::generateUnitQuestionBank($unitPath, $courseId, $count); $response->json([ 'status' => 'success', 'message' => "تم توليد وتحديث بنك الأسئلة بالذكاء الاصطناعي بنجاح ({$result['total_in_bank']} سؤالاً معيارياً في قاعدة البيانات)!", 'data' => $result ]); } catch (\Throwable $e) { $response->status(500)->json([ 'status' => 'error', 'message' => 'تعذر توليد بنك الأسئلة: ' . $e->getMessage() ]); } } /** * List all available simulations for a subject or overall * GET /api/curriculum/simulations */ public function listSimulations(Request $request, Response $response): void { $subject = $request->getQueryParams()['subject'] ?? 'physics_10'; $simDir = realpath(__DIR__ . '/../../storage/curriculum/simulations') . "/{$subject}"; $simulations = []; if (is_dir($simDir)) { $files = glob("{$simDir}/*.html"); foreach ($files as $f) { $slug = basename($f, '.html'); $title = ($slug === 'vectors_lab') ? 'مختبر جمع وتحليل المتجهات التفاعلي' : (($slug === 'motion_1d_lab') ? 'مختبر الحركة في بُعد واحد والتسارع' : $slug); $unit = ($slug === 'vectors_lab') ? 'الوحدة 1: المتجهات والكميات' : (($slug === 'motion_1d_lab') ? 'الوحدة 2: الحركة والقوى' : 'تجارب تفاعلية'); $simulations[] = [ 'slug' => $slug, 'title' => $title, 'unit_title' => $unit, 'url' => "/api/curriculum/simulations/{$subject}/{$slug}", 'has_canvas' => true, 'watermark' => 'منصة صَقِل التعليمية الذكية © Saqel Lab', ]; } } $response->json([ 'status' => 'success', 'data' => $simulations ]); } /** * Get / Serve Interactive HTML5 Canvas Simulation * GET /api/curriculum/simulations/{subject}/{simName} */ public function getSimulation(Request $request, Response $response): void { $subject = preg_replace('/[^a-zA-Z0-9_-]/', '', $request->getParam('subject') ?? ''); $simName = preg_replace('/[^a-zA-Z0-9_-]/', '', $request->getParam('simName') ?? ''); $simPath = realpath(__DIR__ . '/../../storage/curriculum/simulations') . "/{$subject}/{$simName}.html"; if (!file_exists($simPath)) { // Fallback check in root simulations $simPath = realpath(__DIR__ . '/../../storage/curriculum/simulations') . "/{$simName}.html"; } if (!file_exists($simPath)) { $response->status(404)->json(['status' => 'error', 'message' => 'المحاكي التفاعلي غير موجود']); return; } header('Content-Type: text/html; charset=UTF-8'); header('X-Frame-Options: SAMEORIGIN'); readfile($simPath); exit; } /** * Get real document or textbook markdown content dynamically per subject * GET /api/curriculum/document */ public function getDocumentContent(Request $request, Response $response): void { $params = $request->getQueryParams(); $file = $params['file'] ?? ''; $subject = strtolower($params['subject'] ?? ''); $type = $params['type'] ?? 'textbook'; $storage = realpath(__DIR__ . '/../../storage/curriculum'); if (!$storage || empty($file) || !str_ends_with(strtolower($file), '.md')) { $response->status(400)->json([ 'status' => 'error', 'message' => 'يجب تحديد ملف Markdown معتمد للوثيقة المطلوبة.' ]); return; } $candidate = realpath($storage . '/' . ltrim($file, '/')); $storagePrefix = rtrim($storage, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; if ($candidate && str_starts_with($candidate, $storagePrefix) && is_file($candidate)) { $resolvedFile = $candidate; $content = file_get_contents($resolvedFile); $response->json([ 'status' => 'success', 'file' => str_replace("{$storage}/", '', $resolvedFile), 'subject' => $subject, 'type' => $type, 'content' => $content ]); return; } $response->status(404)->json([ 'status' => 'error', 'message' => 'ملف الوثيقة المعتمد غير موجود أو غير متاح حالياً.' ]); } /** * Serves an approved asset from a published curriculum bundle by UUID only. * GET /api/curriculum/assets/{assetId} */ public function getPublishedAsset(Request $request, Response $response): void { $assetId = trim((string)$request->getParam('assetId', '')); try { $asset = PublishedContentService::findPublishedAsset($assetId); } catch (\Throwable $e) { error_log('Published asset lookup failed: ' . $e->getMessage()); $response->status(503)->json([ 'status' => 'unavailable', 'message' => 'فهرس الأصول المنشورة غير متاح حالياً.' ]); return; } if (!$asset) { $response->status(404)->json([ 'status' => 'error', 'message' => 'الأصل المطلوب غير منشور أو غير متاح لك.' ]); return; } $path = PublishedContentService::readLocalAsset($asset); if (!$path) { $response->status(404)->json([ 'status' => 'error', 'message' => 'النسخة المنشورة من الأصل غير متاحة في التخزين.' ]); return; } if (str_starts_with((string)$asset['mime_type'], 'text/')) { $content = file_get_contents($path); if ($content === false) { $response->status(503)->json(['status' => 'unavailable', 'message' => 'تعذر قراءة الأصل المنشور.']); return; } $response->json([ 'status' => 'success', 'asset' => self::assetMetadata($asset), 'content' => $content, ]); return; } $response->setHeader('Content-Type', (string)$asset['mime_type']); $response->setHeader('Content-Length', (string)$asset['byte_size']); $response->setHeader('Content-Disposition', 'inline; filename="' . basename($path) . '"'); $response->sendHeaders(); readfile($path); exit; } private static function assetMetadata(array $asset): array { return [ 'asset_id' => $asset['uuid'], 'asset_type' => $asset['asset_type'], 'mime_type' => $asset['mime_type'], 'byte_size' => (int)$asset['byte_size'], 'sha256' => $asset['sha256'], 'bundle_version' => $asset['bundle_version'], 'curriculum_lesson_id' => $asset['curriculum_lesson_uuid'], 'identity' => [ 'grade' => $asset['grade_key'], 'subject' => $asset['subject_key'], 'semester' => $asset['semester_key'], 'unit' => $asset['unit_key'], 'lesson' => $asset['lesson_key'], ], ]; } /** * استرجاع محتوى المختبر التفاعلي الذكي (أمثلة الكتاب الوزاري + 10 تدريبات ذكاء اصطناعي) * يقرأ مباشرة من الملف المخبوز المسبق أو قاعدة البيانات بأقصى سرعة * GET /api/curriculum/interactive-lab */ public function getInteractiveLab(Request $request, Response $response): void { $file = (string) $request->getQuery('file', ''); $subject = (string) $request->getQuery('subject', 'arabic_10'); $topic = (string) $request->getQuery('topic', 'inna_and_sisters'); // 1. Check if companion file exists if (!empty($file)) { $cachedLab = CurriculumService::getLessonLabData($file); if (!empty($cachedLab)) { $response->json([ 'status' => 'success', 'source' => 'prebaked_companion_file', 'data' => $cachedLab ]); return; } } // 2. Otherwise serve and bake on-the-fly $data = \App\Services\AiHumanitiesLabGeneratorService::getLabContent($subject, $topic); if (!empty($file)) { CurriculumService::saveLessonLabData($file, $data); } $response->json([ 'status' => 'success', 'source' => 'generated_and_cached', 'data' => $data ]); } /** * بناء وتجهيز أصول المختبر التفاعلي مسبقاً وحفظها في المنهاج وقاعدة البيانات بنقرة واحدة من الاستوديو * POST /api/curriculum/bake-lab */ public function bakeInteractiveLab(Request $request, Response $response): void { $body = $request->getBody(); $file = $body['file'] ?? ''; $subject = $body['subject'] ?? 'arabic_10'; $topic = $body['topic'] ?? basename($file, '.md'); if (empty($file)) { $response->status(400)->json(['status' => 'error', 'message' => 'مسار ملف الدرس مطلوب']); return; } $content = CurriculumService::getLessonMarkdown($file); $labData = \App\Services\AiHumanitiesLabGeneratorService::bakeLessonLab($file, $subject, $topic, $content); $response->json([ 'status' => 'success', 'message' => 'تم بناء وتثبيت المختبر التفاعلي كأصل ثابت في ملفات المنهاج وقاعدة البيانات بنجاح', 'file' => $file, 'lab_data' => $labData ]); } /** * استرجاع شجرة العائلات والقطاعات المهنية لـ 140 تخصصاً لمؤسسة التدريب المهني (VTC) * GET /api/vocational/tree */ public function getVocationalTree(Request $request, Response $response): void { $taxonomy = \App\Services\VocationalCurriculumService::getVocationalTaxonomy(); $response->json([ 'status' => 'success', 'data' => $taxonomy ]); } /** * استرجاع تفاصيل التخصص المهني والوحدات النمطية وبطاقات التمارين * GET /api/vocational/specialization */ public function getVocationalSpecialization(Request $request, Response $response): void { $details = \App\Services\VocationalCurriculumService::getPilotSpecializationDetails(); $response->json([ 'status' => 'success', 'data' => $details ]); } /** * استرجاع بيانات المحاكي والمختبر الافتراضي للتخصص المهني * GET /api/vocational/simulation */ public function getVocationalSimulation(Request $request, Response $response): void { $simKey = $request->getQueryParams()['key'] ?? 'ev_diagnostic_lab'; $meta = \App\Services\VocationalCurriculumService::getSimulationMetadata($simKey); if (!$meta) { $response->status(404)->json([ 'status' => 'error', 'message' => 'المحاكي المطلوب غير متوفر' ]); return; } $response->json([ 'status' => 'success', 'data' => $meta ]); } }