384 lines
16 KiB
PHP
384 lines
16 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers;
|
|
|
|
use App\Core\Request;
|
|
use App\Core\Response;
|
|
use App\Core\Database;
|
|
use App\Core\Security;
|
|
use App\Core\Validator;
|
|
|
|
class ChatController
|
|
{
|
|
/**
|
|
* Auto-provisions the official teacher and sample students if database is empty
|
|
*/
|
|
private static function ensureSeedUsers(): void
|
|
{
|
|
try {
|
|
// Ensure default Teacher: الأستاذ حمزة الغويري
|
|
$teacher = Database::selectOne("SELECT id FROM users WHERE role = 'teacher' LIMIT 1");
|
|
if (!$teacher) {
|
|
$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));
|
|
$phone = '962790000001';
|
|
$phoneHash = Security::blindIndex($phone);
|
|
$encPhone = Security::encrypt($phone);
|
|
$encName = Security::encrypt('الأستاذ حمزة الغويري');
|
|
$pwd = password_hash('Saqel@2026', PASSWORD_BCRYPT);
|
|
$tId = Database::insert(
|
|
"INSERT INTO users (uuid, full_name, phone_number, phone_hash, password_hash, role, status, token_version) VALUES (?, ?, ?, ?, ?, 'teacher', 'active', 1)",
|
|
[$uuid, $encName, $encPhone, $phoneHash, $pwd]
|
|
);
|
|
Database::insert(
|
|
"INSERT INTO teacher_profiles (user_id, specialization, revenue_share_pct, contract_type) VALUES (?, 'الرياضيات العلمي', 50.00, 'exclusive')",
|
|
[$tId]
|
|
);
|
|
}
|
|
|
|
// Ensure default sample students if no students exist
|
|
$student = Database::selectOne("SELECT id FROM users WHERE role = 'student' LIMIT 1");
|
|
if (!$student) {
|
|
$sampleStudents = [
|
|
['name' => 'أحمد محمد الغويري', '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)
|
|
]
|
|
]);
|
|
}
|
|
}
|