Update Saqel Platform: 2026-08-28 18:21:58

This commit is contained in:
Hamza-Ayed
2026-08-28 18:21:58 +03:00
parent 2c529c579c
commit abe862f04d
6 changed files with 486 additions and 164 deletions
+158 -12
View File
@@ -359,37 +359,183 @@ class AuthController
]);
}
/**
/**
* Get Current Authenticated User Data
* GET /api/auth/me
*/
public function me(Request $request, Response $response): void
{
$userId = $request->user_id;
$userId = (int)$request->user_id;
$role = $request->role ?? 'student';
$user = Database::selectOne(
"SELECT uuid, full_name, phone_number, role, grade_level, stream, status, created_at FROM users WHERE id = ? LIMIT 1",
[$userId]
);
$userData = null;
if (!$user) {
if ($role === 'teacher') {
$user = Database::selectOne(
"SELECT t.id, t.uuid, t.full_name, t.specialization, t.bio, t.school_id, ai.phone_number, ai.status, t.created_at
FROM teachers t
JOIN auth_identities ai ON t.identity_id = ai.id
WHERE t.id = ? LIMIT 1",
[$userId]
);
if ($user) {
$isCompleted = !empty($user['full_name']) && $user['full_name'] !== 'معلم جديد' && !empty($user['specialization']) && $user['specialization'] !== 'بانتظار تحديد التخصص';
$userData = [
'id' => $user['id'],
'uuid' => $user['uuid'],
'full_name' => $user['full_name'],
'name' => $user['full_name'],
'role' => 'teacher',
'specialization' => $user['specialization'],
'bio' => $user['bio'],
'phone' => Security::decrypt($user['phone_number']),
'status' => $user['status'],
'is_completed' => $isCompleted,
'is_teacher' => true,
'is_student' => false,
];
}
} elseif ($role === 'guardian') {
$user = Database::selectOne(
"SELECT g.id, g.uuid, g.full_name, g.national_id, ai.phone_number, ai.status, g.created_at
FROM guardians g
JOIN auth_identities ai ON g.identity_id = ai.id
WHERE g.id = ? LIMIT 1",
[$userId]
);
if ($user) {
$userData = [
'id' => $user['id'],
'uuid' => $user['uuid'],
'full_name' => $user['full_name'],
'name' => $user['full_name'],
'role' => 'guardian',
'phone' => Security::decrypt($user['phone_number']),
'status' => $user['status'],
'is_completed' => true,
'is_teacher' => false,
'is_student' => false,
];
}
} else {
// Student
$user = Database::selectOne(
"SELECT s.id, s.uuid, s.full_name, s.national_id, s.grade_level, s.stream, s.readiness_score, s.school_id, ai.phone_number, ai.status, s.created_at
FROM students s
JOIN auth_identities ai ON s.identity_id = ai.id
WHERE s.id = ? LIMIT 1",
[$userId]
);
if ($user) {
$isCompleted = !empty($user['full_name']) && $user['full_name'] !== 'طالب جديد' && $user['full_name'] !== 'الطالب المتميز' && !empty($user['grade_level']);
$userData = [
'id' => $user['id'],
'uuid' => $user['uuid'],
'full_name' => $user['full_name'],
'name' => $user['full_name'],
'role' => 'student',
'national_id' => $user['national_id'],
'grade_level' => $user['grade_level'],
'stream' => $user['stream'],
'readiness_score' => $user['readiness_score'],
'phone' => Security::decrypt($user['phone_number']),
'status' => $user['status'],
'is_completed' => $isCompleted,
'is_teacher' => false,
'is_student' => true,
];
}
}
if (!$userData) {
$response->status(404)->json([
'status' => 'error',
'message' => 'المستخدم غير موجود'
'message' => 'المستخدم غير موجود أو تم إعادة تهيئة قاعدة البيانات'
]);
return;
}
$user['full_name'] = Security::decrypt($user['full_name']);
$user['phone_number'] = Security::decrypt($user['phone_number']);
$response->json([
'status' => 'success',
'data' => $user
'data' => $userData
]);
}
/**
* Check if Student Profile is complete
* GET /api/student/profile/status
*/
public function studentProfileStatus(Request $request, Response $response): void
{
$studentId = (int)$request->user_id;
$student = Database::selectOne(
"SELECT s.*, ai.phone_number FROM students s JOIN auth_identities ai ON s.identity_id = ai.id WHERE s.id = ? LIMIT 1",
[$studentId]
);
if (!$student) {
$response->status(401)->json(['status' => 'error', 'message' => 'طالب غير مسجل']);
return;
}
$isCompleted = !empty($student['full_name']) && $student['full_name'] !== 'طالب جديد' && $student['full_name'] !== 'الطالب المتميز' && !empty($student['grade_level']);
$response->json([
'status' => 'success',
'is_completed' => $isCompleted,
'user' => [
'id' => $student['id'],
'uuid' => $student['uuid'],
'full_name' => $student['full_name'],
'national_id' => $student['national_id'],
'grade_level' => $student['grade_level'],
'stream' => $student['stream'],
'role' => 'student'
]
]);
}
/**
* Setup Student Profile
* POST /api/student/profile/setup
*/
public function studentProfileSetup(Request $request, Response $response): void
{
$studentId = (int)$request->user_id;
$body = $request->getBody();
$fullName = trim((string)($body['full_name'] ?? ''));
$gradeLevel = trim((string)($body['grade_level'] ?? 'grade_10'));
$stream = trim((string)($body['stream'] ?? 'scientific'));
$nationalId = trim((string)($body['national_id'] ?? ''));
if (empty($fullName)) {
$response->status(400)->json(['status' => 'error', 'message' => 'الاسم الكامل مطلوب']);
return;
}
Database::query(
"UPDATE students SET full_name = ?, grade_level = ?, stream = ?, national_id = IF(? != '', ?, national_id), updated_at = NOW() WHERE id = ?",
[$fullName, $gradeLevel, $stream, $nationalId, $nationalId, $studentId]
);
$student = Database::selectOne("SELECT * FROM students WHERE id = ? LIMIT 1", [$studentId]);
$response->json([
'status' => 'success',
'message' => 'تم استكمال ملف الطالب بنجاح! مرحباً بك في منصة صَقِل',
'user' => [
'id' => $student['id'],
'uuid' => $student['uuid'],
'full_name' => $student['full_name'],
'grade_level' => $student['grade_level'],
'stream' => $student['stream'],
'role' => 'student'
]
]);
}
/**
* Logout and destroy Redis active session
* POST /api/auth/logout
*/
+10 -30
View File
@@ -53,20 +53,19 @@ class TeacherController
*/
public function setupProfile(Request $request, Response $response): void
{
$userId = $request->user_id;
$teacherId = (int)$request->user_id;
$body = $request->getBody();
$validator = new Validator();
$isValid = $validator->validate($body, [
'full_name' => 'required',
'specialization' => 'required',
'password' => 'required|min:8',
]);
if (!$isValid) {
$response->status(400)->json([
'status' => 'error',
'message' => 'بيانات الملف الشخصي غير مكتملة أو كلمة المرور قصيرة (أقل من 8 خانات)',
'message' => 'بيانات الملف الشخصي غير مكتملة',
'errors' => $validator->getErrors()
]);
return;
@@ -75,45 +74,26 @@ class TeacherController
$fullName = trim((string)$body['full_name']);
$specialization = trim((string)$body['specialization']);
$bio = trim((string)($body['bio'] ?? ''));
$gradeLevels = is_array($body['grade_levels'] ?? null) ? implode(',', $body['grade_levels']) : (string)($body['grade_levels'] ?? 'tawjihi_2008');
$passwordHash = Security::hashPassword((string)$body['password']);
// 1. Update User table
$encryptedName = Security::encrypt($fullName);
// Update Teachers Table Directly
Database::query(
"UPDATE users SET full_name = ?, password_hash = ?, grade_level = ?, status = 'active' WHERE id = ?",
[$encryptedName, $passwordHash, $gradeLevels, $userId]
"UPDATE teachers SET full_name = ?, specialization = ?, bio = ?, updated_at = NOW() WHERE id = ?",
[$fullName, $specialization, $bio, $teacherId]
);
// 2. Upsert Teacher Profile
$existingProfile = Database::selectOne("SELECT id FROM teacher_profiles WHERE user_id = ? LIMIT 1", [$userId]);
if ($existingProfile) {
Database::query(
"UPDATE teacher_profiles SET bio = ?, specialization = ? WHERE user_id = ?",
[$bio, $specialization, $userId]
);
} else {
Database::insert(
"INSERT INTO teacher_profiles (user_id, bio, specialization, revenue_share_pct, contract_type) VALUES (?, ?, ?, 50.00, 'exclusive')",
[$userId, $bio, $specialization]
);
}
$teacher = Database::selectOne("SELECT * FROM teachers WHERE id = ? LIMIT 1", [$teacherId]);
$response->json([
'status' => 'success',
'message' => 'تم توثيق بيانات المعلم وإعداد كلمة المرور بنجاح!',
'message' => 'تم توثيق بيانات المعلم واعتماد ملفك بنجاح!',
'data' => [
'full_name' => $fullName,
'specialization' => $specialization,
'grade_levels' => $gradeLevels
'full_name' => $teacher['full_name'] ?? $fullName,
'specialization' => $teacher['specialization'] ?? $specialization,
'bio' => $teacher['bio'] ?? $bio
]
]);
}
/**
* Teacher Dashboard Statistics
* GET /api/teacher/dashboard
*/
public function getDashboard(Request $request, Response $response): void
{
$userId = $request->user_id;
+177 -91
View File
@@ -32,7 +32,9 @@ class CurriculumStudio
:root {
--bg-base: #0B0F19;
--bg-card: rgba(22, 27, 34, 0.85);
--bg-hover: rgba(255, 255, 255, 0.05);
--border: rgba(255, 255, 255, 0.08);
--border-color: rgba(255, 255, 255, 0.12);
--accent-cyan: #00F5D4;
--accent-gold: #FFD166;
--accent-purple: #7B2CBF;
@@ -110,6 +112,12 @@ class CurriculumStudio
font-size: 11.5px; font-family: monospace; color: var(--accent-cyan); margin-bottom: 14px;
display: flex; align-items: center; justify-content: space-between;
}
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
</style>
</head>
<body>
@@ -121,7 +129,10 @@ class CurriculumStudio
<span style="font-weight: 800; font-size: 18px; color: #FFF;">صَقِل Enterprise</span>
<span style="font-size: 11px; font-weight: 700; color: var(--accent-gold); background: rgba(255,209,102,0.12); padding: 2px 10px; border-radius: 980px;">استوديو تفريغ وفهرسة المناهج الحية 📚</span>
</div>
<div style="display: flex; gap: 10px;">
<div style="display: flex; gap: 10px; align-items: center;">
<button type="button" onclick="openDirectLogViewer()" style="font-size: 12px; color: var(--accent-gold); background: rgba(255,209,102,0.1); border: 1px solid rgba(255,209,102,0.3); padding: 5px 14px; border-radius: 980px; cursor: pointer; font-weight: 700;">
🔍 سجل العمليات والسيرفر (Logs)
</button>
<a href="/teacher" style="font-size: 12px; color: var(--accent-cyan); text-decoration: none; border: 1px solid rgba(0,245,212,0.3); padding: 5px 14px; border-radius: 980px;">استوديو المعلم 👨‍🏫</a>
<a href="/student" style="font-size: 12px; color: var(--text-muted); text-decoration: none; border: 1px solid var(--border); padding: 5px 14px; border-radius: 980px;">بوابة الطالب 🎓</a>
</div>
@@ -134,7 +145,7 @@ class CurriculumStudio
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px;">
<div>
<h2 style="font-size: 20px; font-weight: 900; color: #FFF; margin-bottom: 4px;">رفع وتفريغ كتب المناهج الوزارية (PDF ⟵ Markdown Tree) 📑</h2>
<p style="font-size: 12.5px; color: var(--text-secondary);" id="upload_status_desc">اختر ملف كتاب المنهاج (PDF) ليقوم الخادم بتفريغه وفهرسة وحداته ودروسه الحقيقية على القرص وتوليد ملفات المارك داون.</p>
<p style="font-size: 12.5px; color: var(--text-secondary);" id="upload_status_desc">اختر ملف كتاب المنهاج (PDF) ليقوم الخادم والذكاء الاصطناعي بتفريغ الوحدات والدروس والمصادر كملفات Markdown حية.</p>
</div>
<div style="display: flex; gap: 10px; align-items: center;">
<input type="file" id="curriculum_pdf_input" accept=".pdf" style="display: none;" onchange="handleRealPdfUpload(this)">
@@ -192,14 +203,46 @@ class CurriculumStudio
</div>
</div>
<!-- Upload Progress & Live Terminal Modal -->
<div id="upload_progress_overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(10, 15, 26, 0.9); backdrop-filter: blur(10px); z-index: 9999; flex-direction: column; align-items: center; justify-content: center;">
<div style="background: #16203D; border: 1px solid rgba(255,255,255,0.15); border-radius: 20px; width: 550px; max-width: 92%; padding: 28px; text-align: center; box-shadow: 0 20px 50px rgba(0,0,0,0.7);">
<div id="ai_spinner" style="font-size: 44px; margin-bottom: 12px; animation: pulse 1.5s infinite;">🧠</div>
<h3 style="color: #FFF; font-size: 18px; font-weight: 800; margin-bottom: 6px;">معالجة واستخراج الذكاء الاصطناعي</h3>
<p id="upload_progress_text" style="color: var(--accent-cyan); font-size: 13px; font-weight: 700; margin-bottom: 14px;">جاري التهيئة...</p>
<div style="width: 100%; background: rgba(0,0,0,0.5); border-radius: 980px; height: 12px; margin-bottom: 8px; overflow: hidden; border: 1px solid rgba(255,255,255,0.1);">
<div id="upload_progress_bar" style="height: 100%; width: 5%; background: linear-gradient(90deg, #00F5D4, #FFD166); transition: width 0.4s ease;"></div>
</div>
<span id="upload_progress_percent" style="color: var(--accent-gold); font-size: 12px; font-weight: 800; font-family: monospace;">5%</span>
<div style="margin-top: 20px; display: flex; justify-content: center; gap: 10px;">
<button type="button" id="btn_toggle_logs" onclick="toggleUploadLogs()" style="background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.15); color: #FFF; padding: 6px 16px; border-radius: 980px; font-size: 12px; cursor: pointer; font-weight: 700;">
🔍 تفاصيل السجل الحي (Terminal Logs)
</button>
<button type="button" id="btn_close_overlay" onclick="closeProgressOverlay()" style="display: none; background: rgba(239,68,68,0.15); border: 1px solid #EF4444; color: #F87171; padding: 6px 16px; border-radius: 980px; font-size: 12px; cursor: pointer; font-weight: 700;">
إغلاق النافذة ✕
</button>
</div>
<!-- Terminal Output Box -->
<div id="upload_logs_container" style="display: none; margin-top: 18px; width: 100%; text-align: left;">
<div style="background: #0D1117; border: 1px solid #30363D; border-radius: 10px; padding: 12px; font-family: monospace; font-size: 11.5px; color: #39D353; height: 160px; overflow-y: auto; direction: ltr;" id="upload_logs_textarea">
[System] Initializing background task worker...
</div>
</div>
</div>
</div>
<script>
let currentFilePath = '';
let currentUploadTaskId = null;
let logsPollingInterval = null;
document.addEventListener('DOMContentLoaded', () => {
renderCurriculumTree(window.CURRICULUM_TREE);
});
function renderCurriculumTree(tree) {
function renderCurriculumTree(tree) {
const container = document.getElementById('curriculum_tree_container');
const entries = Object.entries(tree || {});
@@ -240,20 +283,18 @@ class CurriculumStudio
}
}
// NEW: Render Resources (Minhaji Style Categories)
// Minhaji Style Categories
if (sem.resources && Object.keys(sem.resources).length > 0) {
html += `<div style="margin-top: 15px; margin-bottom: 5px; font-weight: bold; color: var(--text-secondary); font-size: 11px; padding-right: 15px;">📂 مصادر المادة الإضافية:</div>`;
html += `<div style="display: flex; flex-wrap: wrap; gap: 6px; padding-right: 15px; margin-bottom: 10px;">`;
for (const [resKey, resourceGroup] of Object.entries(sem.resources)) {
if (!resourceGroup.items || resourceGroup.items.length === 0) continue;
// Hexagon / Badge Style
html += `<div style="flex: 1 1 45%; background: var(--bg-hover); border: 1px solid var(--border-color); border-radius: 6px; padding: 8px; text-align: center; cursor: pointer; transition: all 0.2s;" onmouseover="this.style.background='rgba(255,209,102,0.1)'" onmouseout="this.style.background='var(--bg-hover)'">
<div style="font-size: 14px; margin-bottom: 4px;">📌</div>
<div style="font-size: 10px; font-weight: bold; color: var(--text-primary);">${escapeHtml(resourceGroup.name)}</div>
</div>`;
// Render actual files below the badge (hidden or just list them)
for (const item of resourceGroup.items) {
const activeClass = (item.file === currentFilePath) ? ' active' : '';
html += `
@@ -273,7 +314,6 @@ class CurriculumStudio
container.innerHTML = html;
// Auto-select first lesson if none selected
if (!currentFilePath) {
const firstGrade = Object.values(tree)[0];
const firstSub = firstGrade?.subjects ? Object.values(firstGrade.subjects)[0] : null;
@@ -285,6 +325,7 @@ class CurriculumStudio
}
}
}
async function selectLesson(filePath, title, breadcrumb, outcomes) {
currentFilePath = filePath;
document.getElementById('empty_selection_view').style.display = 'none';
@@ -294,7 +335,6 @@ class CurriculumStudio
document.getElementById('current_lesson_title_display').textContent = title;
document.getElementById('server_file_path_display').textContent = `backend/storage/curriculum/${filePath}`;
// Render Outcomes
const tags = document.getElementById('current_outcomes_tags');
tags.innerHTML = (outcomes || []).map(o => `
<span style="font-size: 11px; font-weight: 700; background: rgba(255,209,102,0.12); color: var(--accent-gold); border: 1px solid rgba(255,209,102,0.3); padding: 3px 10px; border-radius: 980px;">
@@ -302,11 +342,11 @@ class CurriculumStudio
</span>
`).join('');
// Highlight Active in Sidebar
document.querySelectorAll('.tree-lesson').forEach(el => el.classList.remove('active'));
event?.currentTarget?.classList.add('active');
if (window.event?.currentTarget) {
window.event.currentTarget.classList.add('active');
}
// Fetch Real Markdown from Server
try {
const res = await fetch(`/api/curriculum/lesson?file=${encodeURIComponent(filePath)}`);
const data = await res.json();
@@ -321,15 +361,24 @@ class CurriculumStudio
}
}
// ==========================================================
// Live PDF Upload & Real-Time AI Extraction Tracking
// ==========================================================
async function handleRealPdfUpload(input) {
if (!input.files || !input.files[0]) return;
const file = input.files[0];
const btn = document.getElementById('btn_upload_pdf');
const desc = document.getElementById('upload_status_desc');
btn.disabled = true;
btn.innerHTML = '<span>⏳ جارٍ فك تشفير وتفريغ الكتاب...</span>';
desc.textContent = `جارٍ تفريغ ملف [${file.name}] إلى شجرة ملفات Markdown حقيقية على السيرفر...`;
// Show Progress Modal Overlay
const overlay = document.getElementById('upload_progress_overlay');
overlay.style.display = 'flex';
document.getElementById('btn_close_overlay').style.display = 'none';
document.getElementById('upload_progress_bar').style.width = '8%';
document.getElementById('upload_progress_text').textContent = `جاري رفع [${file.name}] إلى الخادم...`;
document.getElementById('upload_progress_percent').textContent = '8%';
document.getElementById('upload_logs_textarea').innerHTML = `[System] Uploading ${escapeHtml(file.name)} (${(file.size/1024/1024).toFixed(2)} MB)...<br>`;
const formData = new FormData();
formData.append('pdf_file', file);
@@ -340,35 +389,128 @@ class CurriculumStudio
body: formData
});
const data = await res.json();
if (res.ok && data.status === 'success') {
window.CURRICULUM_TREE = data.tree;
currentFilePath = data.active_file || '';
renderCurriculumTree(data.tree);
if (data.active_file) {
document.getElementById('empty_selection_view').style.display = 'none';
document.getElementById('active_lesson_view').style.display = 'block';
document.getElementById('lesson_markdown_editor').value = data.active_md || '';
document.getElementById('current_lesson_title_display').textContent = data.active_title || 'الدرس المستخرج';
document.getElementById('current_breadcrumb').textContent = data.active_breadcrumb || 'المسار التعليمي';
document.getElementById('server_file_path_display').textContent = `${data.server_storage_dir}/${data.active_file}`;
}
alert(`✅ ${data.message}\n\nتم حفظ ملفات المنهاج على السيرفر في:\n${data.server_storage_dir}`);
desc.textContent = `تم اعتماد المنهاج [${file.name}] بنجاح في قاعدة المعرفة الشجرية!`;
if (res.ok && data.status === 'processing') {
currentUploadTaskId = data.task_id;
document.getElementById('upload_progress_bar').style.width = '15%';
document.getElementById('upload_progress_text').textContent = data.message || 'تم استلام الملف، جاري بدء الذكاء الاصطناعي...';
document.getElementById('upload_progress_percent').textContent = '15%';
appendLog(`[Server] Task registered with ID: ${data.task_id}`);
pollTaskStatus(data.task_id, file.name);
} else if (res.ok && data.status === 'success') {
overlay.style.display = 'none';
resetUploadBtn();
loadCurriculumTree();
} else {
alert('⚠️ ' + (data.message || 'فشلت معالجة الملف'));
document.getElementById('upload_progress_text').textContent = '⚠️ ' + (data.message || 'فشلت معالجة الملف');
document.getElementById('btn_close_overlay').style.display = 'inline-block';
resetUploadBtn();
}
} catch (err) {
console.error('PDF Upload error:', err);
alert('حدث خطأ في الاتصال بالخادم أثناء رفع الملف.');
} finally {
btn.disabled = false;
btn.innerHTML = '<span>📤 رفع كتاب المنهاج PDF الحقيقي</span>';
input.value = '';
document.getElementById('upload_progress_text').textContent = 'حدث خطأ في الاتصال بالخادم أثناء رفع الملف.';
document.getElementById('btn_close_overlay').style.display = 'inline-block';
resetUploadBtn();
}
}
async function pollTaskStatus(taskId, fileName) {
try {
const res = await fetch(`/api/curriculum/upload-status?task_id=${taskId}`);
if (!res.ok) throw new Error("Network response was not ok");
const state = await res.json();
// Live Update Modal
document.getElementById('upload_progress_bar').style.width = (state.progress || 20) + '%';
document.getElementById('upload_progress_percent').textContent = (state.progress || 20) + '%';
document.getElementById('upload_progress_text').textContent = state.message || "جاري المعالجة بالذكاء الاصطناعي...";
fetchUploadLogs();
if (state.status === 'completed') {
document.getElementById('upload_progress_bar').style.width = '100%';
document.getElementById('upload_progress_percent').textContent = '100%';
document.getElementById('upload_progress_text').textContent = `✅ اكتملت قراءة وتفريغ المنهاج بنجاح!`;
setTimeout(() => {
document.getElementById('upload_progress_overlay').style.display = 'none';
resetUploadBtn();
if (state.tree) {
window.CURRICULUM_TREE = state.tree;
renderCurriculumTree(state.tree);
if (state.active_file) {
selectLesson(state.active_file, state.extracted_data?.subject_name || 'الدرس المستخرج', 'المنهاج المستخرج', []);
}
} else {
location.reload();
}
}, 2000);
} else if (state.status === 'error') {
document.getElementById('upload_progress_text').textContent = '⚠️ فشل: ' + state.message;
document.getElementById('btn_close_overlay').style.display = 'inline-block';
resetUploadBtn();
} else {
// Poll again in 2.5 seconds
setTimeout(() => pollTaskStatus(taskId, fileName), 2500);
}
} catch(e) {
setTimeout(() => pollTaskStatus(taskId, fileName), 3000);
}
}
async function fetchUploadLogs() {
if (!currentUploadTaskId) return;
try {
const res = await fetch(`/api/curriculum/upload-log?task_id=${currentUploadTaskId}`);
const data = await res.json();
const container = document.getElementById('upload_logs_textarea');
if (data.status === 'success' && data.log) {
container.innerHTML = data.log.replace(/\n/g, '<br>');
container.scrollTop = container.scrollHeight;
}
} catch (e) {
console.error('Fetch logs error:', e);
}
}
function appendLog(msg) {
const container = document.getElementById('upload_logs_textarea');
container.innerHTML += `${escapeHtml(msg)}<br>`;
container.scrollTop = container.scrollHeight;
}
function toggleUploadLogs() {
const container = document.getElementById('upload_logs_container');
container.style.display = (container.style.display === 'none') ? 'block' : 'none';
if (container.style.display === 'block') {
fetchUploadLogs();
}
}
function openDirectLogViewer() {
const overlay = document.getElementById('upload_progress_overlay');
overlay.style.display = 'flex';
document.getElementById('btn_close_overlay').style.display = 'inline-block';
document.getElementById('upload_logs_container').style.display = 'block';
if (currentUploadTaskId) {
fetchUploadLogs();
} else {
document.getElementById('upload_logs_textarea').innerHTML = '[System] No active task ID yet. Upload a textbook PDF to stream live AI logs.<br>';
}
}
function closeProgressOverlay() {
document.getElementById('upload_progress_overlay').style.display = 'none';
resetUploadBtn();
}
function resetUploadBtn() {
const btn = document.getElementById('btn_upload_pdf');
btn.disabled = false;
btn.innerHTML = '<span>📤 رفع كتاب المنهاج PDF الحقيقي</span>';
document.getElementById('curriculum_pdf_input').value = '';
}
async function saveActiveMarkdown() {
if (!currentFilePath) return;
const content = document.getElementById('lesson_markdown_editor').value;
@@ -439,62 +581,6 @@ class CurriculumStudio
return d.innerHTML;
}
</script>
<!-- Upload Progress Modal -->
<div id="upload_progress_overlay" style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(10, 15, 26, 0.85); backdrop-filter: blur(8px); z-index: 9999; flex-direction: column; align-items: center; justify-content: center;">
<div style="background: var(--bg-card); padding: 30px; border-radius: 16px; border: 1px solid var(--border-color); width: 400px; max-width: 90%; text-align: center; box-shadow: 0 10px 40px rgba(0,0,0,0.5);">
<div id="ai_spinner" style="font-size: 40px; margin-bottom: 15px; animation: pulse 1.5s infinite;">🧠</div>
<h3 style="color: var(--text-primary); margin-bottom: 10px; font-family: 'SF Arabic', sans-serif;">معالجة الذكاء الاصطناعي</h3>
<div style="width: 100%; background: var(--bg-hover); border-radius: 10px; height: 10px; margin-bottom: 15px; overflow: hidden; position: relative;">
<div id="upload_progress_bar" style="height: 100%; width: 0%; background: linear-gradient(90deg, var(--accent-gold), #ffed4a); transition: width 0.4s ease;"></div>
</div>
<p id="upload_progress_text" style="color: var(--text-secondary); font-size: 13px; font-weight: bold;">جاري التهيئة...</p>
<p id="upload_progress_percent" style="color: var(--accent-gold); font-size: 12px; margin-top: 5px;">0%</p>
<button id="btn_view_logs" type="button" style="margin-top: 15px; background: transparent; border: 1px solid var(--border-color); color: var(--text-muted); padding: 5px 12px; border-radius: 6px; font-size: 11px; cursor: pointer; transition: all 0.2s;" onmouseover="this.style.color='#fff'" onmouseout="this.style.color='var(--text-muted)'" onclick="toggleUploadLogs()">🔍 عرض سجل السيرفر (Logs)</button>
<div id="upload_logs_container" style="display: none; margin-top: 15px; width: 100%; text-align: left;">
<textarea id="upload_logs_textarea" readonly style="width: 100%; height: 120px; background: #000; color: #0f0; font-family: monospace; font-size: 11px; border: 1px solid #333; border-radius: 4px; padding: 8px; resize: none; direction: ltr;"></textarea>
</div>
</div>
</div>
<script>
let currentUploadTaskId = null;
function toggleUploadLogs() {
const container = document.getElementById('upload_logs_container');
if (container.style.display === 'none') {
container.style.display = 'block';
fetchUploadLogs();
} else {
container.style.display = 'none';
}
}
async function fetchUploadLogs() {
if (!currentUploadTaskId) return;
try {
const res = await fetch(`/api/curriculum/upload-log?task_id=${currentUploadTaskId}`);
const data = await res.json();
const textarea = document.getElementById('upload_logs_textarea');
if (data.status === 'success') {
textarea.value = data.log || 'No logs generated yet...';
textarea.scrollTop = textarea.scrollHeight;
}
} catch (e) {
console.error(e);
}
}
</script>
<style>
@keyframes pulse {
0% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.1); opacity: 0.7; }
100% { transform: scale(1); opacity: 1; }
}
</style>
</body>
</html>
<?php
+128 -26
View File
@@ -585,10 +585,54 @@ class StudentPortal
<input type="text" id="student_otp" maxlength="6" placeholder="• • • • • •" class="input-text" style="font-size: 24px; text-align: center; letter-spacing: 8px;" onkeypress="if(event.key==='Enter') verifyStudentOtp()">
</div>
<button type="button" id="btn_verify_otp" onclick="verifyStudentOtp()" class="btn-primary" style="margin-bottom: 12px;">
<span>تأكيد الرمز والدخول 🚀</span>
<span>تأكيد الرمز 💬</span>
</button>
<button type="button" onclick="switchToPhoneStep()" style="width: 100%; background: transparent; border: none; color: var(--text-muted); font-size: 12px; cursor: pointer; font-weight: 600;">← تغيير رقم الهاتف</button>
</div>
<!-- Step 3: Student Onboarding Form (For New Students) -->
<div id="step_onboarding_container" style="display: none;">
<div style="text-align: center; margin-bottom: 16px;">
<span style="font-size: 32px;">📝</span>
<h3 style="font-size: 16px; font-weight: 800; color: #FFF; margin-top: 6px;">استكمال بيانات الطالب 🎓</h3>
<p style="font-size: 12px; color: var(--text-muted);">يرجى تزويدنا ببياناتك الدراسية لنخصص لك المنهاج والنتاجات التعليمية بدقة.</p>
</div>
<div class="form-group">
<label class="form-label">الاسم الرباعي للطالب *</label>
<input type="text" id="onboard_student_name" placeholder="مثال: أحمد محمد علي الغويري" class="input-text">
</div>
<div class="form-group">
<label class="form-label">الصف الدراسي *</label>
<select id="onboard_student_grade" class="input-text" style="background: var(--bg-card); color: #FFF; cursor: pointer;">
<option value="grade_10" selected>الصف العاشر الأساسي (Grade 10)</option>
<option value="grade_11">الأول ثانوي (Grade 11)</option>
<option value="tawjihi_2008">توجيهي 2008 (الثانوية العامة)</option>
<option value="grade_9">الصف التاسع الأساسي</option>
<option value="grade_8">الصف الثامن الأساسي</option>
</select>
</div>
<div class="form-group">
<label class="form-label">الفرع الأكاديمي</label>
<select id="onboard_student_stream" class="input-text" style="background: var(--bg-card); color: #FFF; cursor: pointer;">
<option value="scientific" selected>الفرع العلمي (Scientific)</option>
<option value="literary">الفرع الأدبي (Literary)</option>
<option value="general">التعليم العام / الأساسي</option>
<option value="vocational">التعليم المهني والتقني (BTEC)</option>
</select>
</div>
<div class="form-group">
<label class="form-label">الرقم الوطني / أو رقم الجلوس (اختياري)</label>
<input type="text" id="onboard_student_national_id" placeholder="الرقم الوطني المكون من 10 خانات" class="input-text">
</div>
<button type="button" id="btn_complete_onboarding" onclick="completeStudentOnboarding()" class="btn-primary" style="margin-top: 10px;">
<span>إتمام التسجيل وبدء التعلم 🚀</span>
</button>
</div>
</div>
</div>
@@ -1939,7 +1983,7 @@ class StudentPortal
}
}
async function verifyStudentOtp() {
async function verifyStudentOtp() {
const phone = document.getElementById('student_phone').value.trim();
const otp = document.getElementById('student_otp').value.trim();
if (!otp || otp.length < 4) {
@@ -1954,11 +1998,19 @@ class StudentPortal
});
const data = await res.json();
if (res.ok && data.status === 'success') {
localStorage.setItem('saqel_student_jwt', data.data.token);
localStorage.setItem('saqel_student_user', JSON.stringify(data.data.user));
renderDashboard(data.data.user);
initWebSocket(data.data.token);
showLuxuryToast('أهلاً بك يا بطل! 🚀', 'تم تسجيل الدخول بنجاح.');
const token = data.data.token;
localStorage.setItem('saqel_student_jwt', token);
// Check if student needs onboarding
const isNew = data.data.user.is_new || data.data.user.name === 'طالب جديد' || data.data.user.name === 'الطالب المتميز';
if (isNew) {
switchToStudentOnboarding();
} else {
localStorage.setItem('saqel_student_user', JSON.stringify(data.data.user));
renderDashboard(data.data.user);
initWebSocket(token);
showLuxuryToast('أهلاً بك يا بطل! 🚀', 'تم تسجيل الدخول بنجاح.');
}
} else {
showError(data.message || 'رمز التحقق غير صحيح أو منتهي الصلاحية');
}
@@ -1967,11 +2019,67 @@ class StudentPortal
}
}
function switchToStudentOnboarding() {
document.getElementById('step_phone_container').style.display = 'none';
document.getElementById('step_otp_container').style.display = 'none';
document.getElementById('step_onboarding_container').style.display = 'block';
document.getElementById('alert_box_error').style.display = 'none';
document.getElementById('alert_box_success').style.display = 'none';
}
async function completeStudentOnboarding() {
const token = localStorage.getItem('saqel_student_jwt');
const fullName = document.getElementById('onboard_student_name').value.trim();
const grade = document.getElementById('onboard_student_grade').value;
const stream = document.getElementById('onboard_student_stream').value;
const nationalId = document.getElementById('onboard_student_national_id').value.trim();
if (!fullName || fullName.length < 3) {
showError('يرجى إدخال اسم الطالب الكامل (الرباعي)');
return;
}
const btn = document.getElementById('btn_complete_onboarding');
btn.disabled = true;
btn.innerHTML = '<span>⏳ جاري حفظ البيانات...</span>';
try {
const res = await fetch('/api/student/profile/setup', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({
full_name: fullName,
grade_level: grade,
stream: stream,
national_id: nationalId
})
});
const data = await res.json();
if (res.ok && data.status === 'success') {
const u = data.user;
localStorage.setItem('saqel_student_user', JSON.stringify(u));
renderDashboard(u);
initWebSocket(token);
showLuxuryToast('تم استكمال الحساب بنجاح! 🚀', `مرحباً بك يا ${fullName}`);
} else {
showError(data.message || 'فشل حفظ الملف الشخصي');
}
} catch (e) {
showError('تعذر الاتصال بالسيرفر أثناء حفظ البيانات');
} finally {
btn.disabled = false;
btn.innerHTML = '<span>إتمام التسجيل وبدء التعلم 🚀</span>';
}
}
function renderDashboard(user) {
document.getElementById('auth_box_view').style.display = 'none';
document.getElementById('dashboard_container').style.display = 'block';
document.getElementById('auth_user_badge').style.display = 'flex';
const name = user.full_name || 'طالب صَقِل';
const name = user.full_name || user.name || 'طالب صَقِل';
document.getElementById('header_student_name').textContent = `أهلاً، ${name}`;
document.getElementById('dashboard_welcome_title').textContent = `أهلاً بك يا ${name} 🚀`;
}
@@ -1983,37 +2091,31 @@ class StudentPortal
}
function switchToPhoneStep() {
document.getElementById('step_onboarding_container').style.display = 'none';
document.getElementById('step_otp_container').style.display = 'none';
document.getElementById('step_phone_container').style.display = 'block';
document.getElementById('alert_box_error').style.display = 'none';
document.getElementById('alert_box_success').style.display = 'none';
}
function showError(msg) {
document.getElementById('alert_box_success').style.display = 'none';
const errBox = document.getElementById('alert_box_error');
document.getElementById('alert_error_msg').textContent = msg;
errBox.style.display = 'block';
}
function showSuccess(msg) {
document.getElementById('alert_box_error').style.display = 'none';
const okBox = document.getElementById('alert_box_success');
document.getElementById('alert_success_msg').textContent = msg;
okBox.style.display = 'block';
}
async function checkStudentSession(token) {
try {
const res = await fetch('/api/auth/me', {
headers: { 'Authorization': 'Bearer ' + token }
});
if (res.status === 401) {
handleLogout();
return;
}
const data = await res.json();
if (res.ok && data.data) {
renderDashboard(data.data.user || data.data);
} else {
// Token invalid or database was reset: clear cached session
handleLogout();
const user = data.data;
if (!user.is_completed) {
switchToStudentOnboarding();
} else {
localStorage.setItem('saqel_student_user', JSON.stringify(user));
renderDashboard(user);
}
}
} catch (e) {
console.error('Student session check error:', e);
+11 -5
View File
@@ -1219,13 +1219,19 @@ class TeacherPortal
const user = data.data.user;
const profile = data.data.profile || {};
localStorage.setItem('saqel_teacher_jwt', token);
if (user) {
localStorage.setItem('saqel_teacher_user', JSON.stringify({ ...user, ...profile }));
}
document.cookie = "saqel_teacher_jwt=" + token + "; path=/; max-age=2592000; SameSite=Lax";
initWebSocket(token);
renderDashboard(user, profile);
await checkTeacherSession(token);
const isNewTeacher = user?.is_new || user?.full_name === 'معلم جديد' || user?.name === 'معلم جديد' || user?.name === 'الأستاذ المعتمد';
if (isNewTeacher) {
switchToOnboardingStep();
} else {
if (user) {
localStorage.setItem('saqel_teacher_user', JSON.stringify({ ...user, ...profile }));
}
renderDashboard(user, profile);
await checkTeacherSession(token);
}
} else {
showError(data.message || 'رمز التحقق غير صحيح');
}
+2
View File
@@ -87,6 +87,8 @@ $router->post('/api/auth/otp/request', [\App\Controllers\AuthController::class,
$router->post('/api/auth/otp/verify', [\App\Controllers\AuthController::class, 'verifyOtp'], [\App\Middlewares\RateLimitMiddleware::class]);
$router->post('/api/auth/logout', [\App\Controllers\AuthController::class, 'logout'], [\App\Middlewares\AuthMiddleware::class]);
$router->get('/api/auth/me', [\App\Controllers\AuthController::class, 'me'], [\App\Middlewares\AuthMiddleware::class]);
$router->get('/api/student/profile/status', [\App\Controllers\AuthController::class, 'studentProfileStatus'], [\App\Middlewares\AuthMiddleware::class]);
$router->post('/api/student/profile/setup', [\App\Controllers\AuthController::class, 'studentProfileSetup'], [\App\Middlewares\AuthMiddleware::class]);
// Legacy / Direct Auth
$router->post('/api/auth/register', [\App\Controllers\AuthController::class, 'register'], [\App\Middlewares\RateLimitMiddleware::class]);