feat: initial project structure (Model Project) - Backend + Multi-Frontend + Docker
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
'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()
|
||||
Reference in New Issue
Block a user