'أحمد محمد الغويري', 'phone' => '962790000002'], ['name' => 'عمر بني صخر (علمي)', 'phone' => '962790000003'], ['name' => 'سارة المشاقبة (توجيهي 2008)', 'phone' => '962790000004'] ]; foreach ($sampleStudents as $s) { $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)); $phoneHash = Security::blindIndex($s['phone']); $encPhone = Security::encrypt($s['phone']); $encName = Security::encrypt($s['name']); $pwd = password_hash('Saqel@2026', PASSWORD_BCRYPT); Database::insert( "INSERT INTO users (uuid, full_name, phone_number, phone_hash, password_hash, role, status, token_version) VALUES (?, ?, ?, ?, ?, 'student', 'active', 1)", [$uuid, $encName, $encPhone, $phoneHash, $pwd] ); } } } catch (\Exception $e) { error_log("Seed users note: " . $e->getMessage()); } } /** * List all active conversations and available contacts (Student or Teacher) * GET /api/chat/conversations */ public function getConversations(Request $request, Response $response): void { self::ensureSeedUsers(); $userId = (int)$request->user_id; $userRole = (string)($request->role ?? 'student'); // 1. Fetch users with conversation history $activeConvs = []; try { $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, 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 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 = ?) ) 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; return [ 'user_id' => (int)$row['user_id'], 'user_uuid' => $row['user_uuid'] ?? '', 'full_name' => $decryptedName ?: ($row['user_role'] === 'teacher' ? 'الأستاذ حمزة الغويري' : 'طالب صَقِل'), 'role' => $row['user_role'] ?? 'student', '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), ]; }, $allRows); $response->json([ 'status' => 'success', 'data' => $conversations ]); } /** * Get chat message history between authenticated user and another user * GET /api/chat/messages?other_user_id=123&course_id=456 */ public function getMessages(Request $request, Response $response): void { $userId = $request->user_id; $queryParams = $request->getQueryParams(); $otherUserId = (int)($queryParams['other_user_id'] ?? $queryParams['user_id'] ?? 0); $courseId = !empty($queryParams['course_id']) ? (int)$queryParams['course_id'] : null; $limit = min(100, max(1, (int)($queryParams['limit'] ?? 50))); if (!$otherUserId) { $response->status(400)->json([ 'status' => 'error', 'message' => 'معرف الطرف الآخر (other_user_id) مطلوب' ]); return; } // Fetch other user info $otherUser = Database::selectOne("SELECT id, uuid, full_name, role FROM users WHERE id = ? LIMIT 1", [$otherUserId]); if (!$otherUser) { $response->status(404)->json(['status' => 'error', 'message' => 'المستخدم الآخر غير موجود']); return; } $rawOtherName = (string)($otherUser['full_name'] ?? ''); $otherUserName = Security::decrypt($rawOtherName) ?: $rawOtherName; // Fetch messages bidirectional $sql = " SELECT id, uuid, sender_id, receiver_id, course_id, lesson_id, message, message_type, media_url, is_read, read_at, created_at FROM chat_messages WHERE ((sender_id = ? AND receiver_id = ?) OR (sender_id = ? AND receiver_id = ?)) "; $params = [$userId, $otherUserId, $otherUserId, $userId]; if ($courseId) { $sql .= " AND (course_id = ? OR course_id IS NULL)"; $params[] = $courseId; } $sql .= " ORDER BY created_at ASC LIMIT ?"; $params[] = $limit; $messages = Database::select($sql, $params); // Mark unread messages sent by other user to me as read Database::query( "UPDATE chat_messages SET is_read = 1, read_at = NOW() WHERE sender_id = ? AND receiver_id = ? AND is_read = 0", [$otherUserId, $userId] ); $formattedMessages = array_map(function ($msg) use ($userId) { return [ 'id' => (int)$msg['id'], 'uuid' => $msg['uuid'], 'is_mine' => ((int)$msg['sender_id'] === (int)$userId), 'sender_id' => (int)$msg['sender_id'], 'receiver_id' => (int)$msg['receiver_id'], 'course_id' => $msg['course_id'] ? (int)$msg['course_id'] : null, 'lesson_id' => $msg['lesson_id'] ? (int)$msg['lesson_id'] : null, 'message' => $msg['message'], 'message_type' => $msg['message_type'], 'media_url' => $msg['media_url'], 'is_read' => (bool)$msg['is_read'], 'read_at' => $msg['read_at'], 'created_at' => $msg['created_at'], ]; }, $messages); $response->json([ 'status' => 'success', 'data' => [ 'other_user' => [ 'id' => (int)$otherUser['id'], 'uuid' => $otherUser['uuid'], 'full_name' => $otherUserName, 'role' => $otherUser['role'], ], 'messages' => $formattedMessages ] ]); } /** * Send a new chat message (Text, Voice Note, Image, File) * POST /api/chat/messages */ public function sendMessage(Request $request, Response $response): void { $userId = $request->user_id; $body = $request->getBody(); $validator = new Validator(); $isValid = $validator->validate($body, [ 'receiver_id' => 'required', 'message' => 'required' ]); if (!$isValid) { $response->status(400)->json([ 'status' => 'error', 'message' => 'معرف المستقبل ونص الرسالة مطلوبان', 'errors' => $validator->getErrors() ]); return; } $receiverId = (int)$body['receiver_id']; $messageText = trim((string)$body['message']); $messageType = (string)($body['message_type'] ?? 'text'); $mediaUrl = !empty($body['media_url']) ? trim((string)$body['media_url']) : null; $courseId = !empty($body['course_id']) ? (int)$body['course_id'] : null; $lessonId = !empty($body['lesson_id']) ? (int)$body['lesson_id'] : null; if (!in_array($messageType, ['text', 'voice', 'image', 'file'], true)) { $messageType = 'text'; } // Validate receiver exists, or fallback to counterpart role automatically $receiver = Database::selectOne("SELECT id, role FROM users WHERE id = ? LIMIT 1", [$receiverId]); if (!$receiver) { $targetRole = ($request->role === 'teacher') ? 'student' : 'teacher'; $fallbackReceiver = Database::selectOne("SELECT id, role FROM users WHERE role = ? ORDER BY id ASC LIMIT 1", [$targetRole]); if (!$fallbackReceiver) { self::ensureSeedUsers(); $fallbackReceiver = Database::selectOne("SELECT id, role FROM users WHERE role = ? ORDER BY id ASC LIMIT 1", [$targetRole]); } if ($fallbackReceiver) { $receiverId = (int)$fallbackReceiver['id']; $receiver = $fallbackReceiver; } else { $response->status(404)->json([ 'status' => 'error', 'message' => 'المستخدم المستلم غير موجود' ]); return; } } $uuid = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000, mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) ); $messageId = Database::insert( "INSERT INTO chat_messages (uuid, sender_id, receiver_id, course_id, lesson_id, message, message_type, media_url, is_read) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)", [$uuid, $userId, $receiverId, $courseId, $lessonId, $messageText, $messageType, $mediaUrl] ); $response->status(201)->json([ 'status' => 'success', 'message' => 'تم إرسال الرسالة بنجاح', 'data' => [ 'id' => $messageId, 'uuid' => $uuid, 'sender_id' => $userId, 'receiver_id' => $receiverId, 'is_mine' => true, 'course_id' => $courseId, 'lesson_id' => $lessonId, 'message' => $messageText, 'message_type' => $messageType, 'media_url' => $mediaUrl, 'is_read' => false, 'created_at' => date('Y-m-d H:i:s'), ] ]); } /** * Mark all messages in a conversation as read * POST /api/chat/read */ public function markAsRead(Request $request, Response $response): void { $userId = $request->user_id; $body = $request->getBody(); $senderId = (int)($body['sender_id'] ?? 0); if (!$senderId) { $response->status(400)->json([ 'status' => 'error', 'message' => 'معرف المرسل مطلوب' ]); return; } Database::query( "UPDATE chat_messages SET is_read = 1, read_at = NOW() WHERE sender_id = ? AND receiver_id = ? AND is_read = 0", [$senderId, $userId] ); $response->json([ 'status' => 'success', 'message' => 'تم تحديث حالة القراءة' ]); } /** * Get total unread count for badge notification * GET /api/chat/unread-count */ public function getUnreadCount(Request $request, Response $response): void { $userId = $request->user_id; $row = Database::selectOne( "SELECT COUNT(*) as unread_total FROM chat_messages WHERE receiver_id = ? AND is_read = 0", [$userId] ); $response->json([ 'status' => 'success', 'data' => [ 'unread_count' => (int)($row['unread_total'] ?? 0) ] ]); } }