'use strict' const DirectMessage = require('../models/DirectMessage') const { query: queryDb } = require('../database/postgres') const { sseSend, isOnline } = require('../services/sse') /** * Extrai accountId e tenantId da request, compatível com: * - verifyTeamToken → req.teamAccount = { accountId, tenantId, ... } * - verifyAnyToken → req.admin = { id, tenant_id, ... } OU req.teamAccount */ function extractAuth(req) { const accountId = req.teamAccount?.accountId || req.admin?.id || null const tenantId = req.tenantId || req.teamAccount?.tenantId || req.admin?.tenant_id || null return { accountId, tenantId } } class DirectMessageController { /** * POST /api/admin/inbox/direct-message * Enviar DM para um colaborador */ async send(req, res) { try { const { to_account, content } = req.body const { accountId: fromAccountId, tenantId } = extractAuth(req) if (!fromAccountId || !tenantId) { return res.status(401).json({ error: 'Não autenticado' }) } if (!to_account || !content?.trim()) { return res.status(400).json({ error: 'to_account e content obrigatórios' }) } const msg = await DirectMessage.send(tenantId, fromAccountId, to_account, content) res.json(msg) // Notificar destinatário via SSE (se estiver conectado) sseSend(to_account, 'dm.new', { id: msg.id, from: fromAccountId, to: to_account, content: content, created_at: msg.created_at, }) } catch (err) { console.error('[dm.send]', err.message) res.status(500).json({ error: 'Erro ao enviar mensagem' }) } } /** * GET /api/admin/inbox/direct/:otherAccountId * Histórico de DMs com um colaborador */ async getConversation(req, res) { try { const { otherAccountId } = req.params const { accountId, tenantId } = extractAuth(req) const limit = Math.min(Number(req.query.limit) || 50, 200) if (!accountId || !tenantId) { return res.status(401).json({ error: 'Não autenticado' }) } if (!otherAccountId) { return res.status(400).json({ error: 'otherAccountId obrigatório' }) } const msgs = await DirectMessage.getConversation(tenantId, accountId, Number(otherAccountId), limit) // Marcar como lidas ao abrir a conversa await DirectMessage.markAsRead(tenantId, accountId, Number(otherAccountId)) res.json(msgs) } catch (err) { console.error('[dm.getConversation]', err.message) res.status(500).json({ error: 'Erro ao buscar conversa' }) } } /** * GET /api/admin/inbox/direct-messages * Últimas DMs com cada colaborador + contagem de não lidas */ async getLastMessages(req, res) { try { const { accountId, tenantId } = extractAuth(req) if (!accountId || !tenantId) { return res.status(401).json({ error: 'Não autenticado' }) } const [lastMsgs, unreadByContact] = await Promise.all([ DirectMessage.getLastMessagesByAccount(tenantId, accountId), DirectMessage.getUnreadCountByContact(tenantId, accountId), ]) // Enriquecer com info do outro usuário + unread count const enriched = await Promise.all( lastMsgs.map(async (msg) => { const otherAccountId = msg.other_account const rows = await queryDb( `SELECT id, name, email, availability FROM accounts WHERE id = $1`, [otherAccountId] ) const account = rows[0] || null return { id: msg.id, other_account: otherAccountId, other_account_name: account?.name || 'Unknown', other_account_availability: account?.availability || 'offline', last_message: msg.content, last_message_at: msg.created_at, unread_count: unreadByContact[otherAccountId] || 0, } }) ) res.json(enriched) } catch (err) { console.error('[dm.getLastMessages]', err.message) res.status(500).json({ error: 'Erro ao buscar DMs' }) } } /** * GET /api/admin/inbox/colleagues * Listar todos os colaboradores do tenant com status online/offline * Para popular a coluna de contatos */ async getColleagues(req, res) { try { const { accountId, tenantId } = extractAuth(req) if (!tenantId) { return res.status(401).json({ error: 'Não autenticado' }) } // accountId pode ser null para admins sem account — nesse caso lista todos const colleagues = accountId ? await queryDb( `SELECT a.id, a.name, a.email, a.availability, a.avatar_url FROM accounts a WHERE a.tenant_id = $1 AND a.id != $2 AND a.active = true ORDER BY CASE WHEN a.availability = 'online' THEN 0 WHEN a.availability = 'busy' THEN 1 ELSE 2 END, a.name`, [tenantId, accountId] ) : await queryDb( `SELECT a.id, a.name, a.email, a.availability, a.avatar_url FROM accounts a WHERE a.tenant_id = $1 AND a.active = true ORDER BY CASE WHEN a.availability = 'online' THEN 0 WHEN a.availability = 'busy' THEN 1 ELSE 2 END, a.name`, [tenantId] ) // Adicionar unread count para cada colega (só quando temos accountId) const unreadByContact = accountId ? await DirectMessage.getUnreadCountByContact(tenantId, accountId) : {} // Presença em tempo real: SSE conectado = online (exceto se DB diz busy) const enriched = colleagues.map(c => { let availability = c.availability if (isOnline(c.id)) { // Se conectado via SSE e não está como busy no DB → online if (availability !== 'busy') availability = 'online' } else { // Sem conexão SSE → offline independente do DB availability = 'offline' } return { ...c, availability, unread_count: unreadByContact[c.id] || 0 } }) res.json(enriched) } catch (err) { console.error('[dm.getColleagues]', err.message) res.status(500).json({ error: 'Erro ao buscar colaboradores' }) } } /** * PATCH /api/admin/inbox/direct/:messageId/read * Marcar uma DM como lida */ async markAsRead(req, res) { try { const { messageId } = req.params const { accountId } = extractAuth(req) if (!accountId) { return res.status(401).json({ error: 'Não autenticado' }) } // Verificar que a mensagem é para este usuário const rows = await queryDb( `SELECT id FROM direct_messages WHERE id = $1 AND to_account = $2`, [messageId, accountId] ) const msg = rows[0] || null if (!msg) { return res.status(404).json({ error: 'Mensagem não encontrada' }) } await queryDb( `UPDATE direct_messages SET read_at = NOW() WHERE id = $1`, [messageId] ) res.json({ ok: true }) } catch (err) { console.error('[dm.markAsRead]', err.message) res.status(500).json({ error: 'Erro ao marcar como lida' }) } } } module.exports = new DirectMessageController()