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
+154 -69
View File
@@ -389,7 +389,7 @@ class TeacherController
*/
public function getMyReputation(Request $request, Response $response): void
{
$teacherId = $request->user_id ?? 1;
$teacherId = (int)$request->user_id;
$metrics = \App\Services\TeacherRatingService::recalculateTeacherMetrics($teacherId);
$response->json([
@@ -404,25 +404,66 @@ class TeacherController
*/
public function getMonetizationDashboard(Request $request, Response $response): void
{
$teacherId = (int)($request->user_id ?? 1);
$teacherId = (int)$request->user_id;
// Dual Pricing Model:
// 1. Institutional Students (Military Culture + Partner Private Schools): ZERO fees (0.00 JOD)
// 2. Marketplace / External Students: Paid via CliQ (e.g. 20.00 JOD) -> 55% Teacher, 15% Directorate, 30% Saqel
$institutionalStudentsCount = 1420; // 0.00 JOD (Covered by institutional agreement)
$externalPaidStudentsCount = 380; // Paying via CliQ
$pricePerCourse = 20.0; // JOD per semester course
$grossRevenue = $externalPaidStudentsCount * $pricePerCourse; // 7,600 JOD
// 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;
}
$teacherShare = round($grossRevenue * 0.55, 2); // 4,180 JOD (55%)
$directorateShare = round($grossRevenue * 0.15, 2); // 1,140 JOD (15%)
$platformShare = round($grossRevenue * 0.30, 2); // 2,280 JOD (30%)
// 1. Real Student Counts from Database
$totalStudents = (int)(Database::selectOne("SELECT COUNT(*) as c FROM students")['c'] ?? 0);
$availableBalance = round($teacherShare * 0.75, 2); // Ready for CliQ payout
$pendingClearance = round($teacherShare * 0.25, 2);
// 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);
// Fetch teacher's actual CliQ payout queue
// 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',
@@ -430,51 +471,35 @@ class TeacherController
'model_name' => 'نموذج الشراكة المزدوج والتقاسم الثلاثي — صَقِل',
'audience_breakdown' => [
'institutional_students' => [
'count' => $institutionalStudentsCount,
'count' => $institutionalCount,
'tuition_fee_jod' => 0.00,
'status' => 'مجاني بالكامل (مشمول ضمن اتفاقية مديرية الثقافة العسكرية والمدارس الشريكة) 🎖️',
'status' => 'مجاني بالكامل (مشمول ضمن المدارس الشريكة ورعاية المنظومة) 🎖️',
],
'marketplace_students' => [
'count' => $externalPaidStudentsCount,
'count' => $paidSubscribers,
'tuition_fee_jod' => $pricePerCourse,
'payment_channel' => 'CliQ (نظام كليك الفوري)',
'status' => 'طلبة مستقلون من خارج المدارس الشريكة',
'status' => 'طلبة مشتركون عبر نظام كليك',
],
],
'course_price_jod' => $pricePerCourse,
'gross_revenue_jod' => $grossRevenue,
'revenue_split' => [
'teacher_percent' => 55,
'teacher_amount_jod' => $teacherShare,
'directorate_percent'=> 15,
'teacher_percent' => 55,
'teacher_amount_jod' => $teacherShare,
'directorate_percent' => 15,
'directorate_amount_jod' => $directorateShare,
'platform_percent' => 30,
'platform_amount_jod'=> $platformShare,
'platform_percent' => 30,
'platform_amount_jod' => $platformShare,
],
'wallet' => [
'available_balance_jod' => $availableBalance,
'pending_clearance_jod' => $pendingClearance,
'total_withdrawn_jod' => 8450.00,
'cliq_payout_alias' => 'AHMAD@CLIQ',
'iban' => 'JO94 ARAB 1234 5678 9012 3456',
'pending_clearance_jod' => 0.0,
'total_withdrawn_jod' => $totalWithdrawn,
'cliq_payout_alias' => $payoutAlias,
'recent_payouts' => $payouts,
],
'courses' => [
[
'course_title' => 'الفيزياء للتوجيهي العلمي (الفصل الأول)',
'subscribers' => 240,
'revenue_jod' => 4800,
'teacher_net_jod'=> 2640,
'status' => 'active_selling',
],
[
'course_title' => 'المكثف الشامل لقوانين نيوتن وحفظ الطاقة',
'subscribers' => 140,
'revenue_jod' => 2800,
'teacher_net_jod'=> 1540,
'status' => 'active_selling',
],
]
'courses' => $courses
]
]);
}
@@ -488,26 +513,19 @@ class TeacherController
$body = $request->getBody();
$title = $body['lesson_title'] ?? 'حصة أستوديو جديدة';
$subject = $body['subject'] ?? 'الفيزياء';
$durationMinutes = (float)($body['duration_minutes'] ?? 22.0);
$durationMinutes = (float)($body['duration_minutes'] ?? 15.0);
// 1. Cognitive Focus Duration Gate Check (20 - 25 min max MIT standard)
$durationValid = $durationMinutes >= 15.0 && $durationMinutes <= 25.0;
// 1. Cognitive Focus Duration Gate Check (Up to 25 min max)
$durationValid = $durationMinutes <= 25.0;
$durationWarning = null;
if ($durationMinutes > 25.0) {
$durationWarning = 'تنبيه إدراكي: مدة الحصة تتجاوز 25 دقيقة. أثبتت أبحاث معهد ماساتشوستس أن التركيز الذهني يهبط بعد الدقيقة 18. يجب تقسيم الحصة إلى جزأين أو تضمين فواصل سقراطية إجبارية كل 7 دقائق.';
} elseif ($durationMinutes < 15.0) {
$durationWarning = 'تنبيه تربوي: مدة الحصة أقل من 15 دقيقة، تأكد من استيفاء كافة النتاجات الوزارية للدرس.';
$durationWarning = 'تنبيه إدراكي: مدة الحصة تتجاوز 25 دقيقة. يُفضل تقسيم الحصة أو اختصارها لضمان أعلى استيعاب للطلبة.';
}
// 2. Video Technical & Pedagogical Quality Assessment
$audioClarityScore = 96; // 96%
$videoBitrateScore = 94; // 1080p crisp bitrate
$curriculumAlignmentScore = 97; // Matched outcomes
$watermarkInjected = true; // Dynamic forensic watermark
$simulatedScore = round(($audioClarityScore + $videoBitrateScore + $curriculumAlignmentScore) / 3, 1);
$isApproved = $simulatedScore >= 85 && $durationMinutes <= 25.0;
// 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',
@@ -517,19 +535,86 @@ class TeacherController
'duration_minutes' => $durationMinutes,
'duration_gate_passed' => $durationValid,
'duration_warning' => $durationWarning,
'quality_score' => $simulatedScore,
'approval_status' => $isApproved ? 'approved_for_broadcast' : 'needs_revision',
'threshold_required' => 85,
'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 دقيقة)' : 'مرفوض (يتجاوز 25 دقيقة)',
'curriculum_alignment' => "{$curriculumAlignmentScore}% تطابق مع مخرجات المنهاج الوزاري",
'audio_clarity' => "{$audioClarityScore}% نقاء صوتي ممتاز مع عزل الضوضاء",
'forensic_watermark' => $watermarkInjected ? 'تم دمج العلامة المائية للرقم الوطني والاسم ديناميكياً' : 'مفقودة',
'socratic_stops_count' => 3,
'duration_mit_gate' => $durationMinutes <= 25.0 ? 'مقبول ومثالي للتركيز الإدراكي' : 'مرفوض (يتجاوز 25 دقيقة)',
'curriculum_alignment' => 'بانتظار تحليل الملف الفعلي',
'audio_clarity' => 'بانتظار تحليل الملف الفعلي',
'forensic_watermark' => 'تُضاف ضمن مسار البث بعد نجاح الرفع',
'socratic_stops_count' => 0,
],
'decision' => $isApproved
? 'الحصة معتمدة ومؤهلة للبث المشفر والعرض لطلبة المنظومة والبيع للطلبة المستقلين'
: 'الحصة بحاجة لتعديل المدة أو النقاء الصوتي قبل إطلاقها على شبكة صَقِل',
'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
]
]);
}