diff --git a/backend/app/Controllers/ChatController.php b/backend/app/Controllers/ChatController.php index 346f620..4f60933 100644 --- a/backend/app/Controllers/ChatController.php +++ b/backend/app/Controllers/ChatController.php @@ -11,71 +11,74 @@ use App\Core\Validator; class ChatController { /** - * List all active conversations for the authenticated user (Student or Teacher) + * List all active conversations and available contacts (Student or Teacher) * GET /api/chat/conversations */ public function getConversations(Request $request, Response $response): void { - $userId = $request->user_id; - - // Find all distinct other parties in conversations - $sql = " - SELECT - u.id as user_id, - u.uuid as user_uuid, - u.full_name as encrypted_name, - u.role as user_role, - tp.specialization, - m.last_message, - m.last_message_type, - m.last_message_at, - COALESCE(unread.unread_count, 0) as unread_count - FROM ( - SELECT - CASE WHEN sender_id = ? THEN receiver_id ELSE sender_id END as other_user_id, - message as last_message, - message_type as last_message_type, - created_at as last_message_at, - ROW_NUMBER() OVER ( - PARTITION BY (CASE WHEN sender_id = ? THEN receiver_id ELSE sender_id END) - ORDER BY created_at DESC - ) as rn - FROM chat_messages - WHERE sender_id = ? OR receiver_id = ? - ) m - JOIN users u ON u.id = m.other_user_id - LEFT JOIN teacher_profiles tp ON tp.user_id = u.id - LEFT JOIN ( - SELECT sender_id, COUNT(*) as unread_count - FROM chat_messages - WHERE receiver_id = ? AND is_read = 0 - GROUP BY sender_id - ) unread ON unread.sender_id = u.id - WHERE m.rn = 1 - ORDER BY m.last_message_at DESC - "; + $userId = (int)$request->user_id; + $userRole = (string)($request->role ?? 'student'); + // 1. Fetch users with conversation history + $activeConvs = []; try { - $rows = Database::select($sql, [$userId, $userId, $userId, $userId, $userId]); - } catch (\Exception $e) { - // Fallback for MySQL setups without window functions $rows = Database::select(" SELECT u.id as user_id, u.uuid as user_uuid, u.full_name as encrypted_name, u.role as user_role, - tp.specialization + tp.specialization, + m.message as last_message, + m.message_type as last_message_type, + m.created_at as last_message_at, + (SELECT COUNT(*) FROM chat_messages WHERE sender_id = u.id AND receiver_id = ? AND is_read = 0) as unread_count FROM users u LEFT JOIN teacher_profiles tp ON tp.user_id = u.id - WHERE u.id IN ( - SELECT DISTINCT CASE WHEN sender_id = ? THEN receiver_id ELSE sender_id END - FROM chat_messages - WHERE sender_id = ? OR receiver_id = ? + JOIN chat_messages m ON m.id = ( + SELECT MAX(id) FROM chat_messages + WHERE (sender_id = ? AND receiver_id = u.id) OR (sender_id = u.id AND receiver_id = ?) ) - ", [$userId, $userId, $userId]); + WHERE u.id != ? + ORDER BY m.created_at DESC + ", [$userId, $userId, $userId, $userId]); + $activeConvs = $rows ?: []; + } catch (\Exception $e) { + error_log("Active convs query note: " . $e->getMessage()); } + $existingUserIds = array_column($activeConvs, 'user_id'); + $existingUserIds[] = $userId; + + // 2. Fetch other users who haven't started a conversation yet + $targetRole = ($userRole === 'teacher' || $userRole === 'super_admin') ? 'student' : 'teacher'; + + $inPlaceholders = implode(',', array_fill(0, count($existingUserIds), '?')); + $otherUsers = []; + try { + $otherUsers = Database::select(" + SELECT + u.id as user_id, + u.uuid as user_uuid, + u.full_name as encrypted_name, + u.role as user_role, + tp.specialization, + '' as last_message, + 'text' as last_message_type, + NULL as last_message_at, + 0 as unread_count + FROM users u + LEFT JOIN teacher_profiles tp ON tp.user_id = u.id + WHERE u.role = ? AND u.id NOT IN ({$inPlaceholders}) + ORDER BY u.id DESC + LIMIT 50 + ", array_merge([$targetRole], $existingUserIds)); + } catch (\Exception $e) { + error_log("Other users query note: " . $e->getMessage()); + } + + $allRows = array_merge($activeConvs, $otherUsers ?: []); + $conversations = array_map(function ($row) { $rawName = (string)($row['encrypted_name'] ?? ''); $decryptedName = Security::decrypt($rawName) ?: $rawName; @@ -83,15 +86,15 @@ class ChatController return [ 'user_id' => (int)$row['user_id'], 'user_uuid' => $row['user_uuid'] ?? '', - 'full_name' => $decryptedName ?: ($row['user_role'] === 'teacher' ? 'أستاذ المادة' : 'طالب صَقِل'), + 'full_name' => $decryptedName ?: ($row['user_role'] === 'teacher' ? 'الأستاذ حمزة الغويري' : 'طالب صَقِل'), 'role' => $row['user_role'] ?? 'student', - 'specialization' => $row['specialization'] ?? null, - 'last_message' => $row['last_message'] ?? '', + 'specialization' => $row['specialization'] ?? ($row['user_role'] === 'teacher' ? 'الرياضيات العلمي' : null), + 'last_message' => $row['last_message'] ?: ($row['user_role'] === 'teacher' ? 'متاح للإجابة عن استفساراتك' : 'طالب جديد — اضغط لبدء المحادثة'), 'last_message_type' => $row['last_message_type'] ?? 'text', 'last_message_at' => $row['last_message_at'] ?? null, 'unread_count' => (int)($row['unread_count'] ?? 0), ]; - }, $rows); + }, $allRows); $response->json([ 'status' => 'success', diff --git a/backend/app/Views/StudentPortal.php b/backend/app/Views/StudentPortal.php index c6a5a9f..2e479c7 100644 --- a/backend/app/Views/StudentPortal.php +++ b/backend/app/Views/StudentPortal.php @@ -960,6 +960,8 @@ class StudentPortal } } + 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'; @@ -968,51 +970,59 @@ class StudentPortal 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'; - } - // Socratic In-Video Checkpoint Execution - function triggerCheckpointDemo() { - const video = document.getElementById('lesson_video_player'); - if (video) video.pause(); - document.getElementById('socratic_quiz_modal').style.display = 'flex'; - } - - function handleCheckpointAnswer(btn, isCorrect) { - const feedback = document.getElementById('checkpoint_feedback'); - const btns = document.querySelectorAll('.quiz-option-btn'); - btns.forEach(b => b.disabled = true); - - if (isCorrect) { - btn.className = 'quiz-option-btn correct'; - feedback.style.color = '#34D399'; - feedback.textContent = '✓ إجابة ممتازة وصحيحة! تم تثبيت المفهوم المعرفي بنجاح.'; - feedback.style.display = 'block'; - setTimeout(() => { - document.getElementById('socratic_quiz_modal').style.display = 'none'; - btns.forEach(b => { b.disabled = false; b.className = 'quiz-option-btn'; }); - feedback.style.display = 'none'; - const video = document.getElementById('lesson_video_player'); - if (video) video.play(); - }, 1800); - } else { - btn.className = 'quiz-option-btn wrong'; - feedback.style.color = '#F87171'; - feedback.textContent = '✗ إجابة غير دقيقة — يتم تطبيق الإرجاع السقراطي 45 ثانية لإعادة الشرح.'; - feedback.style.display = 'block'; - setTimeout(() => { - document.getElementById('socratic_quiz_modal').style.display = 'none'; - btns.forEach(b => { b.disabled = false; b.className = 'quiz-option-btn'; }); - feedback.style.display = 'none'; - const video = document.getElementById('lesson_video_player'); - if (video) { - video.currentTime = Math.max(0, video.currentTime - 45); - video.play(); - } - }, 2200); + if (tab === 'chat') { + loadStudentChat(); } } - // Real-time Chat + 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 = '
مرحباً بك! اكتب سؤالك هنا وسيجيبك أستاذ المادة فوراً.
'; + } 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; @@ -1032,31 +1042,47 @@ class StudentPortal if (!message) return; input.value = ''; - const teacherId = 1; + const token = localStorage.getItem('saqel_student_jwt'); + + // If activeTeacherId is null, try to find teacher + if (!activeTeacherId) { + await loadStudentChat(); + } + const targetId = activeTeacherId || 1; - if (wsSocket && wsSocket.readyState === WebSocket.OPEN) { - wsSocket.send(JSON.stringify({ - event: 'chat_send', - data: { - receiver_id: teacherId, - message: message, - message_type: 'text' - } - })); - } else { - const token = localStorage.getItem('saqel_student_jwt'); - await fetch('/api/chat/messages', { + // 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: teacherId, - message: message + receiver_id: targetId, + message: message, + message_type: 'text' }) }); - appendChatMessage({ message: message, is_mine: true }); + 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' + } + })); } } diff --git a/backend/app/Views/TeacherPortal.php b/backend/app/Views/TeacherPortal.php index 677d7a6..13d2c41 100644 --- a/backend/app/Views/TeacherPortal.php +++ b/backend/app/Views/TeacherPortal.php @@ -680,7 +680,7 @@ class TeacherPortal function renderConversations(convs) { const container = document.getElementById('conv_list_container'); if (!convs || convs.length === 0) { - container.innerHTML = '
لا توجد رسائل سابقة. الطلاب المسجلون سيظهرون هنا فور سؤالهم.
'; + container.innerHTML = '
لا يوجد طلاب مسجلون حالياً. سيظهر الطلاب هنا فور تسجيلهم.
'; return; } @@ -693,13 +693,18 @@ class TeacherPortal
${c.last_message || 'محادثة جديدة'}
`).join(''); + + // Auto-select first conversation if none is selected + if (!activeRecipientId && convs.length > 0) { + selectConversation(convs[0].user_id, convs[0].full_name); + } } async function selectConversation(userId, name) { activeRecipientId = userId; document.getElementById('active_chat_user_name').textContent = name; - loadConversations(); - + const items = document.querySelectorAll('.conv-item'); + const token = localStorage.getItem('saqel_teacher_jwt'); try { const res = await fetch(`/api/chat/messages?other_user_id=${userId}`, { @@ -709,7 +714,11 @@ class TeacherPortal if (res.ok && data.status === 'success') { const messagesArea = document.getElementById('chat_messages_area'); messagesArea.innerHTML = ''; - data.data.messages.forEach(msg => appendChatMessage(msg)); + if (data.data.messages.length === 0) { + messagesArea.innerHTML = '
بدء المحادثة مع الطالب ' + name + ' — اكتب رسالتك بالأسفل.
'; + } else { + data.data.messages.forEach(msg => appendChatMessage(msg)); + } } } catch (e) { console.error('Fetch messages error:', e); @@ -738,8 +747,32 @@ class TeacherPortal } input.value = ''; + const token = localStorage.getItem('saqel_teacher_jwt'); - // Try sending via Workerman WebSocket first + // 1. Send via REST API to ensure DB write + try { + const res = await fetch('/api/chat/messages', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + token + }, + body: JSON.stringify({ + receiver_id: activeRecipientId, + 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 teacher chat notice:', err); + appendChatMessage({ message: message, is_mine: true, created_at: 'الآن' }); + } + + // 2. Broadcast via WebSocket if (wsSocket && wsSocket.readyState === WebSocket.OPEN) { wsSocket.send(JSON.stringify({ event: 'chat_send', @@ -749,21 +782,6 @@ class TeacherPortal message_type: 'text' } })); - } else { - // Fallback to REST API - const token = localStorage.getItem('saqel_teacher_jwt'); - await fetch('/api/chat/messages', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ' + token - }, - body: JSON.stringify({ - receiver_id: activeRecipientId, - message: message - }) - }); - appendChatMessage({ message: message, is_mine: true }); } } diff --git a/backend/app/bootstrap.php b/backend/app/bootstrap.php index 2a8d6ca..82ef962 100644 --- a/backend/app/bootstrap.php +++ b/backend/app/bootstrap.php @@ -25,19 +25,30 @@ spl_autoload_register(function ($class) { } }); -// 2. Load Environment Variables (Fixed Dedicated CloudPanel Path: /home/intaleqapp-saqel/.env) +// 2. Load Environment Variables with Multi-Path Fallback try { - // Fixed CloudPanel root path on server, with local fallback for local development - $env_file = file_exists('/home/intaleqapp-saqel/.env') - ? '/home/intaleqapp-saqel/.env' - : APP_ROOT . '/.env'; + $candidatePaths = [ + '/home/intaleqapp-saqel/.env', + '/home/intaleqapp-saqel/htdocs/saqel.intaleqapp.com/.env', + '/home/intaleqapp-saqel/htdocs/saqel.intaleqapp.com/saqel/.env', + '/home/intaleqapp-saqel/htdocs/saqel.intaleqapp.com/saqel/backend/.env', + APP_ROOT . '/.env', + APP_ROOT . '/../.env' + ]; - if (file_exists($env_file)) { - define('LOADED_ENV_PATH', $env_file); - \App\Core\Env::load($env_file); - } else { - define('LOADED_ENV_PATH', 'NOT_FOUND: ' . $env_file); - error_log("⚠️ [Env Warning] .env file not found at: {$env_file}"); + $loaded = false; + foreach ($candidatePaths as $path) { + if (file_exists($path)) { + define('LOADED_ENV_PATH', $path); + \App\Core\Env::load($path); + $loaded = true; + break; + } + } + + if (!$loaded) { + define('LOADED_ENV_PATH', 'NOT_FOUND'); + error_log("⚠️ [Env Warning] .env file not found in any candidate path"); } } catch (\Exception $e) { error_log('Env Load Error: ' . $e->getMessage());