user_id; $teacher = Database::selectOne("SELECT id, uuid, full_name, specialization, bio, is_marketplace_public, grades_taught, school_name FROM teachers WHERE id = ? LIMIT 1", [$teacherId]); if (!$teacher) { $response->status(401)->json([ 'status' => 'error', 'message' => 'حساب المعلم غير مسجل أو يتطلب تسجيل الدخول', 'is_completed' => false ]); return; } $isCompleted = !empty($teacher['full_name']) && $teacher['full_name'] !== 'معلم جديد' && !empty($teacher['specialization']) && $teacher['specialization'] !== 'بانتظار تحديد التخصص'; $gradesList = ['الصف العاشر الأساسي', 'الأول ثانوي العلمي', 'الثاني ثانوي (التوجيهي)']; if (!empty($teacher['grades_taught'])) { $decodedGrades = json_decode($teacher['grades_taught'], true); if (is_array($decodedGrades)) { $gradesList = $decodedGrades; } } $response->json([ 'status' => 'success', 'is_completed' => $isCompleted, 'user' => [ 'uuid' => $teacher['uuid'], 'full_name' => $teacher['full_name'], 'role' => 'teacher', 'status' => 'active', ], 'profile' => [ 'specialization' => $teacher['specialization'] ?? 'الفيزياء', 'grades_taught' => $gradesList, 'school_name' => $teacher['school_name'] ?? 'مدرسة الملك عبد الله الثاني للتميز', 'bio' => $teacher['bio'] ?? 'معلم معتمد في منصة صَقِل' ] ]); } /** * Complete or Update Teacher Onboarding Profile (Password, Name, Bio, Grades, Subjects) * POST /api/teacher/profile/setup */ public function setupProfile(Request $request, Response $response): void { self::ensureTeacherColumns(); $teacherId = (int)$request->user_id; $body = $request->getBody(); $validator = new Validator(); $isValid = $validator->validate($body, [ 'full_name' => 'required', 'specialization' => 'required', ]); if (!$isValid) { $response->status(400)->json([ 'status' => 'error', 'message' => 'بيانات الملف الشخصي غير مكتملة', 'errors' => $validator->getErrors() ]); return; } $fullName = trim((string)$body['full_name']); $specialization = trim((string)$body['specialization']); $bio = trim((string)($body['bio'] ?? '')); $schoolName = trim((string)($body['school_name'] ?? '')); $gradesTaught = $body['grades_taught'] ?? ['الصف العاشر الأساسي', 'الأول ثانوي العلمي']; $gradesJson = is_array($gradesTaught) ? json_encode($gradesTaught, JSON_UNESCAPED_UNICODE) : (string)$gradesTaught; // Update Teachers Table Directly Database::query( "UPDATE teachers SET full_name = ?, specialization = ?, bio = ?, school_name = ?, grades_taught = ?, updated_at = NOW() WHERE id = ?", [$fullName, $specialization, $bio, $schoolName, $gradesJson, $teacherId] ); $teacher = Database::selectOne("SELECT * FROM teachers WHERE id = ? LIMIT 1", [$teacherId]); $metrics = Database::selectOne('SELECT weighted_student_rating, total_reviews_count FROM teacher_performance_metrics WHERE teacher_id = ? LIMIT 1', [$userId]) ?: []; $response->json([ 'status' => 'success', 'message' => 'تم توثيق بيانات المعلم والصفوف والمباحث بنجاح!', 'data' => [ 'full_name' => $teacher['full_name'] ?? $fullName, 'specialization' => $teacher['specialization'] ?? $specialization, 'school_name' => $teacher['school_name'] ?? $schoolName, 'grades_taught' => is_array($gradesTaught) ? $gradesTaught : json_decode($gradesJson, true), 'bio' => $teacher['bio'] ?? $bio ] ]); } public function getDashboard(Request $request, Response $response): void { $userId = $request->user_id; // Total Courses $coursesCount = (int)Database::selectOne("SELECT COUNT(*) as total FROM courses WHERE teacher_id = ?", [$userId])['total']; // Total Lessons $lessonsCount = (int)Database::selectOne( "SELECT COUNT(l.id) as total FROM lessons l JOIN courses c ON l.course_id = c.id WHERE c.teacher_id = ?", [$userId] )['total']; // Total Unique Active Students $studentsCount = (int)Database::selectOne( "SELECT COUNT(DISTINCT lp.student_id) as total FROM lesson_progress lp JOIN lessons l ON lp.lesson_id = l.id JOIN courses c ON l.course_id = c.id WHERE c.teacher_id = ?", [$userId] )['total']; $metrics = Database::selectOne( 'SELECT weighted_student_rating, total_reviews_count FROM teacher_performance_metrics WHERE teacher_id = ? LIMIT 1', [$userId] ) ?: []; $response->json([ 'status' => 'success', 'data' => [ 'courses_count' => $coursesCount, 'lessons_count' => $lessonsCount, 'students_count' => $studentsCount, 'rating' => isset($metrics['weighted_student_rating']) ? (float)$metrics['weighted_student_rating'] : null, 'rating_count' => (int)($metrics['total_reviews_count'] ?? 0), 'completion_avg' => null ] ]); } /** * List Teacher's Courses * GET /api/teacher/courses */ public function getCourses(Request $request, Response $response): void { CurriculumService::ensureSchema(); $userId = (int)$request->user_id; $courses = Database::select( "SELECT c.id, c.uuid, c.title, c.description, c.semester, c.price_jod, c.is_published, c.created_at, COUNT(l.id) as lessons_total FROM courses c LEFT JOIN lessons l ON l.course_id = c.id WHERE c.teacher_id = ? GROUP BY c.id ORDER BY c.id ASC", [$userId] ); if (empty($courses)) { // Auto-seed default courses for this teacher $uuid1 = 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)); $uuid2 = 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)); Database::query( "INSERT INTO courses (uuid, teacher_id, subject_id, title, description, semester, price_jod, is_published) VALUES (?, ?, 1, 'الرياضيات العلمي — توجيهي 2008 (المستوى الثالث)', 'شرح المنهاج الوزاري الجديد مع التحليل الجنائي وتطبيقات التفاضل', 'first', 35.00, 1), (?, ?, 2, 'الثقافة العسكرية والتربية الوطنية (المستوى الموحد)', 'منهاج مدارس الثقافة العسكرية المعتمد مع بنك الأسئلة والخرائط الذهنية', 'first', 25.00, 1)", [$uuid1, $userId, $uuid2, $userId] ); $courses = Database::select( "SELECT c.id, c.uuid, c.title, c.description, c.semester, c.price_jod, c.is_published, c.created_at, COUNT(l.id) as lessons_total FROM courses c LEFT JOIN lessons l ON l.course_id = c.id WHERE c.teacher_id = ? GROUP BY c.id ORDER BY c.id ASC", [$userId] ); } $response->json([ 'status' => 'success', 'data' => $courses ]); } /** * Add New Course * POST /api/teacher/courses */ public function addCourse(Request $request, Response $response): void { $body = $request->getBody(); $title = trim((string)($body['title'] ?? '')); $description = trim((string)($body['description'] ?? '')); $subjectId = (int)($body['subject_id'] ?? 1); $semester = (string)($body['semester'] ?? 'first'); $price = (float)($body['price_jod'] ?? 35.00); if (empty($title)) { $response->status(400)->json([ 'status' => 'error', 'message' => 'عنوان الدورة مطلوب' ]); return; } $uuid = 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) ); $courseId = Database::insert( "INSERT INTO courses (uuid, subject_id, teacher_id, title, description, semester, price_jod, is_published) VALUES (?, ?, ?, ?, ?, ?, ?, 1)", [$uuid, $subjectId, $request->user_id, $title, $description, $semester, $price] ); $response->status(201)->json([ 'status' => 'success', 'message' => 'تم إنشاء الدورة بنجاح', 'data' => [ 'course_id' => $courseId, 'uuid' => $uuid ] ]); } /** * Add New Lesson to Course * POST /api/teacher/lessons */ public function addLesson(Request $request, Response $response): void { $body = $request->getBody(); $courseId = (int)($body['course_id'] ?? 0); $title = trim((string)($body['title'] ?? '')); $bunnyVideoId = trim((string)($body['bunny_video_id'] ?? '')); $duration = (int)($body['duration_seconds'] ?? 0); $sequenceOrder = (int)($body['sequence_order'] ?? 1); if (!$courseId || empty($title) || empty($bunnyVideoId)) { $response->status(400)->json([ 'status' => 'error', 'message' => 'معرف الدورة، عنوان الدرس، ومعرف فيديو Bunny مطلوبين' ]); return; } // Verify Course Ownership $course = Database::selectOne("SELECT id, teacher_id FROM courses WHERE id = ? LIMIT 1", [$courseId]); if (!$course || ($course['teacher_id'] != $request->user_id && $request->role !== 'super_admin')) { $response->status(403)->json([ 'status' => 'error', 'message' => 'غير مصرح: لا تملك هذه الدورة' ]); return; } $lessonId = Database::insert( "INSERT INTO lessons (course_id, title, sequence_order, bunny_video_id, duration_seconds, is_free_preview) VALUES (?, ?, ?, ?, ?, 0)", [$courseId, $title, $sequenceOrder, $bunnyVideoId, $duration] ); $response->status(201)->json([ 'status' => 'success', 'message' => 'تمت إضافة الدرس بنجاح', 'data' => [ 'lesson_id' => $lessonId ] ]); } /** * Get All Teachers for Marketplace Discovery * GET /api/teachers */ public function getMarketplaceTeachers(Request $request, Response $response): void { $queryParams = $request->getQueryParams(); $sortBy = $queryParams['sort'] ?? 'merit'; $teachers = \App\Services\TeacherRatingService::getTeachersMarketplace($sortBy); $response->json([ 'status' => 'success', 'data' => $teachers ]); } /** * Get Teacher Metrics Breakdown & Live Reputation * GET /api/teachers/{id}/metrics */ public function getTeacherMetrics(Request $request, Response $response): void { $teacherId = (int)$request->getParam('id'); if (!$teacherId) { $response->status(400)->json(['status' => 'error', 'message' => 'معرف المعلم غير صالح']); return; } $metrics = \App\Services\TeacherRatingService::recalculateTeacherMetrics($teacherId); $reviews = Database::select( "SELECT tr.*, u.full_name as student_name FROM teacher_reviews tr JOIN students u ON tr.student_id = u.id WHERE tr.teacher_id = ? AND tr.is_flagged_anomaly = 0 ORDER BY tr.created_at DESC LIMIT 20", [$teacherId] ); $response->json([ 'status' => 'success', 'data' => [ 'metrics' => $metrics, 'reviews' => $reviews ] ]); } /** * Submit Student Review with Anti-Brigade & Engagement Weighting * POST /api/teachers/{id}/reviews */ public function submitReview(Request $request, Response $response): void { $teacherId = (int)$request->getParam('id'); $studentId = $request->user_id; $body = $request->getBody(); $ratingOverall = (float)($body['rating_overall'] ?? 5.0); $clarity = (int)($body['rating_clarity'] ?? 5); $speed = (int)($body['rating_response_speed'] ?? 5); $socratic = (int)($body['rating_socratic_interaction'] ?? 5); $text = trim((string)($body['review_text'] ?? '')); $courseId = !empty($body['course_id']) ? (int)$body['course_id'] : null; $lessonId = !empty($body['lesson_id']) ? (int)$body['lesson_id'] : null; if (!$teacherId || !$studentId) { $response->status(400)->json(['status' => 'error', 'message' => 'معرف المعلم والطالب مطلوبان']); return; } $res = \App\Services\TeacherRatingService::submitReview( $teacherId, $studentId, $ratingOverall, $clarity, $speed, $socratic, $text, $courseId, $lessonId ); $response->json([ 'status' => 'success', 'message' => 'تم تسجيل تقييمك واحتساب وزنه المعرفي بنجاح', 'data' => $res ]); } /** * Get Logged-in Teacher's own reputation score * GET /api/teacher/reputation */ public function getMyReputation(Request $request, Response $response): void { $teacherId = (int)$request->user_id; $metrics = \App\Services\TeacherRatingService::recalculateTeacherMetrics($teacherId); $response->json([ 'status' => 'success', 'data' => $metrics ]); } /** * لوحة تسييل الحصص والشراكة المالية للمعلم (الفصل السابع في العرض) * GET /api/teacher/monetization */ public function getMonetizationDashboard(Request $request, Response $response): void { $teacherId = (int)$request->user_id; // Fetch real teacher data $teacher = Database::selectOne("SELECT * FROM teachers WHERE id = ? LIMIT 1", [$teacherId]); if (!$teacher) { $response->status(404)->json(['status' => 'error', 'message' => 'ملف المعلم غير موجود']); return; } // Revenue sharing is being rebuilt around provider-backed ledger entries // and eligible watch minutes. Do not turn course prices, platform sales, // or legacy payout rows into a teacher wallet in the meantime. $courses = Database::select( "SELECT c.id, c.title as course_title, c.price_jod, c.is_published, COUNT(l.id) as lessons_total FROM courses c LEFT JOIN lessons l ON l.course_id = c.id WHERE c.teacher_id = ? GROUP BY c.id ORDER BY c.id ASC", [$teacherId] ); $response->json([ 'status' => 'success', 'data' => [ 'financial_status' => 'unavailable', 'financial_message' => 'قيد التفعيل المؤسسي — بانتظار اعتماد سياسة كليك والربط البنكي الرسمي ومطابقة الدقائق التعليمية المستحقة.', 'audience_breakdown' => [ 'institutional_students' => ['count' => 0], 'marketplace_students' => ['count' => 0], ], 'course_price_jod' => null, 'gross_revenue_jod' => null, 'revenue_split' => null, 'wallet' => [ 'available_balance_jod' => null, 'pending_clearance_jod' => null, 'total_withdrawn_jod' => null, 'cliq_payout_alias' => '', 'recent_payouts' => [], ], 'courses' => $courses, ], ]); return; // 1. Real Student Counts from Database $totalStudents = (int)(Database::selectOne("SELECT COUNT(*) as c FROM students")['c'] ?? 0); // Count verified paid receipts $paidRow = Database::selectOne( "SELECT COUNT(DISTINCT student_id) as paid_count, COALESCE(SUM(amount_jod), 0) as total_revenue FROM cliq_payments WHERE verification_status = 'verified'" ); $paidSubscribers = (int)($paidRow['paid_count'] ?? 0); $grossRevenue = (float)($paidRow['total_revenue'] ?? 0.0); // Institutional students are enrolled school students (0.00 JOD) $institutionalCount = max(0, $totalStudents - $paidSubscribers); $priceRow = Database::selectOne( "SELECT COALESCE(AVG(price_jod), 0) AS average_price FROM courses WHERE teacher_id = ?", [$teacherId] ); $pricePerCourse = (float)($priceRow['average_price'] ?? 0.0); if ($grossRevenue <= 0 && $paidSubscribers > 0) { $grossRevenue = $paidSubscribers * $pricePerCourse; } // Tripartite Revenue Split (55% Teacher / 15% Directorate / 30% Saqel) $teacherShare = round($grossRevenue * 0.55, 2); $directorateShare = round($grossRevenue * 0.15, 2); $platformShare = round($grossRevenue * 0.30, 2); // Fetch actual payouts from payout_queue $payouts = \App\Services\CliqPaymentService::getTeacherPayouts($teacherId); $totalWithdrawn = 0.0; foreach ($payouts as $p) { if ($p['status'] === 'completed') { $totalWithdrawn += (float)$p['amount_jod']; } } $availableBalance = max(0.0, round($teacherShare - $totalWithdrawn, 2)); // Fetch real courses $courses = Database::select( "SELECT c.id, c.title as course_title, c.price_jod, c.is_published, COUNT(l.id) as lessons_total FROM courses c LEFT JOIN lessons l ON l.course_id = c.id WHERE c.teacher_id = ? GROUP BY c.id ORDER BY c.id ASC", [$teacherId] ); $payoutAlias = !empty($payouts) ? (string)($payouts[0]['teacher_cliq_alias'] ?? '') : ''; $response->json([ 'status' => 'success', 'data' => [ 'model_name' => 'نموذج الشراكة المزدوج والتقاسم الثلاثي — صَقِل', 'audience_breakdown' => [ 'institutional_students' => [ 'count' => $institutionalCount, 'tuition_fee_jod' => 0.00, 'status' => 'مجاني بالكامل (مشمول ضمن المدارس الشريكة ورعاية المنظومة) 🎖️', ], 'marketplace_students' => [ 'count' => $paidSubscribers, 'tuition_fee_jod' => $pricePerCourse, 'payment_channel' => 'CliQ (نظام كليك الفوري)', 'status' => 'طلبة مشتركون عبر نظام كليك', ], ], 'course_price_jod' => $pricePerCourse, 'gross_revenue_jod' => $grossRevenue, 'revenue_split' => [ 'teacher_percent' => 55, 'teacher_amount_jod' => $teacherShare, 'directorate_percent' => 15, 'directorate_amount_jod' => $directorateShare, 'platform_percent' => 30, 'platform_amount_jod' => $platformShare, ], 'wallet' => [ 'available_balance_jod' => $availableBalance, 'pending_clearance_jod' => 0.0, 'total_withdrawn_jod' => $totalWithdrawn, 'cliq_payout_alias' => $payoutAlias, 'recent_payouts' => $payouts, ], 'courses' => $courses ] ]); } /** * بوابة تدقيق جودة حصص الأستوديو وضوابط التركيز الإدراكي وفحص الفيديو * POST /api/teacher/audit-studio-video */ public function auditStudioVideo(Request $request, Response $response): void { $body = $request->getBody(); $title = $body['lesson_title'] ?? 'حصة أستوديو جديدة'; $subject = $body['subject'] ?? 'الفيزياء'; $durationMinutes = (float)($body['duration_minutes'] ?? 15.0); // 1. Cognitive Focus Duration Gate Check (Up to 25 min max) $durationValid = $durationMinutes <= 25.0; $durationWarning = null; if ($durationMinutes > 25.0) { $durationWarning = 'تنبيه إدراكي: مدة الحصة تتجاوز 25 دقيقة. يُفضل تقسيم الحصة أو اختصارها لضمان أعلى استيعاب للطلبة.'; } // This endpoint is only a metadata preflight. Media quality is measured // after the actual multipart upload by AiVideoAnalyzerService. $preflightScore = $durationValid ? 100 : 0; $response->json([ 'status' => 'success', 'data' => [ 'lesson_title' => $title, 'subject' => $subject, 'duration_minutes' => $durationMinutes, 'duration_gate_passed' => $durationValid, 'duration_warning' => $durationWarning, 'quality_score' => $preflightScore, 'approval_status' => $durationValid ? 'ready_for_upload' : 'needs_revision', 'threshold_required' => 100, 'curriculum_alignment' => 'يُقاس بعد رفع الفيديو وتحليله على الخادم', 'audio_clarity' => 'يُقاس من الملف الفعلي بعد الرفع', 'socratic_stops_count' => 0, 'audit_checkpoints' => [ 'duration_mit_gate' => $durationMinutes <= 25.0 ? 'مقبول ومثالي للتركيز الإدراكي' : 'مرفوض (يتجاوز 25 دقيقة)', 'curriculum_alignment' => 'بانتظار تحليل الملف الفعلي', 'audio_clarity' => 'بانتظار تحليل الملف الفعلي', 'forensic_watermark' => 'تُضاف ضمن مسار البث بعد نجاح الرفع', 'socratic_stops_count' => 0, ], 'decision' => $durationValid ? 'نجح فحص البيانات الأولي. ارفع الملف لبدء تحليل الجودة والتقطيع إلى HLS.' : 'الحصة بحاجة لاختصار المدة لأقل من 25 دقيقة قبل رفعها.', ] ]); } /** * رفع واعتماد الحصة من استوديو المعلم * POST /api/teacher/lessons/upload */ public function uploadLesson(Request $request, Response $response): void { $response->status(410)->json([ 'status' => 'error', 'message' => 'استخدم مسار الرفع الفعلي multipart: /api/teacher/videos/upload-direct', ]); } /** * استعلام أسئلة واستفسارات الطلبة الفعلية للمعلم * GET /api/teacher/qna */ public function getQnA(Request $request, Response $response): void { $teacherId = (int)$request->user_id; // Fetch real student doubts and inquiries from chat_messages $messages = Database::select( "SELECT cm.id, cm.uuid, cm.message, cm.message_type, cm.created_at, cm.is_read, s.identity_id as student_id, s.full_name as student_name, s.grade_level FROM chat_messages cm JOIN students s ON cm.sender_identity_id = s.identity_id WHERE cm.receiver_identity_id = ? ORDER BY cm.id DESC LIMIT 20", [$request->identity_id] ); $doubts = []; if (!empty($messages)) { foreach ($messages as $msg) { $reply = Database::selectOne( "SELECT id, message_type FROM chat_messages WHERE sender_identity_id = ? AND receiver_identity_id = ? AND id > ? LIMIT 1", [$request->identity_id, $msg['student_id'], $msg['id']] ); $doubts[] = [ 'id' => 'doubt-' . $msg['id'], 'student_id' => (int)$msg['student_id'], 'student_name' => $msg['student_name'] ?: '', 'class_name' => $msg['grade_level'] ?: '', 'question' => $msg['message'], 'time' => $msg['created_at'], 'replied' => !empty($reply), 'voice_reply' => !empty($reply) && ($reply['message_type'] === 'voice'), ]; } } // Assignments are returned only when the assignments module persists them. $homeworks = []; $response->json([ 'status' => 'success', 'data' => [ 'doubts' => $doubts, 'homeworks' => $homeworks ] ]); } /** * Broadcast an announcement or voice note to all students in a class or course via WebSocket * POST /api/teacher/broadcast */ public function broadcastToClass(Request $request, Response $response): void { $teacherId = (int)$request->user_id; $body = $request->getBody(); $teacher = Database::selectOne("SELECT full_name, specialization FROM teachers WHERE id = ? LIMIT 1", [$teacherId]); $teacherName = $teacher['full_name'] ?? 'الأستاذ المعتمد'; $title = trim((string)($body['title'] ?? 'إشعار توجيهي من المعلم 🎙️')); $message = trim((string)($body['message'] ?? '')); $messageType = (string)($body['message_type'] ?? 'text'); // text or voice $mediaUrl = !empty($body['media_url']) ? trim((string)$body['media_url']) : null; $courseId = !empty($body['course_id']) ? (int)$body['course_id'] : 1; $gradeLevel = trim((string)($body['grade_level'] ?? 'الصف العاشر الأساسي')); $broadcastPayload = [ 'broadcast_id' => 'bc-' . time(), 'sender_id' => $teacherId, 'sender_name' => $teacherName, 'title' => $title, 'message' => $message, 'message_type' => $messageType, 'media_url' => $mediaUrl, 'course_id' => $courseId, 'grade_level' => $gradeLevel, 'created_at' => date('Y-m-d H:i:s'), ]; // 1. Broadcast via Workerman WebSocket on course/public channel ChatController::pushToWorkerman('broadcast_course', 0, $broadcastPayload); // 2. Also register in chat messages table for all students in grade level try { $students = Database::select("SELECT id FROM students WHERE grade_level = ? LIMIT 50", [$gradeLevel]); if (empty($students)) { $students = Database::select("SELECT id FROM students LIMIT 10"); } foreach ($students as $std) { $uuid = 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)); Database::insert( "INSERT INTO chat_messages (uuid, sender_id, receiver_id, course_id, message, message_type, media_url, is_read, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, 0, NOW())", [$uuid, $teacherId, $std['id'], $courseId, $message, $messageType, $mediaUrl] ); } } catch (\Throwable $e) {} $response->status(200)->json([ 'status' => 'success', 'message' => 'تم بث الإشعار الصوتي والتوجيهي لجميع طلبة الشعبة عبر شبكة الويب سوكت الحية بنجاح 📡', 'data' => $broadcastPayload ]); } }