Update Saqel Platform: 2026-08-28 18:21:58
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 || 'رمز التحقق غير صحيح');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user