Files
saqel/backend/app/Views/StudentPortal.php
T

1459 lines
68 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Views;
class StudentPortal
{
public static function render(): string
{
return <<<'HTML'
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>بوابة الطالب — منصة صَقِل التعليمية</title>
<link rel="icon" type="image/jpeg" href="/assets/images/saqel_logo.jpg">
<link rel="apple-touch-icon" href="/assets/images/saqel_logo.jpg">
<!-- Apple SF Pro & Alexandria Arabic Web Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Alexandria:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
<!-- Apple-Grade Luxury Cupertino CSS -->
<style>
:root {
--bg-dark: #000000;
--bg-card: #121214;
--bg-card-hover: #18181B;
--bg-input: #09090B;
--border: rgba(255, 255, 255, 0.08);
--border-focus: #0071E3;
--text-primary: #F5F5F7;
--text-secondary: #A1A1A6;
--text-muted: #6E6E73;
--accent-blue: #0071E3;
--accent-blue-hover: #0077ED;
--accent-cyan: #00F5D4;
--accent-gold: #F59E0B;
--accent-red: #EF4444;
--accent-green: #10B981;
--accent-purple: #8B5CF6;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, "SF Pro AR", "SF Pro Display", "SF Pro Text", "SF Arabic", "Alexandria", "Segoe UI", Roboto, sans-serif;
letter-spacing: -0.015em;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
background-color: var(--bg-dark);
color: var(--text-primary);
min-height: 100vh;
display: flex;
flex-direction: column;
overflow-x: hidden;
background-image:
radial-gradient(circle at 50% 0%, rgba(0, 113, 227, 0.15) 0%, transparent 60%),
radial-gradient(circle at 90% 40%, rgba(245, 158, 11, 0.05) 0%, transparent 50%),
radial-gradient(circle at 10% 80%, rgba(0, 245, 212, 0.05) 0%, transparent 50%);
background-size: 100% 100%;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 0 24px;
width: 100%;
}
/* Apple Glass Header */
header {
border-bottom: 1px solid var(--border);
background-color: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(24px) saturate(180%);
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
height: 68px;
display: flex;
align-items: center;
justify-content: space-between;
}
.logo-area {
display: flex;
align-items: center;
gap: 12px;
text-decoration: none;
}
.logo-img {
width: 38px;
height: 38px;
border-radius: 10px;
object-fit: cover;
box-shadow: 0 0 16px rgba(0, 113, 227, 0.35);
border: 1px solid rgba(255, 255, 255, 0.15);
transition: transform 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
.logo-area:hover .logo-img { transform: scale(1.06); }
.brand-title {
font-size: 19px;
font-weight: 800;
color: #FFFFFF;
letter-spacing: -0.03em;
}
.portal-pill {
font-size: 11px;
font-weight: 700;
color: var(--accent-cyan);
background: rgba(0, 245, 212, 0.1);
border: 1px solid rgba(0, 245, 212, 0.25);
padding: 3px 10px;
border-radius: 980px;
margin-right: 8px;
}
.ws-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-weight: 700;
padding: 4px 12px;
border-radius: 980px;
transition: all 0.3s;
}
.ws-online { background: rgba(16, 185, 129, 0.15); color: #34D399; border: 1px solid rgba(16, 185, 129, 0.3); }
.ws-offline { background: rgba(239, 68, 68, 0.15); color: #F87171; border: 1px solid rgba(239, 68, 68, 0.3); }
.ws-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; box-shadow: 0 0 6px currentColor; }
/* Auth Form Box */
.auth-card-wrapper {
max-width: 440px;
margin: 60px auto 40px;
width: 100%;
}
.auth-header { text-align: center; margin-bottom: 28px; }
.auth-title { font-size: 26px; font-weight: 800; color: #FFF; margin-bottom: 8px; }
.auth-subtitle { font-size: 14px; color: var(--text-secondary); }
.auth-box {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 28px;
padding: 36px;
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6);
}
/* Form Controls */
.form-group { margin-bottom: 20px; }
.form-label { display: block; font-size: 13px; font-weight: 600; color: var(--text-secondary); margin-bottom: 8px; }
.input-text {
width: 100%;
padding: 13px 16px;
background: var(--bg-input);
border: 1px solid var(--border);
border-radius: 14px;
color: #FFF;
font-size: 14px;
outline: none;
transition: all 0.2s;
}
.input-text:focus { border-color: var(--accent-blue); box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.25); }
.phone-input-group { display: flex; gap: 8px; }
.country-badge {
background: rgba(255, 255, 255, 0.05);
border: 1px solid var(--border);
border-radius: 14px;
padding: 0 14px;
display: flex;
align-items: center;
font-weight: 700;
font-size: 13px;
color: var(--text-secondary);
}
.btn-primary {
width: 100%;
padding: 13px;
border-radius: 980px;
border: none;
background: var(--accent-blue);
color: #FFF;
font-size: 14.5px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 4px 20px rgba(0, 113, 227, 0.4);
}
.btn-primary:hover { background: var(--accent-blue-hover); transform: scale(1.01); }
.btn-secondary {
background: none;
border: none;
color: var(--text-muted);
font-size: 13px;
cursor: pointer;
transition: color 0.2s;
}
.btn-secondary:hover { color: #FFF; }
/* Dashboard UI */
.dashboard-hero {
background: linear-gradient(135deg, rgba(18, 18, 20, 0.95), rgba(24, 24, 27, 0.8));
border: 1px solid var(--border);
border-radius: 28px;
padding: 32px 36px;
margin: 32px 0 24px;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 24px;
}
.readiness-box {
display: flex;
align-items: center;
gap: 16px;
background: rgba(0, 0, 0, 0.6);
border: 1px solid var(--border);
border-radius: 20px;
padding: 16px 24px;
}
.gauge-circle {
width: 58px;
height: 58px;
border-radius: 50%;
background: radial-gradient(circle, rgba(0, 245, 212, 0.2) 0%, transparent 70%);
border: 3px solid var(--accent-cyan);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-weight: 900;
color: #FFFFFF;
box-shadow: 0 0 16px rgba(0, 245, 212, 0.3);
}
/* Tabs Nav */
.tabs-nav {
display: flex;
gap: 10px;
margin-bottom: 24px;
border-bottom: 1px solid var(--border);
padding-bottom: 12px;
overflow-x: auto;
}
.tab-btn {
background: rgba(255, 255, 255, 0.04);
border: 1px solid var(--border);
color: var(--text-secondary);
font-size: 13.5px;
font-weight: 700;
padding: 10px 22px;
border-radius: 980px;
cursor: pointer;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
white-space: nowrap;
}
.tab-btn:hover { background: rgba(255, 255, 255, 0.08); color: #FFF; }
.tab-btn.active {
background: var(--accent-blue);
color: #FFF;
border-color: var(--accent-blue);
box-shadow: 0 2px 12px rgba(0, 113, 227, 0.4);
}
.studio-card {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 28px;
padding: 32px;
margin-bottom: 40px;
}
/* Socratic Interactive Video Player */
.video-container {
position: relative;
background: #000000;
border: 1px solid var(--border);
border-radius: 20px;
overflow: hidden;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.8);
}
.video-element {
width: 100%;
height: 480px;
object-fit: cover;
display: block;
background: #050507;
}
.player-bar {
background: rgba(18, 18, 20, 0.95);
border-top: 1px solid var(--border);
padding: 14px 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.time-badge {
font-family: monospace;
font-size: 13px;
font-weight: 700;
color: var(--accent-cyan);
background: rgba(0, 245, 212, 0.1);
padding: 4px 10px;
border-radius: 6px;
}
/* Socratic Checkpoint Modal Overlay */
.socratic-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.88);
backdrop-filter: blur(20px);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 32px;
z-index: 10;
text-align: center;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
.quiz-option-btn {
width: 100%;
max-width: 580px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.15);
color: #FFF;
padding: 14px 20px;
border-radius: 14px;
font-size: 14px;
font-weight: 600;
margin-bottom: 10px;
cursor: pointer;
text-align: right;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
}
.quiz-option-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: var(--accent-blue);
transform: translateX(-4px);
}
.quiz-option-btn.correct {
background: rgba(16, 185, 129, 0.25) !important;
border-color: #10B981 !important;
color: #34D399 !important;
}
.quiz-option-btn.wrong {
background: rgba(239, 68, 68, 0.25) !important;
border-color: #EF4444 !important;
color: #F87171 !important;
}
/* Real-time Chat Box */
.student-chat-box {
display: flex;
flex-direction: column;
height: 480px;
border: 1px solid var(--border);
border-radius: 20px;
overflow: hidden;
background: #09090B;
}
.chat-messages {
flex: 1;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 12px;
}
.chat-msg {
max-width: 75%;
padding: 12px 18px;
border-radius: 18px;
font-size: 14px;
line-height: 1.6;
}
.chat-msg.student {
align-self: flex-start;
background: var(--accent-blue);
color: #FFF;
border-bottom-right-radius: 4px;
}
.chat-msg.teacher {
align-self: flex-end;
background: #18181B;
border: 1px solid var(--border);
color: #FFF;
border-bottom-left-radius: 4px;
}
.chat-time { display: block; font-size: 11px; opacity: 0.6; margin-top: 4px; }
.chat-input-bar {
padding: 14px;
background: #121214;
border-top: 1px solid var(--border);
display: flex;
gap: 10px;
}
/* Exam Taking Modal & Diagnostic Report */
.exam-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(20px);
z-index: 200;
display: none;
align-items: center;
justify-content: center;
padding: 20px;
}
.exam-card {
background: #121214;
border: 1px solid var(--border);
border-radius: 28px;
max-width: 680px;
width: 100%;
padding: 36px;
box-shadow: 0 24px 60px rgba(0, 0, 0, 0.8);
position: relative;
}
/* DRM Watermark */
@keyframes bounce-watermark {
0%, 100% { transform: translate(10px, 10px); }
50% { transform: translate(140px, 80px); }
}
.watermark-anim {
position: fixed; bottom: 20px; left: 20px; z-index: 99; pointer-events: none; opacity: 0.35;
animation: bounce-watermark 12s infinite ease-in-out; background: rgba(0, 0, 0, 0.75);
border: 1px solid rgba(255, 255, 255, 0.2); padding: 4px 10px; border-radius: 6px;
font-family: monospace; font-size: 11px; color: var(--accent-cyan);
}
.spinner {
width: 18px; height: 18px; border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: #FFF; border-radius: 50%; animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
footer {
border-top: 1px solid var(--border);
padding: 24px 0;
background: #000000;
color: var(--text-muted);
font-size: 12px;
margin-top: auto;
}
</style>
</head>
<body>
<!-- Header -->
<header>
<div class="container">
<div class="header-content">
<a href="/student" class="logo-area">
<img src="/assets/images/saqel_logo.jpg" alt="صَقِل" class="logo-img">
<div>
<span class="brand-title">صَقِل</span>
<span class="portal-pill">بوابة الطالب الذكية</span>
</div>
</a>
<div id="header_user_actions">
<div id="auth_user_badge" style="display: none; align-items: center; gap: 12px;">
<span id="ws_status_badge" class="ws-badge ws-offline">
<span class="ws-dot"></span>
<span id="ws_status_text">Workerman جاري الاتصال...</span>
</span>
<span style="font-size: 13px; color: var(--text-secondary);" id="student_display_name"></span>
<button onclick="handleLogout()" style="background: none; border: 1px solid rgba(239,68,68,0.4); color: #F87171; border-radius: 980px; padding: 6px 14px; font-size: 12px; cursor: pointer;">
تسجيل الخروج
</button>
</div>
<a href="/teacher" class="nav-btn" id="switch_to_teacher_link" style="color: var(--text-secondary); font-size: 13px; text-decoration: none;">
<span>أنت معلم؟</span>
<span style="color: var(--accent-gold); font-weight: 700; margin-right: 4px;">استوديو المعلمين ←</span>
</a>
</div>
</div>
</div>
</header>
<!-- Main Content -->
<main>
<div class="container">
<!-- 1. AUTHENTICATION (PHONE & OTP) -->
<div id="auth_container" class="auth-card-wrapper">
<div class="auth-header">
<h1 class="auth-title">ادخل لعالم التمكين والفهم</h1>
<p class="auth-subtitle">تسجيل دخول آمن وسريع عبر رمز الواتساب المعتمد</p>
</div>
<div class="auth-box">
<div id="alert_box_error" class="alert alert-error" style="display: none; color: #F87171; background: rgba(239,68,68,0.15); border: 1px solid rgba(239,68,68,0.3); padding: 12px; border-radius: 12px; font-size: 13px; margin-bottom: 16px;">
<span>⚠️ </span><span id="alert_error_msg"></span>
</div>
<div id="alert_box_success" class="alert alert-success" style="display: none; color: #34D399; background: rgba(16,185,129,0.15); border: 1px solid rgba(16,185,129,0.3); padding: 12px; border-radius: 12px; font-size: 13px; margin-bottom: 16px;">
<span>✓ </span><span id="alert_success_msg"></span>
</div>
<!-- Step 1: Phone -->
<div id="step_phone_container">
<div class="form-group">
<label class="form-label">الاسم الكامل للطالب</label>
<input type="text" id="student_fullname" placeholder="مثال: أحمد محمد الغويري" class="input-text">
</div>
<div class="form-group">
<label class="form-label">رقم الهاتف (الواتساب) <span style="color: #EF4444;">*</span></label>
<div class="phone-input-group">
<div class="country-badge">🇯🇴 +962</div>
<input type="tel" id="student_phone" required placeholder="790000000" class="input-text">
</div>
</div>
<button type="button" id="btn_send_otp" onclick="handleSendOtp(event)" class="btn-primary">
<span id="btn_send_text">إرسال رمز التحقق (OTP) ←</span>
<div id="btn_send_spinner" class="spinner" style="display: none;"></div>
</button>
</div>
<!-- Step 2: OTP Code -->
<div id="step_otp_container" style="display: none;">
<div style="text-align: center; margin-bottom: 20px;">
<span style="font-size: 12px; color: var(--text-muted);">تم إرسال رمز التحقق إلى:</span>
<div style="font-family: monospace; font-size: 15px; font-weight: 800; color: var(--accent-cyan); margin-top: 4px;" id="otp_target_display"></div>
</div>
<div class="form-group">
<label class="form-label" style="text-align: center;">أدخل رمز التحقق (6 أرقام)</label>
<input type="text" id="student_otp_code" maxlength="6" autofocus placeholder="• • • • • •" class="input-text" style="text-align: center; font-size: 22px; letter-spacing: 6px;">
</div>
<button type="button" id="btn_verify_otp" onclick="handleVerifyOtp(event)" class="btn-primary">
<span id="btn_verify_text">تأكيد الدخول للمنصة ✨</span>
<div id="btn_verify_spinner" class="spinner" style="display: none;"></div>
</button>
<div style="display: flex; align-items: center; justify-content: space-between; margin-top: 18px;">
<button type="button" onclick="switchToPhoneStep()" class="btn-secondary">← تعديل الرقم</button>
<span id="resend_timer_label" style="font-size: 12px; color: var(--text-muted); display: none;"></span>
<button type="button" id="btn_resend" onclick="handleSendOtp(event)" class="btn-secondary" style="display: none; color: var(--accent-cyan);">إعادة إرسال الرمز ↺</button>
</div>
</div>
</div>
</div>
<!-- 2. STUDENT DASHBOARD (AUTHENTICATED) -->
<div id="dashboard_container" style="width: 100%; display: none;">
<!-- Hero & Tawjihi Readiness Score -->
<div class="dashboard-hero">
<div>
<span style="display: inline-block; font-size: 11px; font-weight: 800; color: var(--accent-cyan); background: rgba(0,245,212,0.12); border: 1px solid rgba(0,245,212,0.3); padding: 4px 12px; border-radius: 980px; margin-bottom: 10px;">توجيهي 2008 — الفرع العلمي</span>
<h2 style="font-size: 26px; font-weight: 800; color: #FFFFFF;" id="dashboard_welcome_title">أهلاً بك يا بطلنا 🚀</h2>
<p style="font-size: 13px; color: var(--text-secondary); margin-top: 4px;">صقل الفهم، حل الأسئلة الوزارية، والتواصل الحي مع أستاذك.</p>
</div>
<div class="readiness-box">
<div class="gauge-circle" id="readiness_gauge_val">87.5%</div>
<div>
<span style="font-size: 11px; font-weight: 700; color: var(--text-muted); display: block;">مؤشر الجاهزية للوزاري</span>
<span style="font-size: 12px; font-weight: 800; color: #34D399;" id="readiness_status_text">مستواك ممتاز ومستقر (+2.4% هذا الأسبوع)</span>
</div>
</div>
</div>
<!-- Tabs Navigation -->
<div class="tabs-nav">
<button class="tab-btn active" id="tab_btn_lessons" onclick="switchStudentTab('lessons')">🎬 الحصص ومشغل الفيديو السقراطي</button>
<button class="tab-btn" id="tab_btn_chat" onclick="switchStudentTab('chat')">💬 اسأل الأستاذ حمزة الغويري (Workerman 0ms)</button>
<button class="tab-btn" id="tab_btn_exams" onclick="switchStudentTab('exams')">📝 الامتحانات وهرمية التقييم المعرفي</button>
</div>
<!-- TAB 1: Socratic Interactive Lesson Player -->
<div id="tab_lessons_content" class="studio-card">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px; flex-wrap: wrap; gap: 10px;">
<div>
<h3 style="font-size: 18px; font-weight: 800;">الدرس 4: قواعد الاشتقاق الأساسية — رياضيات علمي</h3>
<span style="font-size: 12px; color: var(--text-muted);">الأستاذ حمزة الغويري • Bunny Stream DRM Protection</span>
</div>
<span style="font-size: 11px; font-weight: 800; color: var(--accent-cyan); background: rgba(0,245,212,0.1); border: 1px solid rgba(0,245,212,0.3); padding: 5px 14px; border-radius: 980px;">
Checkpoint Active (00:15)
</span>
</div>
<div class="video-container">
<video id="lesson_video_player" class="video-element" controls poster="/assets/images/saqel_logo.jpg">
<source src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4" type="video/mp4">
متصفحك لا يدعم مشغل الفيديو.
</video>
<!-- Socratic Checkpoint Overlay Modal -->
<div id="socratic_quiz_modal" class="socratic-overlay" style="display: none;">
<span style="font-size: 11px; font-weight: 800; color: var(--accent-gold); background: rgba(245, 158, 11, 0.15); border: 1px solid rgba(245, 158, 11, 0.4); padding: 4px 14px; border-radius: 980px; margin-bottom: 14px;">
⚠️ توقف الشرح لفحص الفهم اللحظي (Socratic Active Recall)
</span>
<h3 style="font-size: 18px; font-weight: 800; color: #FFFFFF; margin-bottom: 18px; max-width: 580px;">
إذا كان f(x) = sin(3x)، فما هي مشتقة الاقتران f'(x)؟
</h3>
<div style="width: 100%; display: flex; flex-direction: column; align-items: center;">
<button type="button" class="quiz-option-btn" onclick="handleCheckpointAnswer(this, true)">
أ) 3 cos(3x) (مشتقة الزاوية ضرب مشتقة الاقتران)
</button>
<button type="button" class="quiz-option-btn" onclick="handleCheckpointAnswer(this, false)">
ب) cos(3x)
</button>
<button type="button" class="quiz-option-btn" onclick="handleCheckpointAnswer(this, false)">
ج) -3 cos(3x)
</button>
</div>
<div id="checkpoint_feedback" style="margin-top: 14px; font-size: 13.5px; font-weight: 700; display: none;"></div>
</div>
<div class="player-bar">
<div style="display: flex; align-items: center; gap: 12px;">
<span class="time-badge" id="video_time_display">00:00 / 09:56</span>
<span style="font-size: 12px; color: var(--text-muted);">نقطة التثبيت المعرفي التالية عند: 00:15</span>
</div>
<button type="button" onclick="triggerCheckpointDemo()" style="background: rgba(245,158,11,0.15); border: 1px solid rgba(245,158,11,0.3); color: var(--accent-gold); border-radius: 980px; padding: 5px 14px; font-size: 11.5px; cursor: pointer; font-weight: 700;">
محاكاة نقطة الكويز اللحظي (00:15) ⏱️
</button>
</div>
</div>
</div>
<!-- TAB 2: Real-time Chat with Teacher (Workerman WebSocket) -->
<div id="tab_chat_content" class="studio-card" style="display: none;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px;">
<div>
<h3 style="font-size: 18px; font-weight: 800;">محادثة معلم المادة المباشرة 💬</h3>
<p style="font-size: 12px; color: var(--text-muted);">اسأل الأستاذ حمزة الغويري عن أي فكرة في الدرس وتلقى الإجابة فوراً.</p>
</div>
<span style="font-size: 11px; font-weight: 800; background: rgba(16,185,129,0.15); border: 1px solid rgba(16,185,129,0.4); color: #34D399; padding: 4px 12px; border-radius: 980px;">
الأستاذ حمزة الغويري (متصل الآن 🟢)
</span>
</div>
<div class="student-chat-box">
<div class="chat-messages" id="student_chat_messages">
<div class="chat-msg teacher">
أهلاً بك يا بطل! إذا واجهت أي خطوة غير واضحة في درس قواعد الاشتقاق، اكتب لي سؤالك هنا وسأجيبك فوراً.
<span class="chat-time">قبل قليل</span>
</div>
</div>
<div class="chat-input-bar">
<input type="text" id="student_chat_input" placeholder="اكتب استفسارك للأستاذ حمزة..." class="input-text" onkeypress="if(event.key==='Enter') sendStudentMessage()">
<button type="button" onclick="sendStudentMessage()" class="btn-primary" style="width: auto; padding: 0 26px; font-size: 13px;">إرسال ↵</button>
</div>
</div>
</div>
<!-- TAB 3: Multi-Level Exams & AI Diagnostics -->
<div id="tab_exams_content" class="studio-card" style="display: none;">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px;">
<div>
<h3 style="font-size: 18px; font-weight: 800;">هرمية الامتحانات والتشخيص الذكي 📝</h3>
<p style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">4 مستويات من الامتحانات لقياس الفهم وسد الثغرات قبل الامتحان الوزاري.</p>
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 18px;">
<!-- Level 1 -->
<div style="background: rgba(0,0,0,0.5); border: 1px solid var(--border); border-radius: 20px; padding: 24px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span style="font-size: 11px; font-weight: 800; color: var(--accent-cyan); background: rgba(0,245,212,0.1); padding: 2px 8px; border-radius: 6px;">المستوى 1: اختبار الدرس</span>
<span style="font-size: 11px; color: var(--text-muted);">4 أسئلة • 10 دقائق</span>
</div>
<h4 style="font-size: 16px; font-weight: 800;">امتحان قواعد الاشتقاق</h4>
<p style="font-size: 12px; color: var(--text-muted); margin: 6px 0 16px 0;">فحص المهارات الرياضية المباشرة بعد الدرس.</p>
<button type="button" onclick="startExamModal('امتحان قواعد الاشتقاق', 1)" class="btn-primary" style="padding: 10px; font-size: 13px;">بدء الامتحان الآن ⏱️</button>
</div>
<!-- Level 2 -->
<div style="background: rgba(0,0,0,0.5); border: 1px solid var(--border); border-radius: 20px; padding: 24px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span style="font-size: 11px; font-weight: 800; color: var(--accent-purple); background: rgba(139,92,246,0.1); padding: 2px 8px; border-radius: 6px;">المستوى 2: امتحان الوحدة</span>
<span style="font-size: 11px; color: var(--text-muted);">15 سؤالاً • 30 دقيقة</span>
</div>
<h4 style="font-size: 16px; font-weight: 800;">امتحان وحدة التفاضل الشامل</h4>
<p style="font-size: 12px; color: var(--text-muted); margin: 6px 0 16px 0;">محاكاة معيارية لكافة أنماط الأسئلة الوزارية للوحدة.</p>
<button type="button" onclick="startExamModal('امتحان وحدة التفاضل الشامل', 2)" class="btn-primary" style="padding: 10px; font-size: 13px; background: var(--accent-purple);">بدء امتحان الوحدة 📚</button>
</div>
<!-- Level 3 -->
<div style="background: rgba(0,0,0,0.5); border: 1px solid rgba(245,158,11,0.4); border-radius: 20px; padding: 24px;">
<div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
<span style="font-size: 11px; font-weight: 800; color: var(--accent-gold); background: rgba(245,158,11,0.1); padding: 2px 8px; border-radius: 6px;">المستوى 3: ذكاء اصطناعي</span>
<span style="font-size: 11px; color: var(--text-muted);">تكيفي مخصص</span>
</div>
<h4 style="font-size: 16px; font-weight: 800;">امتحان سد الثغرات التلقائي</h4>
<p style="font-size: 12px; color: var(--text-muted); margin: 6px 0 16px 0;">يولد أسئلة تكيفية تركز على نقاط ضعفك بدقة.</p>
<button type="button" onclick="startExamModal('امتحان سد الثغرات التلقائي', 3)" class="btn-primary" style="padding: 10px; font-size: 13px; background: linear-gradient(135deg, #F59E0B, #D97706);">توليد وبدء التقييم 🧠</button>
</div>
</div>
</div>
<!-- Dynamic DRM Watermark -->
<div class="watermark-anim">
<span id="drm_watermark_text">SAQEL-DRM • 079XXXXXXX</span>
</div>
</div>
</div>
</main>
<!-- 3. INTERACTIVE EXAM TAKING MODAL -->
<div id="exam_taking_modal" class="exam-modal">
<div class="exam-card">
<!-- Header -->
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<div>
<h3 style="font-size: 18px; font-weight: 800;" id="exam_title_display">امتحان قواعد الاشتقاق</h3>
<span style="font-size: 12px; color: var(--text-muted);" id="exam_progress_text">السؤال 1 من 3</span>
</div>
<div style="background: rgba(239,68,68,0.15); border: 1px solid rgba(239,68,68,0.3); color: #F87171; padding: 4px 14px; border-radius: 980px; font-family: monospace; font-size: 13px; font-weight: 800;" id="exam_timer_badge">
⏱️ 14:59
</div>
</div>
<!-- Question Screen -->
<div id="exam_question_container">
<div style="background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 16px; padding: 20px; margin-bottom: 20px;">
<h4 style="font-size: 16px; font-weight: 700; line-height: 1.6;" id="exam_question_text">
إذا كان f(x) = (2x + 1)³، فما هي قيمة المشتقة f'(1)؟
</h4>
</div>
<div id="exam_options_list" style="display: flex; flex-direction: column; gap: 10px; margin-bottom: 24px;">
<!-- Options injected dynamically -->
</div>
<div style="display: flex; justify-content: space-between; align-items: center;">
<button type="button" onclick="closeExamModal()" class="btn-secondary">إلغاء الخروج</button>
<button type="button" id="btn_next_question" onclick="handleNextQuestion()" class="btn-primary" style="width: auto; padding: 10px 28px;">
السؤال التالي ←
</button>
</div>
</div>
<!-- AI Diagnostic Report (Shown on Submit) -->
<div id="exam_result_container" style="display: none; text-align: center;">
<div style="width: 72px; height: 72px; border-radius: 50%; background: rgba(16,185,129,0.15); border: 2px solid #10B981; color: #34D399; font-size: 32px; display: flex; align-items: center; justify-content: center; margin: 0 auto 16px;">
✓
</div>
<h3 style="font-size: 22px; font-weight: 800; margin-bottom: 6px;">تم إنهاء الامتحان وحساب التشخيص الذكي!</h3>
<p style="font-size: 13px; color: var(--text-muted); margin-bottom: 20px;">تقرير التثبيت المعرفي الصادر من محرك صَقِل للذكاء الاصطناعي</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 20px;">
<div style="background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 16px; padding: 16px;">
<span style="font-size: 11px; color: var(--text-muted); display: block;">النتيجة النهائية</span>
<strong style="font-size: 24px; color: #34D399;" id="result_score_display">100%</strong>
</div>
<div style="background: rgba(255,255,255,0.03); border: 1px solid var(--border); border-radius: 16px; padding: 16px;">
<span style="font-size: 11px; color: var(--text-muted); display: block;">أثر مؤشر التوجيهي</span>
<strong style="font-size: 24px; color: var(--accent-cyan);" id="result_readiness_display">+2.4%</strong>
</div>
</div>
<div style="background: rgba(245,158,11,0.08); border: 1px solid rgba(245,158,11,0.25); border-radius: 16px; padding: 18px; text-align: right; margin-bottom: 24px;">
<strong style="font-size: 13px; color: var(--accent-gold); display: block; margin-bottom: 4px;">🔍 التوصية السقراطية الذكية:</strong>
<p style="font-size: 12.5px; color: var(--text-secondary); line-height: 1.6;" id="result_recommendation_text">
أظهرت إتقاناً ممتازاً لقاعدة السلسلة. يُوصى بمواصلة الدرس الخامس للحفاظ على نسق الجاهزية الوزارية.
</p>
</div>
<button type="button" onclick="closeExamModal()" class="btn-primary">
العودة لدروس المادة ✓
</button>
</div>
</div>
</div>
<!-- Footer -->
<footer>
<div class="container">
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px;">
<span>منصة صَقِل التعليمية — الجيل الثاني للتعليم التفاعلي © 2026</span>
<span style="color: var(--accent-cyan);">الخادم متصل ومشفر بـ Workerman & WSS 🔒</span>
</div>
</div>
</footer>
<!-- Logic Controller with Full Real-time WebSocket Client -->
<script>
let wsSocket = null;
let countdownTimer = null;
let timerSeconds = 0;
let studentData = null;
let checkpointTriggered = false;
// Exam State
let currentExamIndex = 0;
let userSelectedAnswers = [];
const sampleQuestions = [
{
text: "إذا كان f(x) = (2x + 1)³، فما هي قيمة المشتقة f'(1)؟",
options: ["54 (3 × 2 × 3²)", "27", "18", "9"],
correct: 0,
topic: "قاعدة السلسلة"
},
{
text: "ما هي مشتقة الاقتران f(x) = tan(2x)؟",
options: ["2 sec²(2x)", "sec²(2x)", "2 tan(2x)", "-2 sec²(2x)"],
correct: 0,
topic: "المشتقات المثلثية"
},
{
text: "إذا كان ميل المماس لمنحنى عند النقطة (1, 3) يساوي 4، فما هي معادلة المماس؟",
options: ["y - 3 = 4(x - 1)", "y - 1 = 4(x - 3)", "y = 4x + 3", "y = 4x - 3"],
correct: 0,
topic: "التفسير الهندسي للمشتقة"
}
];
document.addEventListener('DOMContentLoaded', async () => {
const token = localStorage.getItem('saqel_student_jwt');
const cachedUser = localStorage.getItem('saqel_student_user');
if (token) {
if (cachedUser) {
try {
const u = JSON.parse(cachedUser);
renderDashboard(u);
} catch (e) {}
}
initWebSocket(token);
await checkStudentSession(token);
}
// Video player time listener for Socratic checkpoint
const video = document.getElementById('lesson_video_player');
if (video) {
video.addEventListener('timeupdate', () => {
const cur = Math.floor(video.currentTime);
const dur = Math.floor(video.duration || 596);
document.getElementById('video_time_display').textContent = `${formatTime(cur)} / ${formatTime(dur)}`;
// Trigger checkpoint at second 15 automatically once
if (cur === 15 && !checkpointTriggered) {
checkpointTriggered = true;
video.pause();
document.getElementById('socratic_quiz_modal').style.display = 'flex';
}
});
}
});
function formatTime(secs) {
const m = Math.floor(secs / 60).toString().padStart(2, '0');
const s = (secs % 60).toString().padStart(2, '0');
return `${m}:${s}`;
}
let wsPingInterval = null;
// 1. Initialize Real-time WebSocket (Workerman)
function initWebSocket(token) {
if (!token) return;
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${wsProtocol}//${window.location.host}/ws`;
if (wsSocket && (wsSocket.readyState === WebSocket.OPEN || wsSocket.readyState === WebSocket.CONNECTING)) {
return;
}
try {
wsSocket = new WebSocket(wsUrl);
wsSocket.onopen = () => {
wsSocket.send(JSON.stringify({
event: 'auth',
data: { token: token }
}));
// Heartbeat keep-alive every 20s
clearInterval(wsPingInterval);
wsPingInterval = setInterval(() => {
if (wsSocket && wsSocket.readyState === WebSocket.OPEN) {
wsSocket.send(JSON.stringify({ event: 'ping' }));
}
}, 20000);
};
wsSocket.onmessage = (event) => {
try {
const msg = JSON.parse(event.data);
handleIncomingWsEvent(msg);
} catch (e) {
console.error('WS Parse error:', e);
}
};
wsSocket.onclose = () => {
updateWsStatus(false);
clearInterval(wsPingInterval);
setTimeout(() => {
const currentToken = localStorage.getItem('saqel_student_jwt');
if (currentToken) initWebSocket(currentToken);
}, 5000);
};
wsSocket.onerror = (err) => {
updateWsStatus(false);
};
} catch (e) {
updateWsStatus(false);
}
}
function updateWsStatus(isOnline) {
const badge = document.getElementById('ws_status_badge');
const text = document.getElementById('ws_status_text');
if (!badge || !text) return;
if (isOnline) {
badge.className = 'ws-badge ws-online';
text.textContent = 'Workerman متصل 🟢';
} else {
badge.className = 'ws-badge ws-offline';
text.textContent = 'Workerman غير متصل 🔴';
}
}
// ==========================================
// Luxury Audio Chime & Real-time Notifications
// ==========================================
function playChimeNotification() {
try {
const ctx = new (window.AudioContext || window.webkitAudioContext)();
const now = ctx.currentTime;
const osc1 = ctx.createOscillator();
const gain1 = ctx.createGain();
osc1.type = 'sine';
osc1.frequency.setValueAtTime(587.33, now);
gain1.gain.setValueAtTime(0.2, now);
gain1.gain.exponentialRampToValueAtTime(0.001, now + 0.35);
osc1.connect(gain1);
gain1.connect(ctx.destination);
osc1.start(now);
osc1.stop(now + 0.35);
const osc2 = ctx.createOscillator();
const gain2 = ctx.createGain();
osc2.type = 'sine';
osc2.frequency.setValueAtTime(880.00, now + 0.09);
gain2.gain.setValueAtTime(0.25, now + 0.09);
gain2.gain.exponentialRampToValueAtTime(0.001, now + 0.55);
osc2.connect(gain2);
gain2.connect(ctx.destination);
osc2.start(now + 0.09);
osc2.stop(now + 0.55);
} catch (e) {
console.warn('Audio Context note:', e);
}
}
function showLuxuryToast(title, body, icon = '👨‍🏫') {
let container = document.getElementById('saqel_toast_stack');
if (!container) {
container = document.createElement('div');
container.id = 'saqel_toast_stack';
container.style.cssText = 'position: fixed; top: 24px; left: 24px; z-index: 99999; display: flex; flex-direction: column; gap: 10px; pointer-events: none; direction: rtl;';
document.body.appendChild(container);
}
const toast = document.createElement('div');
toast.style.cssText = `
pointer-events: auto;
min-width: 280px;
max-width: 380px;
background: rgba(22, 27, 34, 0.92);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border: 1px solid rgba(255, 255, 255, 0.15);
border-right: 4px solid var(--accent-color, #38BDF8);
border-radius: 16px;
padding: 14px 18px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6), 0 0 25px rgba(56, 189, 248, 0.2);
display: flex;
align-items: center;
gap: 12px;
animation: toastSlideIn 0.35s cubic-bezier(0.16, 1, 0.3, 1) forwards;
cursor: pointer;
`;
toast.innerHTML = `
<div style="font-size: 24px;">${icon}</div>
<div style="flex: 1; min-width: 0;">
<div style="font-size: 13px; font-weight: 700; color: #FFFFFF; margin-bottom: 2px;">${escapeHtml(title)}</div>
<div style="font-size: 12px; color: var(--text-muted, #94A3B8); white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">${escapeHtml(body)}</div>
</div>
`;
container.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateX(-30px)';
toast.style.transition = 'all 0.3s ease';
setTimeout(() => toast.remove(), 300);
}, 5000);
}
function triggerDesktopNotification(title, body) {
if ('Notification' in window && Notification.permission === 'granted') {
try {
new Notification(title, {
body: body,
icon: '/favicon.ico'
});
} catch (e) {}
}
}
function handleIncomingWsEvent(msg) {
const event = msg.event;
const data = msg.data;
if (event === 'authenticated') {
updateWsStatus(true);
} else if (event === 'chat_message') {
appendChatMessage({ ...data, is_mine: false });
playChimeNotification();
showLuxuryToast('رسالة جديدة من الأستاذ حمزة الغويري 👨‍🏫', data.message || 'رد المعلم على استفسارك');
triggerDesktopNotification('صَقِل — رد الأستاذ حمزة الغويري', data.message || 'رد المعلم على استفسارك');
} else if (event === 'drm_session_terminated') {
alert('⚠️ تم فتح هذا الحساب من متصفح آخر.');
handleLogout();
}
}
let activeTeacherId = null;
function switchStudentTab(tab) {
document.getElementById('tab_lessons_content').style.display = (tab === 'lessons') ? 'block' : 'none';
document.getElementById('tab_chat_content').style.display = (tab === 'chat') ? 'block' : 'none';
document.getElementById('tab_exams_content').style.display = (tab === 'exams') ? 'block' : 'none';
document.getElementById('tab_btn_lessons').className = (tab === 'lessons') ? 'tab-btn active' : 'tab-btn';
document.getElementById('tab_btn_chat').className = (tab === 'chat') ? 'tab-btn active' : 'tab-btn';
document.getElementById('tab_btn_exams').className = (tab === 'exams') ? 'tab-btn active' : 'tab-btn';
if (tab === 'chat') {
loadStudentChat();
}
}
async function loadStudentChat() {
const token = localStorage.getItem('saqel_student_jwt');
if (!token) return;
try {
const res = await fetch('/api/chat/conversations', {
headers: { 'Authorization': 'Bearer ' + token }
});
const data = await res.json();
if (res.ok && data.data && data.data.length > 0) {
const teacher = data.data.find(c => c.role === 'teacher') || data.data[0];
if (teacher) {
activeTeacherId = teacher.user_id;
const badge = document.getElementById('chat_teacher_badge_name');
if (badge) {
badge.textContent = `${teacher.full_name} (${teacher.specialization || 'مدرس المادة'}) (متصل الآن 🟢)`;
}
loadStudentMessages(activeTeacherId);
}
}
} catch (e) {
console.error('Load student chat notice:', e);
}
}
async function loadStudentMessages(teacherId) {
const token = localStorage.getItem('saqel_student_jwt');
if (!token || !teacherId) return;
try {
const res = await fetch(`/api/chat/messages?other_user_id=${teacherId}`, {
headers: { 'Authorization': 'Bearer ' + token }
});
const data = await res.json();
if (res.ok && data.data?.messages) {
const messagesArea = document.getElementById('student_chat_messages');
messagesArea.innerHTML = '';
if (data.data.messages.length === 0) {
messagesArea.innerHTML = '<div style="text-align: center; color: var(--text-muted); font-size: 13px; margin-top: 40px;">مرحباً بك! اكتب سؤالك هنا وسيجيبك أستاذ المادة فوراً.</div>';
} else {
data.data.messages.forEach(msg => appendChatMessage(msg));
}
}
} catch (e) {
console.error('Fetch student messages notice:', e);
}
}
// Real-time Chat Rendering
function appendChatMessage(msg) {
const messagesArea = document.getElementById('student_chat_messages');
const isMine = msg.is_mine || false;
const div = document.createElement('div');
div.className = `chat-msg ${isMine ? 'student' : 'teacher'}`;
div.innerHTML = `
<div>${msg.message}</div>
<span class="chat-time">${msg.created_at || 'الآن'}</span>
`;
messagesArea.appendChild(div);
messagesArea.scrollTop = messagesArea.scrollHeight;
}
async function sendStudentMessage() {
const input = document.getElementById('student_chat_input');
const message = input.value.trim();
if (!message) return;
input.value = '';
const token = localStorage.getItem('saqel_student_jwt');
// If activeTeacherId is null, try to find teacher
if (!activeTeacherId) {
await loadStudentChat();
}
const targetId = activeTeacherId || 1;
// 1. Send via REST API to guarantee database persistence
try {
const res = await fetch('/api/chat/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + token
},
body: JSON.stringify({
receiver_id: targetId,
message: message,
message_type: 'text'
})
});
const data = await res.json();
if (res.ok && data.data) {
appendChatMessage({ message: message, is_mine: true, created_at: 'الآن' });
}
} catch (err) {
console.error('REST chat send notice:', err);
appendChatMessage({ message: message, is_mine: true, created_at: 'الآن' });
}
// 2. Also broadcast over WebSocket
if (wsSocket && wsSocket.readyState === WebSocket.OPEN) {
wsSocket.send(JSON.stringify({
event: 'chat_send',
data: {
receiver_id: targetId,
message: message,
message_type: 'text'
}
}));
}
}
// Exam Taking & AI Diagnostic Modal
function startExamModal(title, level) {
document.getElementById('exam_title_display').textContent = title;
document.getElementById('exam_question_container').style.display = 'block';
document.getElementById('exam_result_container').style.display = 'none';
document.getElementById('exam_taking_modal').style.display = 'flex';
currentExamIndex = 0;
userSelectedAnswers = [];
renderCurrentExamQuestion();
}
function closeExamModal() {
document.getElementById('exam_taking_modal').style.display = 'none';
}
function renderCurrentExamQuestion() {
const q = sampleQuestions[currentExamIndex];
document.getElementById('exam_progress_text').textContent = `السؤال ${currentExamIndex + 1} من ${sampleQuestions.length} • (${q.topic})`;
document.getElementById('exam_question_text').textContent = q.text;
const list = document.getElementById('exam_options_list');
list.innerHTML = '';
q.options.forEach((opt, idx) => {
const btn = document.createElement('button');
btn.type = 'button';
btn.className = 'quiz-option-btn';
btn.textContent = `${['أ', 'ب', 'ج', 'د'][idx]}) ${opt}`;
btn.onclick = () => selectExamOption(idx);
list.appendChild(btn);
});
const nextBtn = document.getElementById('btn_next_question');
nextBtn.textContent = (currentExamIndex === sampleQuestions.length - 1) ? 'إنهاء الامتحان وحساب التقييم ✨' : 'السؤال التالي ←';
}
function selectExamOption(idx) {
userSelectedAnswers[currentExamIndex] = idx;
const btns = document.querySelectorAll('#exam_options_list .quiz-option-btn');
btns.forEach((b, i) => {
if (i === idx) {
b.style.borderColor = 'var(--accent-blue)';
b.style.background = 'rgba(0, 113, 227, 0.2)';
} else {
b.style.borderColor = 'rgba(255,255,255,0.15)';
b.style.background = 'rgba(255,255,255,0.05)';
}
});
}
function handleNextQuestion() {
if (userSelectedAnswers[currentExamIndex] === undefined) {
alert('يرجى اختيار إجابة للمتابعة');
return;
}
if (currentExamIndex < sampleQuestions.length - 1) {
currentExamIndex++;
renderCurrentExamQuestion();
} else {
finishExamEvaluation();
}
}
function finishExamEvaluation() {
document.getElementById('exam_question_container').style.display = 'none';
document.getElementById('exam_result_container').style.display = 'block';
// Calculate score
let correctCount = 0;
userSelectedAnswers.forEach((ans, idx) => {
if (ans === sampleQuestions[idx].correct) correctCount++;
});
const pct = Math.round((correctCount / sampleQuestions.length) * 100);
document.getElementById('result_score_display').textContent = `${pct}%`;
document.getElementById('result_readiness_display').textContent = `+2.4%`;
document.getElementById('readiness_gauge_val').textContent = `89.9%`;
document.getElementById('readiness_status_text').textContent = `مستواك ممتاز ومتقدم (+4.8% إجمالي)`;
}
// Auth Logic
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';
}
function clearAlerts() {
document.getElementById('alert_box_error').style.display = 'none';
document.getElementById('alert_box_success').style.display = 'none';
}
function switchToPhoneStep() {
document.getElementById('step_otp_container').style.display = 'none';
document.getElementById('step_phone_container').style.display = 'block';
clearAlerts();
clearInterval(countdownTimer);
}
function switchToOtpStep(maskedPhone) {
document.getElementById('step_phone_container').style.display = 'none';
document.getElementById('step_otp_container').style.display = 'block';
document.getElementById('otp_target_display').textContent = maskedPhone;
const otpInput = document.getElementById('student_otp_code');
otpInput.value = '';
otpInput.focus();
startTimer(60);
}
function startTimer(seconds) {
timerSeconds = seconds;
clearInterval(countdownTimer);
const timerLabel = document.getElementById('resend_timer_label');
const resendBtn = document.getElementById('btn_resend');
resendBtn.style.display = 'none';
timerLabel.style.display = 'inline';
timerLabel.textContent = `إعادة الإرسال بعد (${timerSeconds}ث)`;
countdownTimer = setInterval(() => {
timerSeconds--;
if (timerSeconds > 0) {
timerLabel.textContent = `إعادة الإرسال بعد (${timerSeconds}ث)`;
} else {
clearInterval(countdownTimer);
timerLabel.style.display = 'none';
resendBtn.style.display = 'inline';
}
}, 1000);
}
async function handleSendOtp(e) {
if (e) e.preventDefault();
clearAlerts();
const phone = document.getElementById('student_phone').value.trim();
const fullName = document.getElementById('student_fullname').value.trim();
const sendBtn = document.getElementById('btn_send_otp');
const sendText = document.getElementById('btn_send_text');
const sendSpinner = document.getElementById('btn_send_spinner');
if (!phone) {
showError('يرجى إدخال رقم الهاتف');
return;
}
sendBtn.disabled = true;
sendText.textContent = 'جارٍ إرسال البطاقة عبر الواتساب...';
sendSpinner.style.display = 'block';
try {
const res = await fetch('/api/auth/otp/request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone_number: phone, role: 'student', full_name: fullName })
});
const data = await res.json();
if (res.ok && data.status === 'success') {
showSuccess(data.message || 'تم إرسال بطاقة التحقق عبر الواتساب!');
switchToOtpStep(data.data?.phone_masked || phone);
} else {
showError(data.message || 'فشل إرسال رمز التحقق.');
}
} catch (err) {
showError('حدث خطأ في الاتصال بالسيرفر.');
} finally {
sendBtn.disabled = false;
sendText.textContent = 'إرسال رمز التحقق (OTP) ←';
sendSpinner.style.display = 'none';
}
}
async function handleVerifyOtp(e) {
if (e) e.preventDefault();
clearAlerts();
const phone = document.getElementById('student_phone').value.trim();
const fullName = document.getElementById('student_fullname').value.trim();
const otpCode = document.getElementById('student_otp_code').value.trim();
const verifyBtn = document.getElementById('btn_verify_otp');
const verifyText = document.getElementById('btn_verify_text');
const verifySpinner = document.getElementById('btn_verify_spinner');
if (!otpCode || otpCode.length < 6) {
showError('يرجى إدخال رمز التحقق المكون من 6 أرقام');
return;
}
verifyBtn.disabled = true;
verifyText.textContent = 'جارٍ التحقق...';
verifySpinner.style.display = 'block';
try {
const res = await fetch('/api/auth/otp/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ phone_number: phone, otp: otpCode, role: 'student', full_name: fullName })
});
const data = await res.json();
if (res.ok && data.status === 'success' && data.data?.token) {
const token = data.data.token;
const user = data.data.user;
localStorage.setItem('saqel_student_jwt', token);
if (user) {
localStorage.setItem('saqel_student_user', JSON.stringify(user));
}
document.cookie = "saqel_student_jwt=" + token + "; path=/; max-age=2592000; SameSite=Lax";
initWebSocket(token);
renderDashboard(user);
await checkStudentSession(token);
} else {
showError(data.message || 'رمز التحقق غير صحيح');
}
} catch (err) {
showError('حدث خطأ في التحقق من الرمز.');
} finally {
verifyBtn.disabled = false;
verifyText.textContent = 'تأكيد الدخول للمنصة ✨';
verifySpinner.style.display = 'none';
}
}
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) {
studentData = data.data;
localStorage.setItem('saqel_student_user', JSON.stringify(data.data));
renderDashboard(data.data);
}
} catch (e) {
console.error('Session check notice:', e);
}
}
function renderDashboard(user) {
document.getElementById('auth_container').style.display = 'none';
document.getElementById('dashboard_container').style.display = 'block';
document.getElementById('switch_to_teacher_link').style.display = 'none';
document.getElementById('auth_user_badge').style.display = 'flex';
const displayName = user?.full_name || 'طالبنا المتميز';
document.getElementById('student_display_name').textContent = `أهلاً، ${displayName}`;
document.getElementById('dashboard_welcome_title').textContent = `أهلاً بك يا ${displayName} 🚀`;
document.getElementById('drm_watermark_text').textContent = `SAQEL-DRM • ${user?.phone_number || '079XXXXXXX'}`;
loadStudentChat();
}
function handleLogout() {
localStorage.removeItem('saqel_student_jwt');
localStorage.removeItem('saqel_student_user');
document.cookie = "saqel_student_jwt=; path=/; max-age=0;";
if (wsSocket) wsSocket.close();
location.reload();
}
</script>
</body>
</html>
HTML;
}
}