Foto automática, boleto na conversa e novo Dashboard
- Foto do cliente atualizada automaticamente ao abrir a conversa (remove botão manual) - Exibe CLI_FOTO na tela de Dados do Cliente; matrícula com conversão robusta - Envio de boleto na conversa (checkbox em Configurações > Empresa): lista títulos em aberto e envia texto com linha digitável + PIX ao cliente - Novo Dashboard (gerente): conversas (abertas/não atendidas/não atribuídas/pendentes), atendentes disponíveis/desconectados e heatmap de tráfego - Presença de atendentes em memória + ping periódico do chat/dashboard Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
const db = require('../database');
|
||||
const { isGerente } = require('../middlewares/roles');
|
||||
const presence = require('../presence');
|
||||
|
||||
/**
|
||||
* Resolve a lista de empresas para a consulta, respeitando a permissão do
|
||||
* usuário. Se vier empresaId e o usuário tiver acesso, usa só ela; senão usa
|
||||
* todas as empresas do usuário.
|
||||
*/
|
||||
function empresasPermitidas(req) {
|
||||
const minhas = (req.user && req.user.empresas) || [];
|
||||
const pedida = parseInt(req.query.empresaId, 10);
|
||||
if (pedida && minhas.includes(pedida)) return [pedida];
|
||||
return minhas;
|
||||
}
|
||||
|
||||
class DashboardController {
|
||||
/**
|
||||
* Ping de presença — marca o atendente como online.
|
||||
* POST /api/:alias/dashboard/ping
|
||||
*/
|
||||
static async ping(req, res) {
|
||||
const { alias } = req.params;
|
||||
if (req.user && req.user.id != null) presence.registrar(alias, req.user.id);
|
||||
res.json({ success: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Estatísticas do dashboard (apenas gerente).
|
||||
* GET /api/:alias/dashboard/stats?empresaId=
|
||||
*/
|
||||
static async stats(req, res) {
|
||||
try {
|
||||
if (!(await isGerente(req))) {
|
||||
return res.status(403).json({ success: false, error: 'Apenas gerentes podem visualizar o dashboard.' });
|
||||
}
|
||||
const { alias } = req.params;
|
||||
const empresas = empresasPermitidas(req);
|
||||
if (empresas.length === 0) {
|
||||
return res.json({ success: true, data: {
|
||||
conversas: { abertas: 0, naoAtendidas: 0, naoAtribuidas: 0, pendentes: 0 },
|
||||
atendentes: { disponiveis: 0, desconectados: 0, lista: [] },
|
||||
trafego: [],
|
||||
} });
|
||||
}
|
||||
const ph = empresas.map(() => '?').join(',');
|
||||
|
||||
// ===== Conversas =====
|
||||
// Abertas: têm atendente E equipe
|
||||
const abertas = await db.query(alias,
|
||||
`SELECT COUNT(*) AS CT FROM CHATC2_CONVERSAS
|
||||
WHERE CON_EMPRESA_ID IN (${ph}) AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A'
|
||||
AND CON_USUARIO_ID IS NOT NULL AND CON_EQUIPE_ID IS NOT NULL`, empresas);
|
||||
|
||||
// Não atendidas: têm atendente+equipe e a última mensagem foi do cliente ('C')
|
||||
const naoAtendidas = await db.query(alias,
|
||||
`SELECT COUNT(*) AS CT FROM CHATC2_CONVERSAS c
|
||||
WHERE c.CON_EMPRESA_ID IN (${ph}) AND c.CON_STATUS IN ('A','E') AND c.CON_SITUACAO = 'A'
|
||||
AND c.CON_USUARIO_ID IS NOT NULL AND c.CON_EQUIPE_ID IS NOT NULL
|
||||
AND (SELECT m.CME_REMETENTE FROM CHATC2_CONVERSAS_MENSAGENS m
|
||||
WHERE m.CME_CONVERSA_ID = c.CON_CODIGO_ID
|
||||
ORDER BY m.CME_DT_ENVIO DESC FETCH FIRST 1 ROWS ONLY) = 'C'`, empresas);
|
||||
|
||||
// Não atribuídas: sem atendente e sem equipe
|
||||
const naoAtribuidas = await db.query(alias,
|
||||
`SELECT COUNT(*) AS CT FROM CHATC2_CONVERSAS
|
||||
WHERE CON_EMPRESA_ID IN (${ph}) AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A'
|
||||
AND CON_USUARIO_ID IS NULL AND CON_EQUIPE_ID IS NULL`, empresas);
|
||||
|
||||
// Pendentes: têm apenas equipe (sem atendente)
|
||||
const pendentes = await db.query(alias,
|
||||
`SELECT COUNT(*) AS CT FROM CHATC2_CONVERSAS
|
||||
WHERE CON_EMPRESA_ID IN (${ph}) AND CON_STATUS IN ('A','E') AND CON_SITUACAO = 'A'
|
||||
AND CON_USUARIO_ID IS NULL AND CON_EQUIPE_ID IS NOT NULL`, empresas);
|
||||
|
||||
// ===== Atendentes =====
|
||||
const usuarios = await db.query(alias,
|
||||
`SELECT DISTINCT u.USU_CODIGO_ID, u.USU_NOME
|
||||
FROM USUARIOS u INNER JOIN USUARIOS_EMPRESA ue ON u.USU_CODIGO_ID = ue.USE_USUARIO_ID
|
||||
WHERE ue.USE_EMPRESA_ID IN (${ph}) AND u.USU_STATUS = 'A' AND COALESCE(u.USU_ACESSO_WEB,0) = 1
|
||||
ORDER BY u.USU_NOME`, empresas);
|
||||
|
||||
const online = presence.idsOnline(alias);
|
||||
let disponiveis = 0, desconectados = 0;
|
||||
const lista = usuarios.map(function (u) {
|
||||
const on = online.has(Number(u.USU_CODIGO_ID));
|
||||
if (on) disponiveis++; else desconectados++;
|
||||
return { id: u.USU_CODIGO_ID, nome: (u.USU_NOME || '').trim(), online: on };
|
||||
});
|
||||
|
||||
// ===== Tráfego das conversas (últimos 365 dias, por dia) =====
|
||||
const desde = new Date(Date.now() - 365 * 24 * 60 * 60 * 1000);
|
||||
const desdeStr = desde.toISOString().split('T')[0];
|
||||
const trafegoRows = await db.query(alias,
|
||||
`SELECT CAST(COALESCE(CON_DT_INICIO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG) AS DATE) AS DIA, COUNT(*) AS CT
|
||||
FROM CHATC2_CONVERSAS
|
||||
WHERE CON_EMPRESA_ID IN (${ph})
|
||||
AND COALESCE(CON_DT_INICIO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG) >= ?
|
||||
GROUP BY CAST(COALESCE(CON_DT_INICIO, CON_PRIMEIRA_MSG, CON_DT_ULTIMA_MSG) AS DATE)`,
|
||||
empresas.concat([desdeStr]));
|
||||
|
||||
const fmtDia = (d) => {
|
||||
if (!d) return null;
|
||||
if (typeof d === 'string') return d.split('T')[0];
|
||||
if (d instanceof Date) return d.toISOString().split('T')[0];
|
||||
return String(d);
|
||||
};
|
||||
const trafego = trafegoRows
|
||||
.map(function (r) { return { dia: fmtDia(r.DIA), total: Number(r.CT) || 0 }; })
|
||||
.filter(function (r) { return r.dia; });
|
||||
|
||||
res.json({ success: true, data: {
|
||||
conversas: {
|
||||
abertas: Number(abertas[0]?.CT) || 0,
|
||||
naoAtendidas: Number(naoAtendidas[0]?.CT) || 0,
|
||||
naoAtribuidas: Number(naoAtribuidas[0]?.CT) || 0,
|
||||
pendentes: Number(pendentes[0]?.CT) || 0,
|
||||
},
|
||||
atendentes: { disponiveis, desconectados, lista },
|
||||
trafego,
|
||||
} });
|
||||
} catch (err) { res.status(500).json({ success: false, error: err.message }); }
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = DashboardController;
|
||||
Reference in New Issue
Block a user