ваше сообщение коммита
This commit is contained in:
@@ -932,10 +932,10 @@ router.get('/history', requireAuth, async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
// Если нужен только подсчет
|
// Если нужен только подсчет
|
||||||
if (countOnly) {
|
if (countOnly) {
|
||||||
let countQuery = 'SELECT COUNT(*) FROM messages WHERE user_id = $1';
|
let countQuery = 'SELECT COUNT(*) FROM messages WHERE user_id = $1 AND message_type = $2';
|
||||||
let countParams = [userId];
|
let countParams = [userId, 'user_chat'];
|
||||||
if (conversationId) {
|
if (conversationId) {
|
||||||
countQuery += ' AND conversation_id = $2';
|
countQuery += ' AND conversation_id = $3';
|
||||||
countParams.push(conversationId);
|
countParams.push(conversationId);
|
||||||
}
|
}
|
||||||
const countResult = await db.getQuery()(countQuery, countParams);
|
const countResult = await db.getQuery()(countQuery, countParams);
|
||||||
@@ -944,7 +944,10 @@ router.get('/history', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Загружаем сообщения через encryptedDb
|
// Загружаем сообщения через encryptedDb
|
||||||
const whereConditions = { user_id: userId };
|
const whereConditions = {
|
||||||
|
user_id: userId,
|
||||||
|
message_type: 'user_chat' // Фильтруем только публичные сообщения
|
||||||
|
};
|
||||||
if (conversationId) {
|
if (conversationId) {
|
||||||
whereConditions.conversation_id = conversationId;
|
whereConditions.conversation_id = conversationId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,24 +41,25 @@ router.get('/', async (req, res) => {
|
|||||||
let result;
|
let result;
|
||||||
if (conversationId) {
|
if (conversationId) {
|
||||||
result = await db.getQuery()(
|
result = await db.getQuery()(
|
||||||
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content, decrypt_text(channel_encrypted, $2) as channel, decrypt_text(role_encrypted, $2) as role, decrypt_text(direction_encrypted, $2) as direction, created_at, decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data
|
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content, decrypt_text(channel_encrypted, $2) as channel, decrypt_text(role_encrypted, $2) as role, decrypt_text(direction_encrypted, $2) as direction, created_at, decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data, message_type
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE conversation_id = $1
|
WHERE conversation_id = $1 AND message_type = 'user_chat'
|
||||||
ORDER BY created_at ASC`,
|
ORDER BY created_at ASC`,
|
||||||
[conversationId, encryptionKey]
|
[conversationId, encryptionKey]
|
||||||
);
|
);
|
||||||
} else if (userId) {
|
} else if (userId) {
|
||||||
result = await db.getQuery()(
|
result = await db.getQuery()(
|
||||||
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content, decrypt_text(channel_encrypted, $2) as channel, decrypt_text(role_encrypted, $2) as role, decrypt_text(direction_encrypted, $2) as direction, created_at, decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data
|
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $2) as sender_type, decrypt_text(content_encrypted, $2) as content, decrypt_text(channel_encrypted, $2) as channel, decrypt_text(role_encrypted, $2) as role, decrypt_text(direction_encrypted, $2) as direction, created_at, decrypt_text(attachment_filename_encrypted, $2) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $2) as attachment_mimetype, attachment_size, attachment_data, message_type
|
||||||
FROM messages
|
FROM messages
|
||||||
WHERE user_id = $1
|
WHERE user_id = $1 AND message_type = 'user_chat'
|
||||||
ORDER BY created_at ASC`,
|
ORDER BY created_at ASC`,
|
||||||
[userId, encryptionKey]
|
[userId, encryptionKey]
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
result = await db.getQuery()(
|
result = await db.getQuery()(
|
||||||
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $1) as sender_type, decrypt_text(content_encrypted, $1) as content, decrypt_text(channel_encrypted, $1) as channel, decrypt_text(role_encrypted, $1) as role, decrypt_text(direction_encrypted, $1) as direction, created_at, decrypt_text(attachment_filename_encrypted, $1) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $1) as attachment_mimetype, attachment_size, attachment_data
|
`SELECT id, user_id, decrypt_text(sender_type_encrypted, $1) as sender_type, decrypt_text(content_encrypted, $1) as content, decrypt_text(channel_encrypted, $1) as channel, decrypt_text(role_encrypted, $1) as role, decrypt_text(direction_encrypted, $1) as direction, created_at, decrypt_text(attachment_filename_encrypted, $1) as attachment_filename, decrypt_text(attachment_mimetype_encrypted, $1) as attachment_mimetype, attachment_size, attachment_data, message_type
|
||||||
FROM messages
|
FROM messages
|
||||||
|
WHERE message_type = 'user_chat'
|
||||||
ORDER BY created_at ASC`,
|
ORDER BY created_at ASC`,
|
||||||
[encryptionKey]
|
[encryptionKey]
|
||||||
);
|
);
|
||||||
@@ -73,6 +74,34 @@ router.get('/', async (req, res) => {
|
|||||||
router.post('/', async (req, res) => {
|
router.post('/', async (req, res) => {
|
||||||
const { user_id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data } = req.body;
|
const { user_id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data } = req.body;
|
||||||
|
|
||||||
|
// Определяем тип сообщения
|
||||||
|
const senderId = req.user && req.user.id;
|
||||||
|
let messageType = 'user_chat'; // по умолчанию для публичных сообщений
|
||||||
|
|
||||||
|
if (senderId) {
|
||||||
|
// Проверяем, является ли отправитель админом
|
||||||
|
const senderCheck = await db.getQuery()(
|
||||||
|
'SELECT role FROM users WHERE id = $1',
|
||||||
|
[senderId]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (senderCheck.rows.length > 0 && (senderCheck.rows[0].role === 'editor' || senderCheck.rows[0].role === 'readonly')) {
|
||||||
|
// Если отправитель админ, проверяем получателя
|
||||||
|
const recipientCheck = await db.getQuery()(
|
||||||
|
'SELECT role FROM users WHERE id = $1',
|
||||||
|
[user_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Если получатель тоже админ, то это приватное сообщение
|
||||||
|
if (recipientCheck.rows.length > 0 && (recipientCheck.rows[0].role === 'editor' || recipientCheck.rows[0].role === 'readonly')) {
|
||||||
|
messageType = 'admin_chat';
|
||||||
|
} else {
|
||||||
|
// Если получатель обычный пользователь, то это публичное сообщение
|
||||||
|
messageType = 'user_chat';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Получаем ключ шифрования
|
// Получаем ключ шифрования
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
@@ -120,29 +149,72 @@ router.post('/', async (req, res) => {
|
|||||||
return res.status(400).json({ error: 'У пользователя не привязан кошелёк. Сообщение не отправлено.' });
|
return res.status(400).json({ error: 'У пользователя не привязан кошелёк. Сообщение не отправлено.' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 1. Проверяем, есть ли беседа для user_id
|
let conversation;
|
||||||
|
|
||||||
|
if (messageType === 'admin_chat') {
|
||||||
|
// Для админских сообщений ищем приватную беседу через conversation_participants
|
||||||
|
let conversationResult = await db.getQuery()(`
|
||||||
|
SELECT c.id
|
||||||
|
FROM conversations c
|
||||||
|
INNER JOIN conversation_participants cp1 ON cp1.conversation_id = c.id AND cp1.user_id = $1
|
||||||
|
INNER JOIN conversation_participants cp2 ON cp2.conversation_id = c.id AND cp2.user_id = $2
|
||||||
|
WHERE c.conversation_type = 'admin_chat'
|
||||||
|
LIMIT 1
|
||||||
|
`, [senderId, user_id]);
|
||||||
|
|
||||||
|
if (conversationResult.rows.length === 0) {
|
||||||
|
// Создаем новую приватную беседу между админами
|
||||||
|
const title = `Приватная беседа ${senderId} - ${user_id}`;
|
||||||
|
const newConv = await db.getQuery()(
|
||||||
|
'INSERT INTO conversations (user_id, title_encrypted, conversation_type, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), $4, NOW(), NOW()) RETURNING *',
|
||||||
|
[user_id, title, encryptionKey, 'admin_chat']
|
||||||
|
);
|
||||||
|
conversation = newConv.rows[0];
|
||||||
|
|
||||||
|
// Добавляем участников в беседу
|
||||||
|
await db.getQuery()(
|
||||||
|
'INSERT INTO conversation_participants (conversation_id, user_id) VALUES ($1, $2), ($1, $3)',
|
||||||
|
[conversation.id, senderId, user_id]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
conversation = { id: conversationResult.rows[0].id };
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Для обычных пользовательских сообщений используем старую логику с user_id
|
||||||
let conversationResult = await db.getQuery()(
|
let conversationResult = await db.getQuery()(
|
||||||
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
|
'SELECT id, user_id, created_at, updated_at, decrypt_text(title_encrypted, $2) as title FROM conversations WHERE user_id = $1 ORDER BY updated_at DESC, created_at DESC LIMIT 1',
|
||||||
[user_id, encryptionKey]
|
[user_id, encryptionKey]
|
||||||
);
|
);
|
||||||
let conversation;
|
|
||||||
if (conversationResult.rows.length === 0) {
|
if (conversationResult.rows.length === 0) {
|
||||||
// 2. Если нет — создаём новую беседу
|
// Создаем новую беседу
|
||||||
const title = `Чат с пользователем ${user_id}`;
|
const title = `Чат с пользователем ${user_id}`;
|
||||||
const newConv = await db.getQuery()(
|
const newConv = await db.getQuery()(
|
||||||
'INSERT INTO conversations (user_id, title_encrypted, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), NOW(), NOW()) RETURNING *',
|
'INSERT INTO conversations (user_id, title_encrypted, conversation_type, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), $4, NOW(), NOW()) RETURNING *',
|
||||||
[user_id, title, encryptionKey]
|
[user_id, title, encryptionKey, 'user_chat']
|
||||||
);
|
);
|
||||||
conversation = newConv.rows[0];
|
conversation = newConv.rows[0];
|
||||||
} else {
|
} else {
|
||||||
conversation = conversationResult.rows[0];
|
conversation = conversationResult.rows[0];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 3. Сохраняем сообщение с conversation_id
|
// 3. Сохраняем сообщение с conversation_id
|
||||||
const result = await db.getQuery()(
|
let result;
|
||||||
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, created_at, attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data)
|
if (messageType === 'admin_chat') {
|
||||||
VALUES ($1,$2,encrypt_text($3,$12),encrypt_text($4,$12),encrypt_text($5,$12),encrypt_text($6,$12),encrypt_text($7,$12),NOW(),encrypt_text($8,$12),encrypt_text($9,$12),$10,$11) RETURNING *`,
|
// Для админских сообщений добавляем sender_id
|
||||||
[user_id, conversation.id, sender_type, content, channel, role, direction, attachment_filename, attachment_mimetype, attachment_size, attachment_data, encryptionKey]
|
result = await db.getQuery()(
|
||||||
|
`INSERT INTO messages (conversation_id, user_id, sender_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at, attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data)
|
||||||
|
VALUES ($1,$2,$3,encrypt_text($4,$13),encrypt_text($5,$13),encrypt_text($6,$13),encrypt_text($7,$13),encrypt_text($8,$13),$9,NOW(),encrypt_text($10,$13),encrypt_text($11,$13),$12,$14) RETURNING *`,
|
||||||
|
[conversation.id, user_id, senderId, sender_type, content, channel, role, direction, messageType, attachment_filename, attachment_mimetype, attachment_size, attachment_data, encryptionKey]
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
// Для обычных сообщений без sender_id
|
||||||
|
result = await db.getQuery()(
|
||||||
|
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at, attachment_filename_encrypted, attachment_mimetype_encrypted, attachment_size, attachment_data)
|
||||||
|
VALUES ($1,$2,encrypt_text($3,$12),encrypt_text($4,$12),encrypt_text($5,$12),encrypt_text($6,$12),encrypt_text($7,$12),$13,NOW(),encrypt_text($8,$12),encrypt_text($9,$12),$10,$11) RETURNING *`,
|
||||||
|
[user_id, conversation.id, sender_type, content, channel, role, direction, messageType, attachment_filename, attachment_mimetype, attachment_size, attachment_data, encryptionKey]
|
||||||
|
);
|
||||||
|
}
|
||||||
// 4. Если это исходящее сообщение для Telegram — отправляем через бота
|
// 4. Если это исходящее сообщение для Telegram — отправляем через бота
|
||||||
if (channel === 'telegram' && direction === 'out') {
|
if (channel === 'telegram' && direction === 'out') {
|
||||||
try {
|
try {
|
||||||
@@ -186,7 +258,10 @@ router.post('/', async (req, res) => {
|
|||||||
// console.error('[messages.js] Ошибка отправки email:', err);
|
// console.error('[messages.js] Ошибка отправки email:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Отправляем WebSocket уведомления
|
||||||
broadcastMessagesUpdate();
|
broadcastMessagesUpdate();
|
||||||
|
|
||||||
res.json({ success: true, message: result.rows[0] });
|
res.json({ success: true, message: result.rows[0] });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
res.status(500).json({ error: 'DB error', details: e.message });
|
res.status(500).json({ error: 'DB error', details: e.message });
|
||||||
@@ -199,7 +274,8 @@ router.post('/mark-read', async (req, res) => {
|
|||||||
// console.log('[DEBUG] /mark-read req.user:', req.user);
|
// console.log('[DEBUG] /mark-read req.user:', req.user);
|
||||||
// console.log('[DEBUG] /mark-read req.body:', req.body);
|
// console.log('[DEBUG] /mark-read req.body:', req.body);
|
||||||
const adminId = req.user && req.user.id;
|
const adminId = req.user && req.user.id;
|
||||||
const { userId, lastReadAt } = req.body;
|
const { userId, lastReadAt, messageType = 'user_chat' } = req.body;
|
||||||
|
|
||||||
if (!adminId) {
|
if (!adminId) {
|
||||||
// console.error('[ERROR] /mark-read: adminId (req.user.id) is missing');
|
// console.error('[ERROR] /mark-read: adminId (req.user.id) is missing');
|
||||||
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
||||||
@@ -208,12 +284,30 @@ router.post('/mark-read', async (req, res) => {
|
|||||||
// console.error('[ERROR] /mark-read: userId or lastReadAt missing');
|
// console.error('[ERROR] /mark-read: userId or lastReadAt missing');
|
||||||
return res.status(400).json({ error: 'userId and lastReadAt required' });
|
return res.status(400).json({ error: 'userId and lastReadAt required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Логика зависит от типа сообщения
|
||||||
|
if (messageType === 'user_chat') {
|
||||||
|
// Обновляем глобальный статус для всех админов
|
||||||
|
await db.query(`
|
||||||
|
INSERT INTO global_read_status (user_id, last_read_at, updated_by_admin_id)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT (user_id) DO UPDATE SET
|
||||||
|
last_read_at = EXCLUDED.last_read_at,
|
||||||
|
updated_by_admin_id = EXCLUDED.updated_by_admin_id,
|
||||||
|
updated_at = NOW()
|
||||||
|
`, [userId, lastReadAt, adminId]);
|
||||||
|
} else if (messageType === 'admin_chat') {
|
||||||
|
// Обновляем персональный статус для админских сообщений
|
||||||
await db.query(`
|
await db.query(`
|
||||||
INSERT INTO admin_read_messages (admin_id, user_id, last_read_at)
|
INSERT INTO admin_read_messages (admin_id, user_id, last_read_at)
|
||||||
VALUES ($1, $2, $3)
|
VALUES ($1, $2, $3)
|
||||||
ON CONFLICT (admin_id, user_id) DO UPDATE SET last_read_at = EXCLUDED.last_read_at
|
ON CONFLICT (admin_id, user_id) DO UPDATE SET last_read_at = EXCLUDED.last_read_at
|
||||||
`, [adminId, userId, lastReadAt]);
|
`, [adminId, userId, lastReadAt]);
|
||||||
res.json({ success: true });
|
} else {
|
||||||
|
return res.status(400).json({ error: 'Invalid messageType. Must be "user_chat" or "admin_chat"' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({ success: true, messageType });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.error('[ERROR] /mark-read:', e);
|
// console.error('[ERROR] /mark-read:', e);
|
||||||
res.status(500).json({ error: e.message });
|
res.status(500).json({ error: e.message });
|
||||||
@@ -227,11 +321,24 @@ router.get('/read-status', async (req, res) => {
|
|||||||
// console.log('[DEBUG] /read-status req.session:', req.session);
|
// console.log('[DEBUG] /read-status req.session:', req.session);
|
||||||
// console.log('[DEBUG] /read-status req.session.userId:', req.session && req.session.userId);
|
// console.log('[DEBUG] /read-status req.session.userId:', req.session && req.session.userId);
|
||||||
const adminId = req.user && req.user.id;
|
const adminId = req.user && req.user.id;
|
||||||
|
const { messageType = 'user_chat' } = req.query;
|
||||||
|
|
||||||
if (!adminId) {
|
if (!adminId) {
|
||||||
// console.error('[ERROR] /read-status: adminId (req.user.id) is missing');
|
// console.error('[ERROR] /read-status: adminId (req.user.id) is missing');
|
||||||
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
||||||
}
|
}
|
||||||
const result = await db.query('SELECT user_id, last_read_at FROM admin_read_messages WHERE admin_id = $1', [adminId]);
|
|
||||||
|
let result;
|
||||||
|
if (messageType === 'user_chat') {
|
||||||
|
// Возвращаем глобальный статус для сообщений с пользователями
|
||||||
|
result = await db.query('SELECT user_id, last_read_at FROM global_read_status');
|
||||||
|
} else if (messageType === 'admin_chat') {
|
||||||
|
// Возвращаем персональный статус для админских сообщений
|
||||||
|
result = await db.query('SELECT user_id, last_read_at FROM admin_read_messages WHERE admin_id = $1', [adminId]);
|
||||||
|
} else {
|
||||||
|
return res.status(400).json({ error: 'Invalid messageType. Must be "user_chat" or "admin_chat"' });
|
||||||
|
}
|
||||||
|
|
||||||
// console.log('[DEBUG] /read-status SQL result:', result.rows);
|
// console.log('[DEBUG] /read-status SQL result:', result.rows);
|
||||||
const map = {};
|
const map = {};
|
||||||
for (const row of result.rows) {
|
for (const row of result.rows) {
|
||||||
@@ -346,9 +453,9 @@ router.post('/broadcast', async (req, res) => {
|
|||||||
await emailBot.sendEmail(email, 'Новое сообщение', content);
|
await emailBot.sendEmail(email, 'Новое сообщение', content);
|
||||||
// Сохраняем в messages с conversation_id
|
// Сохраняем в messages с conversation_id
|
||||||
await db.getQuery()(
|
await db.getQuery()(
|
||||||
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, created_at)
|
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at)
|
||||||
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), NOW())`,
|
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), $9, NOW())`,
|
||||||
[user_id, conversation.id, 'admin', content, 'email', 'user', 'out', encryptionKey]
|
[user_id, conversation.id, 'admin', content, 'email', 'user', 'out', 'user_chat', encryptionKey]
|
||||||
);
|
);
|
||||||
results.push({ channel: 'email', status: 'sent' });
|
results.push({ channel: 'email', status: 'sent' });
|
||||||
sent = true;
|
sent = true;
|
||||||
@@ -363,9 +470,9 @@ router.post('/broadcast', async (req, res) => {
|
|||||||
const bot = await telegramBot.getBot();
|
const bot = await telegramBot.getBot();
|
||||||
await bot.telegram.sendMessage(telegram, content);
|
await bot.telegram.sendMessage(telegram, content);
|
||||||
await db.getQuery()(
|
await db.getQuery()(
|
||||||
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, created_at)
|
`INSERT INTO messages (user_id, conversation_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at)
|
||||||
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), NOW())`,
|
VALUES ($1, $2, encrypt_text($3, $8), encrypt_text($4, $8), encrypt_text($5, $8), encrypt_text($6, $8), encrypt_text($7, $8), $9, NOW())`,
|
||||||
[user_id, conversation.id, 'admin', content, 'telegram', 'user', 'out', encryptionKey]
|
[user_id, conversation.id, 'admin', content, 'telegram', 'user', 'out', 'user_chat', encryptionKey]
|
||||||
);
|
);
|
||||||
results.push({ channel: 'telegram', status: 'sent' });
|
results.push({ channel: 'telegram', status: 'sent' });
|
||||||
sent = true;
|
sent = true;
|
||||||
@@ -435,4 +542,254 @@ router.delete('/history/:userId', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// POST /api/messages/admin/send - отправка сообщения админу
|
||||||
|
router.post('/admin/send', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const adminId = req.user && req.user.id;
|
||||||
|
const { recipientAdminId, content } = req.body;
|
||||||
|
|
||||||
|
if (!adminId) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
||||||
|
}
|
||||||
|
if (!recipientAdminId || !content) {
|
||||||
|
return res.status(400).json({ error: 'recipientAdminId and content required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем ключ шифрования
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||||
|
if (fs.existsSync(keyPath)) {
|
||||||
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
|
}
|
||||||
|
} catch (keyError) {
|
||||||
|
console.error('Error reading encryption key:', keyError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ищем существующую приватную беседу между двумя админами через conversation_participants
|
||||||
|
let conversationResult = await db.getQuery()(`
|
||||||
|
SELECT c.id
|
||||||
|
FROM conversations c
|
||||||
|
INNER JOIN conversation_participants cp1 ON cp1.conversation_id = c.id AND cp1.user_id = $1
|
||||||
|
INNER JOIN conversation_participants cp2 ON cp2.conversation_id = c.id AND cp2.user_id = $2
|
||||||
|
WHERE c.conversation_type = 'admin_chat'
|
||||||
|
LIMIT 1
|
||||||
|
`, [adminId, recipientAdminId]);
|
||||||
|
|
||||||
|
let conversationId;
|
||||||
|
if (conversationResult.rows.length === 0) {
|
||||||
|
// Создаем новую приватную беседу между админами
|
||||||
|
const title = `Приватная беседа ${adminId} - ${recipientAdminId}`;
|
||||||
|
const newConv = await db.getQuery()(
|
||||||
|
'INSERT INTO conversations (user_id, title_encrypted, conversation_type, created_at, updated_at) VALUES ($1, encrypt_text($2, $3), $4, NOW(), NOW()) RETURNING id',
|
||||||
|
[recipientAdminId, title, encryptionKey, 'admin_chat']
|
||||||
|
);
|
||||||
|
conversationId = newConv.rows[0].id;
|
||||||
|
|
||||||
|
// Добавляем участников в беседу
|
||||||
|
await db.getQuery()(
|
||||||
|
'INSERT INTO conversation_participants (conversation_id, user_id) VALUES ($1, $2), ($1, $3)',
|
||||||
|
[conversationId, adminId, recipientAdminId]
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(`[admin/send] Создана новая беседа ${conversationId} между ${adminId} и ${recipientAdminId}`);
|
||||||
|
} else {
|
||||||
|
conversationId = conversationResult.rows[0].id;
|
||||||
|
console.log(`[admin/send] Найдена существующая беседа ${conversationId} между ${adminId} и ${recipientAdminId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем сообщение с типом 'admin_chat'
|
||||||
|
const result = await db.getQuery()(
|
||||||
|
`INSERT INTO messages (conversation_id, user_id, sender_id, sender_type_encrypted, content_encrypted, channel_encrypted, role_encrypted, direction_encrypted, message_type, created_at)
|
||||||
|
VALUES ($1, $2, $3, encrypt_text($4, $9), encrypt_text($5, $9), encrypt_text($6, $9), encrypt_text($7, $9), encrypt_text($8, $9), $10, NOW()) RETURNING id`,
|
||||||
|
[conversationId, recipientAdminId, adminId, 'admin', content, 'web', 'admin', 'out', encryptionKey, 'admin_chat']
|
||||||
|
);
|
||||||
|
|
||||||
|
// Отправляем WebSocket уведомления
|
||||||
|
broadcastMessagesUpdate();
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
messageId: result.rows[0].id,
|
||||||
|
conversationId,
|
||||||
|
messageType: 'admin_chat'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[ERROR] /admin/send:', e);
|
||||||
|
res.status(500).json({ error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/messages/admin/conversations - получить личные чаты админа
|
||||||
|
router.get('/admin/conversations', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const adminId = req.user && req.user.id;
|
||||||
|
|
||||||
|
if (!adminId) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем список админов, с которыми есть переписка
|
||||||
|
const conversations = await db.query(`
|
||||||
|
SELECT DISTINCT
|
||||||
|
CASE
|
||||||
|
WHEN sender_type = 'admin' AND user_id != $1 THEN user_id
|
||||||
|
ELSE sender_id
|
||||||
|
END as admin_id,
|
||||||
|
MAX(created_at) as last_message_at
|
||||||
|
FROM messages
|
||||||
|
WHERE message_type = 'admin_chat'
|
||||||
|
AND (user_id = $1 OR sender_id = $1)
|
||||||
|
GROUP BY admin_id
|
||||||
|
ORDER BY last_message_at DESC
|
||||||
|
`, [adminId]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
conversations: conversations.rows
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[ERROR] /admin/conversations:', e);
|
||||||
|
res.status(500).json({ error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/messages/admin/contacts - получить админов для приватного чата
|
||||||
|
router.get('/admin/contacts', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const adminId = req.user && req.user.id;
|
||||||
|
|
||||||
|
if (!adminId) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем ключ шифрования
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||||
|
if (fs.existsSync(keyPath)) {
|
||||||
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
|
}
|
||||||
|
} catch (keyError) {
|
||||||
|
console.error('Error reading encryption key:', keyError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем всех пользователей, с которыми есть приватные беседы через conversation_participants
|
||||||
|
const adminContacts = await db.getQuery()(`
|
||||||
|
SELECT DISTINCT
|
||||||
|
other_user.id,
|
||||||
|
COALESCE(
|
||||||
|
decrypt_text(other_user.first_name_encrypted, $2),
|
||||||
|
decrypt_text(other_user.username_encrypted, $2),
|
||||||
|
'Пользователь ' || other_user.id
|
||||||
|
) as name,
|
||||||
|
'admin@system' as email,
|
||||||
|
CASE
|
||||||
|
WHEN other_user.role = 'editor' THEN 'admin'
|
||||||
|
WHEN other_user.role = 'readonly' THEN 'admin'
|
||||||
|
ELSE 'user'
|
||||||
|
END as contact_type,
|
||||||
|
MAX(m.created_at) as last_message_at,
|
||||||
|
COUNT(m.id) as message_count
|
||||||
|
FROM conversations c
|
||||||
|
INNER JOIN conversation_participants cp_current ON cp_current.conversation_id = c.id AND cp_current.user_id = $1
|
||||||
|
INNER JOIN conversation_participants cp_other ON cp_other.conversation_id = c.id AND cp_other.user_id != $1
|
||||||
|
INNER JOIN users other_user ON other_user.id = cp_other.user_id
|
||||||
|
LEFT JOIN messages m ON m.conversation_id = c.id AND m.message_type = 'admin_chat'
|
||||||
|
WHERE c.conversation_type = 'admin_chat'
|
||||||
|
GROUP BY
|
||||||
|
other_user.id,
|
||||||
|
other_user.first_name_encrypted,
|
||||||
|
other_user.username_encrypted,
|
||||||
|
other_user.role
|
||||||
|
ORDER BY MAX(m.created_at) DESC
|
||||||
|
`, [adminId, encryptionKey]);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
contacts: adminContacts.rows.map(contact => ({
|
||||||
|
...contact,
|
||||||
|
created_at: contact.last_message_at, // Используем время последнего сообщения как время создания для сортировки
|
||||||
|
telegram: null,
|
||||||
|
wallet: null
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[ERROR] /admin/contacts:', e);
|
||||||
|
res.status(500).json({ error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /api/messages/admin/:adminId - получить сообщения с конкретным админом
|
||||||
|
router.get('/admin/:adminId', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const currentAdminId = req.user && req.user.id;
|
||||||
|
const { adminId } = req.params;
|
||||||
|
|
||||||
|
if (!currentAdminId) {
|
||||||
|
return res.status(401).json({ error: 'Unauthorized: adminId missing' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем ключ шифрования
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
let encryptionKey = 'default-key';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const keyPath = path.join(__dirname, '../ssl/keys/full_db_encryption.key');
|
||||||
|
if (fs.existsSync(keyPath)) {
|
||||||
|
encryptionKey = fs.readFileSync(keyPath, 'utf8').trim();
|
||||||
|
}
|
||||||
|
} catch (keyError) {
|
||||||
|
console.error('Error reading encryption key:', keyError);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем сообщения из приватной беседы между админами через conversation_participants
|
||||||
|
const result = await db.getQuery()(
|
||||||
|
`SELECT m.id, m.user_id, m.sender_id,
|
||||||
|
decrypt_text(m.sender_type_encrypted, $3) as sender_type,
|
||||||
|
decrypt_text(m.content_encrypted, $3) as content,
|
||||||
|
decrypt_text(m.channel_encrypted, $3) as channel,
|
||||||
|
decrypt_text(m.role_encrypted, $3) as role,
|
||||||
|
decrypt_text(m.direction_encrypted, $3) as direction,
|
||||||
|
m.created_at, m.message_type,
|
||||||
|
-- Получаем wallet адреса отправителей (расшифровываем provider_id_encrypted)
|
||||||
|
CASE
|
||||||
|
WHEN sender_ui.provider_encrypted = encrypt_text('wallet', $3)
|
||||||
|
THEN decrypt_text(sender_ui.provider_id_encrypted, $3)
|
||||||
|
ELSE 'Админ'
|
||||||
|
END as sender_wallet,
|
||||||
|
CASE
|
||||||
|
WHEN recipient_ui.provider_encrypted = encrypt_text('wallet', $3)
|
||||||
|
THEN decrypt_text(recipient_ui.provider_id_encrypted, $3)
|
||||||
|
ELSE 'Админ'
|
||||||
|
END as recipient_wallet
|
||||||
|
FROM messages m
|
||||||
|
INNER JOIN conversations c ON c.id = m.conversation_id
|
||||||
|
INNER JOIN conversation_participants cp1 ON cp1.conversation_id = c.id AND cp1.user_id = $1
|
||||||
|
INNER JOIN conversation_participants cp2 ON cp2.conversation_id = c.id AND cp2.user_id = $2
|
||||||
|
LEFT JOIN user_identities sender_ui ON sender_ui.user_id = m.sender_id
|
||||||
|
LEFT JOIN user_identities recipient_ui ON recipient_ui.user_id = m.user_id
|
||||||
|
WHERE m.message_type = 'admin_chat' AND c.conversation_type = 'admin_chat'
|
||||||
|
ORDER BY m.created_at ASC`,
|
||||||
|
[currentAdminId, adminId, encryptionKey]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
messages: result.rows,
|
||||||
|
messageType: 'admin_chat'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[ERROR] /admin/:adminId:', e);
|
||||||
|
res.status(500).json({ error: e.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
@@ -150,7 +150,12 @@ router.get('/', requireAuth, async (req, res, next) => {
|
|||||||
WHEN u.last_name_encrypted IS NULL OR u.last_name_encrypted = '' THEN NULL
|
WHEN u.last_name_encrypted IS NULL OR u.last_name_encrypted = '' THEN NULL
|
||||||
ELSE decrypt_text(u.last_name_encrypted, $${idx++})
|
ELSE decrypt_text(u.last_name_encrypted, $${idx++})
|
||||||
END as last_name,
|
END as last_name,
|
||||||
u.created_at, u.preferred_language, u.is_blocked,
|
u.created_at, u.preferred_language, u.is_blocked, u.role,
|
||||||
|
CASE
|
||||||
|
WHEN u.role = 'editor' THEN 'admin'
|
||||||
|
WHEN u.role = 'readonly' THEN 'admin'
|
||||||
|
ELSE 'user'
|
||||||
|
END as contact_type,
|
||||||
(SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('email', $${idx++}) LIMIT 1) AS email,
|
(SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('email', $${idx++}) LIMIT 1) AS email,
|
||||||
(SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('telegram', $${idx++}) LIMIT 1) AS telegram,
|
(SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('telegram', $${idx++}) LIMIT 1) AS telegram,
|
||||||
(SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('wallet', $${idx++}) LIMIT 1) AS wallet
|
(SELECT decrypt_text(provider_id_encrypted, $${idx++}) FROM user_identities WHERE user_id = u.id AND provider_encrypted = encrypt_text('wallet', $${idx++}) LIMIT 1) AS wallet
|
||||||
@@ -219,7 +224,9 @@ router.get('/', requireAuth, async (req, res, next) => {
|
|||||||
wallet: u.wallet || null,
|
wallet: u.wallet || null,
|
||||||
created_at: u.created_at,
|
created_at: u.created_at,
|
||||||
preferred_language: u.preferred_language || [],
|
preferred_language: u.preferred_language || [],
|
||||||
is_blocked: u.is_blocked || false
|
is_blocked: u.is_blocked || false,
|
||||||
|
contact_type: u.contact_type || 'user',
|
||||||
|
role: u.role || 'user'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json({ success: true, contacts });
|
res.json({ success: true, contacts });
|
||||||
|
|||||||
@@ -10,26 +10,96 @@
|
|||||||
* GitHub: https://github.com/HB3-ACCELERATOR
|
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const encryptedDb = require('./encryptedDatabaseService');
|
const db = require('../db');
|
||||||
|
|
||||||
async function deleteUserById(userId) {
|
async function deleteUserById(userId) {
|
||||||
// console.log('[DELETE] Вызван deleteUserById для userId:', userId);
|
console.log('[DELETE] Вызван deleteUserById для userId:', userId);
|
||||||
try {
|
try {
|
||||||
// console.log('[DELETE] Начинаем удаление user_identities для userId:', userId);
|
// Удаляем в правильном порядке (сначала зависимые таблицы, потом основную)
|
||||||
const resIdentities = await encryptedDb.deleteData('user_identities', { user_id: userId });
|
|
||||||
// console.log('[DELETE] Удалено user_identities:', resIdentities.length);
|
|
||||||
|
|
||||||
// console.log('[DELETE] Начинаем удаление messages для userId:', userId);
|
// 1. Удаляем user_identities
|
||||||
const resMessages = await encryptedDb.deleteData('messages', { user_id: userId });
|
console.log('[DELETE] Начинаем удаление user_identities для userId:', userId);
|
||||||
// console.log('[DELETE] Удалено messages:', resMessages.length);
|
const resIdentities = await db.getQuery()(
|
||||||
|
'DELETE FROM user_identities WHERE user_id = $1 RETURNING id',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
console.log('[DELETE] Удалено user_identities:', resIdentities.rows.length);
|
||||||
|
|
||||||
// console.log('[DELETE] Начинаем удаление пользователя из users:', userId);
|
// 2. Удаляем messages
|
||||||
const result = await encryptedDb.deleteData('users', { id: userId });
|
console.log('[DELETE] Начинаем удаление messages для userId:', userId);
|
||||||
// console.log('[DELETE] Результат удаления пользователя:', result.length, result);
|
const resMessages = await db.getQuery()(
|
||||||
|
'DELETE FROM messages WHERE user_id = $1 RETURNING id',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
console.log('[DELETE] Удалено messages:', resMessages.rows.length);
|
||||||
|
|
||||||
return result.length;
|
// 3. Удаляем conversations
|
||||||
|
console.log('[DELETE] Начинаем удаление conversations для userId:', userId);
|
||||||
|
const resConversations = await db.getQuery()(
|
||||||
|
'DELETE FROM conversations WHERE user_id = $1 RETURNING id',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
console.log('[DELETE] Удалено conversations:', resConversations.rows.length);
|
||||||
|
|
||||||
|
// 4. Удаляем conversation_participants
|
||||||
|
console.log('[DELETE] Начинаем удаление conversation_participants для userId:', userId);
|
||||||
|
const resParticipants = await db.getQuery()(
|
||||||
|
'DELETE FROM conversation_participants WHERE user_id = $1 RETURNING id',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
console.log('[DELETE] Удалено conversation_participants:', resParticipants.rows.length);
|
||||||
|
|
||||||
|
// 5. Удаляем user_preferences
|
||||||
|
console.log('[DELETE] Начинаем удаление user_preferences для userId:', userId);
|
||||||
|
const resPreferences = await db.getQuery()(
|
||||||
|
'DELETE FROM user_preferences WHERE user_id = $1 RETURNING id',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
console.log('[DELETE] Удалено user_preferences:', resPreferences.rows.length);
|
||||||
|
|
||||||
|
// 6. Удаляем verification_codes
|
||||||
|
console.log('[DELETE] Начинаем удаление verification_codes для userId:', userId);
|
||||||
|
const resCodes = await db.getQuery()(
|
||||||
|
'DELETE FROM verification_codes WHERE user_id = $1 RETURNING id',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
console.log('[DELETE] Удалено verification_codes:', resCodes.rows.length);
|
||||||
|
|
||||||
|
// 7. Удаляем guest_user_mapping
|
||||||
|
console.log('[DELETE] Начинаем удаление guest_user_mapping для userId:', userId);
|
||||||
|
const resGuestMapping = await db.getQuery()(
|
||||||
|
'DELETE FROM guest_user_mapping WHERE user_id = $1 RETURNING id',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
console.log('[DELETE] Удалено guest_user_mapping:', resGuestMapping.rows.length);
|
||||||
|
|
||||||
|
// 8. Удаляем user_tag_links
|
||||||
|
console.log('[DELETE] Начинаем удаление user_tag_links для userId:', userId);
|
||||||
|
const resTagLinks = await db.getQuery()(
|
||||||
|
'DELETE FROM user_tag_links WHERE user_id = $1 RETURNING id',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
console.log('[DELETE] Удалено user_tag_links:', resTagLinks.rows.length);
|
||||||
|
|
||||||
|
// 9. Удаляем global_read_status
|
||||||
|
console.log('[DELETE] Начинаем удаление global_read_status для userId:', userId);
|
||||||
|
const resReadStatus = await db.getQuery()(
|
||||||
|
'DELETE FROM global_read_status WHERE user_id = $1 RETURNING user_id',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
console.log('[DELETE] Удалено global_read_status:', resReadStatus.rows.length);
|
||||||
|
|
||||||
|
// 10. Удаляем самого пользователя
|
||||||
|
console.log('[DELETE] Начинаем удаление пользователя из users:', userId);
|
||||||
|
const result = await db.getQuery()(
|
||||||
|
'DELETE FROM users WHERE id = $1 RETURNING id',
|
||||||
|
[userId]
|
||||||
|
);
|
||||||
|
console.log('[DELETE] Результат удаления пользователя:', result.rows.length, result.rows);
|
||||||
|
|
||||||
|
return result.rows.length;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// console.error('[DELETE] Ошибка при удалении пользователя:', e);
|
console.error('[DELETE] Ошибка при удалении пользователя:', e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="contact-table-modal">
|
<div class="contact-table-modal">
|
||||||
<div class="contact-table-header">
|
<div class="contact-table-header">
|
||||||
|
<el-button v-if="canRead" type="info" @click="goToPersonalMessages" style="margin-right: 1em;">Личные сообщения</el-button>
|
||||||
|
<el-button v-if="canEdit" type="success" :disabled="!selectedIds.length" @click="() => openChatForSelected()" style="margin-right: 1em;">Публичное сообщение</el-button>
|
||||||
|
<el-button v-if="canRead" type="warning" :disabled="!selectedIds.length" @click="() => openPrivateChatForSelected()" style="margin-right: 1em;">Приватное сообщение</el-button>
|
||||||
<el-button v-if="canManageSettings" type="info" :disabled="!selectedIds.length" @click="showBroadcastModal = true" style="margin-right: 1em;">Рассылка</el-button>
|
<el-button v-if="canManageSettings" type="info" :disabled="!selectedIds.length" @click="showBroadcastModal = true" style="margin-right: 1em;">Рассылка</el-button>
|
||||||
<el-button v-if="canDelete" type="warning" :disabled="!selectedIds.length" @click="deleteMessagesSelected" style="margin-right: 1em;">Удалить сообщения</el-button>
|
<el-button v-if="canDelete" type="warning" :disabled="!selectedIds.length" @click="deleteMessagesSelected" style="margin-right: 1em;">Удалить сообщения</el-button>
|
||||||
<el-button v-if="canDelete" type="danger" :disabled="!selectedIds.length" @click="deleteSelected" style="margin-right: 1em;">Удалить</el-button>
|
<el-button v-if="canDelete" type="danger" :disabled="!selectedIds.length" @click="deleteSelected" style="margin-right: 1em;">Удалить</el-button>
|
||||||
@@ -74,7 +77,8 @@
|
|||||||
<table class="contact-table">
|
<table class="contact-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th v-if="canEdit || canDelete || canManageSettings"><input type="checkbox" v-model="selectAll" @change="toggleSelectAll" /></th>
|
<th v-if="canRead"><input type="checkbox" v-model="selectAll" @change="toggleSelectAll" /></th>
|
||||||
|
<th>Тип</th>
|
||||||
<th>Имя</th>
|
<th>Имя</th>
|
||||||
<th>Email</th>
|
<th>Email</th>
|
||||||
<th>Telegram</th>
|
<th>Telegram</th>
|
||||||
@@ -85,7 +89,11 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="contact in contactsArray" :key="contact.id" :class="{ 'new-contact-row': newIds.includes(contact.id) }">
|
<tr v-for="contact in contactsArray" :key="contact.id" :class="{ 'new-contact-row': newIds.includes(contact.id) }">
|
||||||
<td v-if="canEdit || canDelete || canManageSettings"><input type="checkbox" v-model="selectedIds" :value="contact.id" /></td>
|
<td v-if="canRead"><input type="checkbox" v-model="selectedIds" :value="contact.id" /></td>
|
||||||
|
<td>
|
||||||
|
<span v-if="contact.contact_type === 'admin'" class="admin-badge">Админ</span>
|
||||||
|
<span v-else class="user-badge">Пользователь</span>
|
||||||
|
</td>
|
||||||
<td>{{ contact.name || '-' }}</td>
|
<td>{{ contact.name || '-' }}</td>
|
||||||
<td>{{ contact.email || '-' }}</td>
|
<td>{{ contact.email || '-' }}</td>
|
||||||
<td>{{ contact.telegram || '-' }}</td>
|
<td>{{ contact.telegram || '-' }}</td>
|
||||||
@@ -125,7 +133,7 @@ const contactsArray = ref([]); // теперь управляем вручную
|
|||||||
const newIds = computed(() => props.newContacts.map(c => c.id));
|
const newIds = computed(() => props.newContacts.map(c => c.id));
|
||||||
const newMsgUserIds = computed(() => props.newMessages.map(m => String(m.user_id)));
|
const newMsgUserIds = computed(() => props.newMessages.map(m => String(m.user_id)));
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { canEdit, canDelete, canManageSettings } = usePermissions();
|
const { canRead, canEdit, canDelete, canManageSettings } = usePermissions();
|
||||||
|
|
||||||
// Фильтры
|
// Фильтры
|
||||||
const filterSearch = ref('');
|
const filterSearch = ref('');
|
||||||
@@ -234,11 +242,21 @@ function buildQuery() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function fetchContacts() {
|
async function fetchContacts() {
|
||||||
|
try {
|
||||||
|
// Загружаем обычные контакты
|
||||||
let url = '/users';
|
let url = '/users';
|
||||||
const query = buildQuery();
|
const query = buildQuery();
|
||||||
if (query) url += '?' + query;
|
if (query) url += '?' + query;
|
||||||
|
console.log('[ContactTable] Загружаем контакты по URL:', url);
|
||||||
const res = await api.get(url);
|
const res = await api.get(url);
|
||||||
|
console.log('[ContactTable] Получен ответ:', res.data);
|
||||||
contactsArray.value = res.data.contacts || [];
|
contactsArray.value = res.data.contacts || [];
|
||||||
|
console.log('[ContactTable] Загружено контактов:', contactsArray.value.length);
|
||||||
|
console.log('[ContactTable] Первые 3 контакта:', contactsArray.value.slice(0, 3));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[ContactTable] Ошибка загрузки контактов:', error);
|
||||||
|
contactsArray.value = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onAnyFilterChange() {
|
function onAnyFilterChange() {
|
||||||
@@ -284,6 +302,59 @@ function onImported() {
|
|||||||
fetchContacts();
|
fetchContacts();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function openChatForSelected() {
|
||||||
|
if (selectedIds.value.length === 0) return;
|
||||||
|
|
||||||
|
// Берем первый выбранный контакт
|
||||||
|
const contactId = selectedIds.value[0];
|
||||||
|
|
||||||
|
// Находим контакт в списке
|
||||||
|
const contact = contactsArray.value.find(c => c.id === contactId);
|
||||||
|
if (!contact) return;
|
||||||
|
|
||||||
|
// Открываем чат с этим контактом (user_chat)
|
||||||
|
await showDetails(contact);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openPrivateChatForSelected(contact = null) {
|
||||||
|
let targetContact = contact;
|
||||||
|
|
||||||
|
// Если контакт не передан, берем из выбранных
|
||||||
|
if (!targetContact) {
|
||||||
|
if (selectedIds.value.length === 0) {
|
||||||
|
console.error('[ContactTable] Нет выбранных контактов');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Берем первый выбранный контакт
|
||||||
|
const contactId = selectedIds.value[0];
|
||||||
|
console.log('[ContactTable] Ищем контакт с ID:', contactId);
|
||||||
|
console.log('[ContactTable] Доступные контакты:', contactsArray.value.map(c => ({ id: c.id, name: c.name })));
|
||||||
|
|
||||||
|
// Находим контакт в списке
|
||||||
|
targetContact = contactsArray.value.find(c => c.id === contactId);
|
||||||
|
if (!targetContact) {
|
||||||
|
console.error('[ContactTable] Контакт не найден с ID:', contactId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что у контакта есть ID
|
||||||
|
if (!targetContact.id) {
|
||||||
|
console.error('[ContactTable] У контакта нет ID:', targetContact);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[ContactTable] Открываем приватный чат с контактом:', targetContact);
|
||||||
|
|
||||||
|
// Открываем приватный чат с этим контактом (admin_chat)
|
||||||
|
router.push({ name: 'admin-chat', params: { adminId: targetContact.id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToPersonalMessages() {
|
||||||
|
router.push({ name: 'personal-messages' });
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSelectAll() {
|
function toggleSelectAll() {
|
||||||
if (selectAll.value) {
|
if (selectAll.value) {
|
||||||
selectedIds.value = contactsArray.value.map(c => c.id);
|
selectedIds.value = contactsArray.value.map(c => c.id);
|
||||||
@@ -473,4 +544,22 @@ async function deleteMessagesSelected() {
|
|||||||
font-size: 1.2em;
|
font-size: 1.2em;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.admin-badge {
|
||||||
|
background: #e3f2fd;
|
||||||
|
color: #1976d2;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-badge {
|
||||||
|
background: #f3e5f5;
|
||||||
|
color: #7b1fa2;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -23,6 +23,18 @@
|
|||||||
message.hasError ? 'has-error' : '',
|
message.hasError ? 'has-error' : '',
|
||||||
]"
|
]"
|
||||||
>
|
>
|
||||||
|
<!-- Информация об отправителе для приватного чата -->
|
||||||
|
<div v-if="message.message_type === 'admin_chat'" class="message-sender-info">
|
||||||
|
<div class="sender-label">
|
||||||
|
<span class="sender-direction">
|
||||||
|
{{ isCurrentUserMessage ? 'Вы →' : '← Получено от' }}
|
||||||
|
</span>
|
||||||
|
<span class="sender-wallet">
|
||||||
|
{{ formatWalletAddress(isCurrentUserMessage ? message.recipient_wallet : message.sender_wallet) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Текстовый контент, если есть -->
|
<!-- Текстовый контент, если есть -->
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
<div v-if="message.content" class="message-content" v-html="formattedContent" />
|
<div v-if="message.content" class="message-content" v-html="formattedContent" />
|
||||||
@@ -60,6 +72,14 @@
|
|||||||
<div class="message-time">
|
<div class="message-time">
|
||||||
{{ formattedTime }}
|
{{ formattedTime }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="message.message_type === 'admin_chat'" class="message-read-status">
|
||||||
|
<span v-if="isCurrentUserMessage" class="read-status">
|
||||||
|
{{ message.isRead ? '✓ Прочитано' : '○ Отправлено' }}
|
||||||
|
</span>
|
||||||
|
<span v-else class="read-status received">
|
||||||
|
Получено
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div v-if="message.isLocal" class="message-status">
|
<div v-if="message.isLocal" class="message-status">
|
||||||
<span class="sending-indicator">Отправка...</span>
|
<span class="sending-indicator">Отправка...</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,6 +102,34 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Простая функция для определения, является ли сообщение отправленным текущим пользователем
|
||||||
|
// Используем данные из самого сообщения для определения направления
|
||||||
|
const isCurrentUserMessage = computed(() => {
|
||||||
|
// Если это admin_chat, используем sender_id для определения
|
||||||
|
if (props.message.message_type === 'admin_chat') {
|
||||||
|
// Для простоты, считаем что если sender_id равен user_id, то это ответное сообщение
|
||||||
|
// Это может потребовать корректировки в зависимости от логики
|
||||||
|
return props.message.sender_id === props.message.user_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Для обычных сообщений используем стандартную логику
|
||||||
|
return props.message.sender_type === 'user' || props.message.role === 'user';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Функция для форматирования wallet адреса
|
||||||
|
const formatWalletAddress = (address) => {
|
||||||
|
if (!address || address === 'Админ') {
|
||||||
|
return 'Админ';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если это wallet адрес (начинается с 0x), показываем сокращенную версию
|
||||||
|
if (address.startsWith('0x') && address.length === 42) {
|
||||||
|
return `${address.slice(0, 6)}...${address.slice(-4)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return address;
|
||||||
|
};
|
||||||
|
|
||||||
// --- Работа с вложениями ---
|
// --- Работа с вложениями ---
|
||||||
const attachment = computed(() => {
|
const attachment = computed(() => {
|
||||||
// Ожидаем массив attachments, даже если там только один элемент
|
// Ожидаем массив attachments, даже если там только один элемент
|
||||||
@@ -405,4 +453,51 @@ function copyEmail(email) {
|
|||||||
.system-btn:hover {
|
.system-btn:hover {
|
||||||
background: var(--color-primary-dark, #2563eb);
|
background: var(--color-primary-dark, #2563eb);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Стили для информации об отправителе в приватном чате */
|
||||||
|
.message-sender-info {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-direction {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-primary, #3b82f6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender-wallet {
|
||||||
|
font-family: monospace;
|
||||||
|
color: var(--color-text-secondary, #666);
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для статуса прочтения */
|
||||||
|
.message-read-status {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-status {
|
||||||
|
font-size: 0.8em;
|
||||||
|
color: var(--color-text-secondary, #666);
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-status.received {
|
||||||
|
color: var(--color-success, #10b981);
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-status:contains('✓') {
|
||||||
|
color: var(--color-success, #10b981);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -24,7 +24,7 @@ export function usePermissions() {
|
|||||||
* Проверяет, может ли пользователь только читать данные
|
* Проверяет, может ли пользователь только читать данные
|
||||||
*/
|
*/
|
||||||
const canRead = computed(() => {
|
const canRead = computed(() => {
|
||||||
return userAccessLevel.value && userAccessLevel.value.hasAccess;
|
return (userAccessLevel.value && userAccessLevel.value.hasAccess) || isAdmin.value;
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -161,6 +161,18 @@ const routes = [
|
|||||||
name: 'contacts-list',
|
name: 'contacts-list',
|
||||||
component: () => import('../views/ContactsView.vue')
|
component: () => import('../views/ContactsView.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/admin-chat/:adminId',
|
||||||
|
name: 'admin-chat',
|
||||||
|
component: () => import('../views/AdminChatView.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresAdmin: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/personal-messages',
|
||||||
|
name: 'personal-messages',
|
||||||
|
component: () => import('../views/PersonalMessagesView.vue'),
|
||||||
|
meta: { requiresAuth: true, requiresAdmin: true }
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
path: '/settings/ai/telegram',
|
path: '/settings/ai/telegram',
|
||||||
@@ -197,16 +209,6 @@ const routes = [
|
|||||||
name: 'page-edit',
|
name: 'page-edit',
|
||||||
component: () => import('../views/content/PageEditView.vue'),
|
component: () => import('../views/content/PageEditView.vue'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/pages/public',
|
|
||||||
name: 'public-pages',
|
|
||||||
component: () => import('../views/content/PublicPagesView.vue'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/pages/public/:id',
|
|
||||||
name: 'public-page-view',
|
|
||||||
component: () => import('../views/content/PublicPageView.vue'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/management',
|
path: '/management',
|
||||||
name: 'management',
|
name: 'management',
|
||||||
|
|||||||
74
frontend/src/services/adminChatService.js
Normal file
74
frontend/src/services/adminChatService.js
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This software is proprietary and confidential.
|
||||||
|
* Unauthorized copying, modification, or distribution is prohibited.
|
||||||
|
*
|
||||||
|
* For licensing inquiries: info@hb3-accelerator.com
|
||||||
|
* Website: https://hb3-accelerator.com
|
||||||
|
* GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
|
*/
|
||||||
|
|
||||||
|
import api from '../api/axios';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Сервис для работы с админскими чатами
|
||||||
|
*/
|
||||||
|
const adminChatService = {
|
||||||
|
/**
|
||||||
|
* Отправляет сообщение другому администратору
|
||||||
|
* @param {number} recipientAdminId - ID получателя
|
||||||
|
* @param {string} content - Содержимое сообщения
|
||||||
|
* @param {Array} attachments - Вложения
|
||||||
|
* @returns {Promise} - Результат отправки
|
||||||
|
*/
|
||||||
|
async sendMessage(recipientAdminId, content, attachments = []) {
|
||||||
|
try {
|
||||||
|
const response = await api.post('/messages/admin/send', {
|
||||||
|
recipientAdminId,
|
||||||
|
content,
|
||||||
|
attachments
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[adminChatService] Ошибка отправки сообщения:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает список админских контактов
|
||||||
|
* @returns {Promise} - Список контактов
|
||||||
|
*/
|
||||||
|
async getAdminContacts() {
|
||||||
|
try {
|
||||||
|
console.log('[adminChatService] Запрашиваем админские контакты...');
|
||||||
|
const response = await api.get('/messages/admin/contacts');
|
||||||
|
console.log('[adminChatService] Получен ответ:', response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[adminChatService] Ошибка получения админов для приватного чата:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает сообщения из приватной беседы с другим администратором
|
||||||
|
* @param {number} adminId - ID администратора
|
||||||
|
* @returns {Promise} - Сообщения
|
||||||
|
*/
|
||||||
|
async getMessages(adminId) {
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/messages/admin/${adminId}`);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[adminChatService] Ошибка получения сообщений:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default adminChatService;
|
||||||
|
|
||||||
|
|
||||||
169
frontend/src/views/AdminChatView.vue
Normal file
169
frontend/src/views/AdminChatView.vue
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This software is proprietary and confidential.
|
||||||
|
Unauthorized copying, modification, or distribution is prohibited.
|
||||||
|
|
||||||
|
For licensing inquiries: info@hb3-accelerator.com
|
||||||
|
Website: https://hb3-accelerator.com
|
||||||
|
GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<div class="admin-chat-header">
|
||||||
|
<span>Приватный чат</span>
|
||||||
|
<button class="close-btn" @click="goBack">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isLoadingMessages" class="loading-container">
|
||||||
|
<div class="loading">Загрузка сообщений...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="chat-container">
|
||||||
|
<ChatInterface
|
||||||
|
:messages="messages"
|
||||||
|
:attachments="chatAttachments"
|
||||||
|
:newMessage="chatNewMessage"
|
||||||
|
:isLoading="isLoadingMessages"
|
||||||
|
:isAdmin="true"
|
||||||
|
@send-message="handleSendMessage"
|
||||||
|
@update:newMessage="val => chatNewMessage = val"
|
||||||
|
@update:attachments="val => chatAttachments = val"
|
||||||
|
@load-more="loadMessages"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, computed } from 'vue';
|
||||||
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
|
import BaseLayout from '../components/BaseLayout.vue';
|
||||||
|
import ChatInterface from '../components/ChatInterface.vue';
|
||||||
|
import adminChatService from '../services/adminChatService.js';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const adminId = computed(() => route.params.adminId);
|
||||||
|
const messages = ref([]);
|
||||||
|
const chatAttachments = ref([]);
|
||||||
|
const chatNewMessage = ref('');
|
||||||
|
const isLoadingMessages = ref(false);
|
||||||
|
|
||||||
|
async function loadMessages() {
|
||||||
|
if (!adminId.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
isLoadingMessages.value = true;
|
||||||
|
console.log('[AdminChatView] Загружаем сообщения для админа:', adminId.value);
|
||||||
|
|
||||||
|
const response = await adminChatService.getMessages(adminId.value);
|
||||||
|
console.log('[AdminChatView] Получен ответ:', response);
|
||||||
|
|
||||||
|
messages.value = response?.messages || [];
|
||||||
|
console.log('[AdminChatView] Загружено сообщений:', messages.value.length);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[AdminChatView] Ошибка загрузки сообщений:', error);
|
||||||
|
messages.value = [];
|
||||||
|
} finally {
|
||||||
|
isLoadingMessages.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSendMessage({ message, attachments }) {
|
||||||
|
if (!message.trim() || !adminId.value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log('[AdminChatView] Отправляем сообщение:', message, 'админу:', adminId.value);
|
||||||
|
|
||||||
|
await adminChatService.sendMessage(adminId.value, message, attachments);
|
||||||
|
|
||||||
|
// Очищаем поле ввода
|
||||||
|
chatNewMessage.value = '';
|
||||||
|
chatAttachments.value = [];
|
||||||
|
|
||||||
|
// Перезагружаем сообщения
|
||||||
|
await loadMessages();
|
||||||
|
|
||||||
|
console.log('[AdminChatView] Сообщение отправлено успешно');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[AdminChatView] Ошибка отправки сообщения:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
router.push({ name: 'crm' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadMessages();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.admin-chat-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
height: 200px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-container {
|
||||||
|
height: calc(100vh - 120px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
color: #888;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для ChatInterface */
|
||||||
|
:deep(.chat-messages) {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.chat-input) {
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f9f9f9;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
<span>Контакты</span>
|
<span>Контакты</span>
|
||||||
<span v-if="newContacts.length" class="badge">+{{ newContacts.length }}</span>
|
<span v-if="newContacts.length" class="badge">+{{ newContacts.length }}</span>
|
||||||
</div>
|
</div>
|
||||||
<ContactTable v-if="canRead" :contacts="contacts" :new-contacts="newContacts" :new-messages="newMessages" @markNewAsRead="markContactsAsRead"
|
<ContactTable v-if="canRead" :contacts="contacts" :new-contacts="newContacts" :new-messages="newMessages" @markNewAsRead="markMessagesAsRead"
|
||||||
:markMessagesAsReadForUser="markMessagesAsReadForUser" :markContactAsRead="markContactAsRead" @close="goBack" />
|
:markMessagesAsReadForUser="markMessagesAsReadForUser" :markContactAsRead="markContactAsRead" @close="goBack" />
|
||||||
|
|
||||||
<!-- Таблица-заглушка для обычных пользователей -->
|
<!-- Таблица-заглушка для обычных пользователей -->
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref } from 'vue';
|
import { ref, onMounted, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
import BaseLayout from '../components/BaseLayout.vue';
|
import BaseLayout from '../components/BaseLayout.vue';
|
||||||
import ContactTable from '../components/ContactTable.vue';
|
import ContactTable from '../components/ContactTable.vue';
|
||||||
@@ -96,12 +96,23 @@ import { usePermissions } from '@/composables/usePermissions';
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
contacts, newContacts, newMessages,
|
contacts, newContacts, newMessages,
|
||||||
markContactsAsRead, markMessagesAsReadForUser, markContactAsRead
|
markMessagesAsRead, markMessagesAsReadForUser, markContactAsRead
|
||||||
} = useContactsAndMessagesWebSocket();
|
} = useContactsAndMessagesWebSocket();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { isAdmin } = useAuthContext();
|
const auth = useAuthContext();
|
||||||
const { canRead } = usePermissions();
|
const { canRead } = usePermissions();
|
||||||
|
|
||||||
|
// Отладочная информация о правах доступа
|
||||||
|
onMounted(() => {
|
||||||
|
console.log('[ContactsView] Permissions debug:', {
|
||||||
|
canRead: canRead.value,
|
||||||
|
isAdmin: auth.isAdmin?.value,
|
||||||
|
userAccessLevel: auth.userAccessLevel?.value,
|
||||||
|
userId: auth.userId?.value,
|
||||||
|
address: auth.address?.value
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function goBack() {
|
function goBack() {
|
||||||
if (window.history.length > 1) {
|
if (window.history.length > 1) {
|
||||||
router.back();
|
router.back();
|
||||||
|
|||||||
272
frontend/src/views/PersonalMessagesView.vue
Normal file
272
frontend/src/views/PersonalMessagesView.vue
Normal file
@@ -0,0 +1,272 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (c) 2024-2025 Тарабанов Александр Викторович
|
||||||
|
All rights reserved.
|
||||||
|
|
||||||
|
This software is proprietary and confidential.
|
||||||
|
Unauthorized copying, modification, or distribution is prohibited.
|
||||||
|
|
||||||
|
For licensing inquiries: info@hb3-accelerator.com
|
||||||
|
Website: https://hb3-accelerator.com
|
||||||
|
GitHub: https://github.com/HB3-ACCELERATOR
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<BaseLayout>
|
||||||
|
<div class="personal-messages-header">
|
||||||
|
<span>Личные сообщения</span>
|
||||||
|
<span v-if="newMessagesCount > 0" class="badge">+{{ newMessagesCount }}</span>
|
||||||
|
<button class="close-btn" @click="goBack">×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isLoading" class="loading-container">
|
||||||
|
<div class="loading">Загрузка бесед...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else-if="personalMessages.length === 0" class="empty-state">
|
||||||
|
<p>У вас пока нет личных бесед с другими администраторами</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="personal-messages-list">
|
||||||
|
<div
|
||||||
|
v-for="message in personalMessages"
|
||||||
|
:key="message.id"
|
||||||
|
class="message-item"
|
||||||
|
>
|
||||||
|
<div class="message-info">
|
||||||
|
<div class="admin-name">{{ message.name }}</div>
|
||||||
|
<div class="message-preview">{{ message.last_message || 'Нет сообщений' }}</div>
|
||||||
|
<div class="message-date">{{ formatDate(message.last_message_at) }}</div>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" size="small" @click="openPersonalChat(message.id)">
|
||||||
|
Открыть
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BaseLayout>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
|
||||||
|
import { useRouter, useRoute } from 'vue-router';
|
||||||
|
import BaseLayout from '../components/BaseLayout.vue';
|
||||||
|
import adminChatService from '../services/adminChatService.js';
|
||||||
|
import { usePermissions } from '@/composables/usePermissions';
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
const route = useRoute();
|
||||||
|
const { canRead } = usePermissions();
|
||||||
|
|
||||||
|
const isLoading = ref(true);
|
||||||
|
const personalMessages = ref([]);
|
||||||
|
const newMessagesCount = ref(0);
|
||||||
|
|
||||||
|
let ws = null;
|
||||||
|
|
||||||
|
async function fetchPersonalMessages() {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
console.log('[PersonalMessagesView] Загружаем личные сообщения...');
|
||||||
|
const response = await adminChatService.getAdminContacts();
|
||||||
|
console.log('[PersonalMessagesView] API ответ:', response);
|
||||||
|
personalMessages.value = response?.contacts || [];
|
||||||
|
console.log('[PersonalMessagesView] Загружено бесед:', personalMessages.value.length);
|
||||||
|
console.log('[PersonalMessagesView] Беседы:', personalMessages.value);
|
||||||
|
newMessagesCount.value = personalMessages.value.length; // Simplified for now
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[PersonalMessagesView] Ошибка загрузки личных сообщений:', error);
|
||||||
|
personalMessages.value = [];
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function connectWebSocket() {
|
||||||
|
try {
|
||||||
|
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
|
const wsUrl = `${protocol}://${window.location.host}/ws`;
|
||||||
|
ws = new WebSocket(wsUrl);
|
||||||
|
|
||||||
|
ws.onopen = () => {
|
||||||
|
console.log('[PersonalMessagesView] WebSocket подключен');
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onmessage = (event) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type === 'contacts-updated' ||
|
||||||
|
data.type === 'messages-updated' ||
|
||||||
|
data.type === 'contact-updated' ||
|
||||||
|
data.type === 'admin-status-changed') {
|
||||||
|
console.log('[PersonalMessagesView] Получено обновление через WebSocket:', data.type);
|
||||||
|
fetchPersonalMessages();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[PersonalMessagesView] Ошибка парсинга WebSocket сообщения:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onerror = (error) => {
|
||||||
|
console.error('[PersonalMessagesView] Ошибка WebSocket:', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
ws.onclose = () => {
|
||||||
|
console.log('[PersonalMessagesView] WebSocket отключен, переподключаемся через 3 секунды');
|
||||||
|
setTimeout(() => {
|
||||||
|
if (ws?.readyState === WebSocket.CLOSED) {
|
||||||
|
connectWebSocket();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[PersonalMessagesView] Ошибка подключения WebSocket:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnectWebSocket() {
|
||||||
|
if (ws) {
|
||||||
|
ws.close();
|
||||||
|
ws = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openPersonalChat(adminId) {
|
||||||
|
console.log('[PersonalMessagesView] Открываем приватный чат с админом:', adminId);
|
||||||
|
router.push({ name: 'admin-chat', params: { adminId } });
|
||||||
|
}
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
if (window.history.length > 1) {
|
||||||
|
router.back();
|
||||||
|
} else {
|
||||||
|
router.push({ name: 'crm' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return '-';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
return date.toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Следим за изменениями роута для обновления при возврате на страницу
|
||||||
|
watch(() => route.path, async (newPath) => {
|
||||||
|
if (newPath === '/personal-messages' && canRead.value) {
|
||||||
|
console.log('[PersonalMessagesView] Возврат на страницу, обновляем список');
|
||||||
|
await fetchPersonalMessages();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (canRead.value) {
|
||||||
|
await fetchPersonalMessages();
|
||||||
|
connectWebSocket();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
disconnectWebSocket();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.personal-messages-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
background: #ff4757;
|
||||||
|
color: white;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background-color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
height: 200px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.personal-messages-list {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
background: white;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-item:hover {
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-name {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-preview {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-date {
|
||||||
|
color: #999;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Стили для загрузки */
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2rem;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
color: #666;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -381,23 +381,22 @@ async function loadMessages() {
|
|||||||
if (!contact.value || !contact.value.id) return;
|
if (!contact.value || !contact.value.id) return;
|
||||||
isLoadingMessages.value = true;
|
isLoadingMessages.value = true;
|
||||||
try {
|
try {
|
||||||
// Получаем conversationId для контакта
|
// Загружаем ВСЕ публичные сообщения этого пользователя (как на главной странице)
|
||||||
const conv = await messagesService.getConversationByUserId(contact.value.id);
|
messages.value = await messagesService.getMessagesByUserId(contact.value.id);
|
||||||
conversationId.value = conv?.id || null;
|
|
||||||
if (conversationId.value) {
|
|
||||||
messages.value = await messagesService.getMessagesByConversationId(conversationId.value);
|
|
||||||
if (messages.value.length > 0) {
|
if (messages.value.length > 0) {
|
||||||
lastMessageDate.value = messages.value[messages.value.length - 1].created_at;
|
lastMessageDate.value = messages.value[messages.value.length - 1].created_at;
|
||||||
} else {
|
} else {
|
||||||
lastMessageDate.value = null;
|
lastMessageDate.value = null;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
messages.value = [];
|
// Также получаем conversationId для отправки новых сообщений
|
||||||
lastMessageDate.value = null;
|
const conv = await messagesService.getConversationByUserId(contact.value.id);
|
||||||
}
|
conversationId.value = conv?.id || null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
console.error('[ContactDetailsView] Ошибка загрузки сообщений:', e);
|
||||||
messages.value = [];
|
messages.value = [];
|
||||||
lastMessageDate.value = null;
|
lastMessageDate.value = null;
|
||||||
|
conversationId.value = null;
|
||||||
} finally {
|
} finally {
|
||||||
isLoadingMessages.value = false;
|
isLoadingMessages.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user