Files
SLACK_COORD_RELATORIO/relatorio/src/v2/format.js
T
Gustavo Santos d21c4596e1 chore: import relatorio (subtree)
git-subtree-dir: relatorio
git-subtree-mainline: e322cec92b
git-subtree-split: 34bcb132f3
2026-10-01 14:26:02 -03:00

194 lines
6.0 KiB
JavaScript

'use strict';
/**
* Montagem das mensagens finais.
*
* formatFallbackMessage: PORTA quase literal da mensagem da V1 (node
* "Code - Preparar indicadores" dos dois workflows *-SQL-Slack.n8n.json).
* Usada em dois lugares na V2:
* 1. Como fallback real quando a IA falha/timeout/JSON invalido.
* 2. Como "mensagem V1" no payload de comparacao do shadow mode.
*
* formatShortMessage: mensagem curta da V2 (secoes 16 e 18 da missao).
*/
function formatDataBr(isoDate) {
const [ano, mes, dia] = isoDate.split('-');
return `${dia}/${mes}`;
}
function formatSaldo(saldo) {
return saldo > 0 ? `+${saldo}` : String(saldo);
}
/** Igual ao texto que a V1 ja envia hoje (nao mudar sem motivo). */
function formatFallbackMessage(row) {
const ehSemanal = row.tipo_periodo === 'semanal';
const titulo = ehSemanal
? '*Resumo semanal do suporte GLPI*'
: '*Resumo diario do suporte GLPI*';
const escopo = ehSemanal ? 'Periodo do relatorio' : 'Data do relatorio';
const unidade = ehSemanal ? 'no periodo' : 'no dia';
const linhas = [
titulo,
'',
`${escopo}: ${row.periodo_label}`,
'',
'*Resumo geral*',
`- Chamados abertos ${unidade}: ${row.abertos}`,
`- Chamados fechados ${unidade}: ${row.fechados}`,
`- Pendentes atuais: ${row.pendentes}`,
`- Alta prioridade ainda pendentes: ${row.alta_prioridade_pendentes}`,
`- Saldo fechados - abertos: ${row.saldo}`,
'',
'*Categorias mais recorrentes*',
...String(row.categorias_recorrentes || 'Sem registros')
.split('\n')
.map((l) => `- ${l}`),
'',
'*Clientes com maior volume*',
...String(row.clientes_volume || 'Sem registros')
.split('\n')
.map((l) => `- ${l}`),
'',
'*Observacao*',
`- Chamados fechados ${unidade} podem ter sido abertos em datas anteriores.`,
];
return linhas.join('\n');
}
/**
* Status operacional (semaforo interno, secao 21). So aparece na mensagem
* quando agrega valor (usado hoje apenas para escolher o tom da linha de
* tendencia/leitura).
*/
function computeOperationalStatus({ insightsAceitos, tendenciaBacklog }) {
const temAlta = insightsAceitos.some((i) => i.severity === 'high');
if (temAlta) return 'CRITICAL';
const temMedia = insightsAceitos.some((i) => i.severity === 'medium');
if (temMedia || tendenciaBacklog.direcao === 'aumentando') return 'ATTENTION';
return 'NORMAL';
}
function buildDestaques({ topCategoria, concentracoesCliente, altaPrioridadeDelta, insightsAceitos, unidade }, config) {
const destaques = [];
if (topCategoria && topCategoria.total > 0) {
destaques.push(`${topCategoria.nome} liderou ${unidade} com ${topCategoria.total} chamados.`);
}
for (const c of concentracoesCliente || []) {
destaques.push(`${c.cliente} concentrou ${c.total} chamados (${Math.round(c.share * 100)}% ${unidade}).`);
}
if (altaPrioridadeDelta && altaPrioridadeDelta.anterior !== null && altaPrioridadeDelta.atual !== altaPrioridadeDelta.anterior) {
const verbo = altaPrioridadeDelta.atual > altaPrioridadeDelta.anterior ? 'subiram' : 'cairam';
destaques.push(
`Chamados de alta prioridade ${verbo} de ${altaPrioridadeDelta.anterior} para ${altaPrioridadeDelta.atual}.`
);
}
for (const insight of insightsAceitos) {
const ocorrencia = insight.occurrenceText;
destaques.push(`${insight.title}${ocorrencia ? ` (${ocorrencia})` : ''}.`);
}
return destaques.slice(0, config.MAX_REPORT_INSIGHTS);
}
function buildTrendLine({ status, tendenciaBacklog, tipoPeriodo }) {
const rotuloBacklog =
tendenciaBacklog.direcao === 'aumentando'
? 'aumento do backlog no periodo'
: tendenciaBacklog.direcao === 'diminuindo'
? 'reducao do backlog no periodo'
: tendenciaBacklog.direcao === 'indisponivel'
? 'backlog sem historico suficiente para comparacao'
: 'backlog estavel no periodo';
const abertura =
status === 'CRITICAL'
? 'Operacao requer atencao'
: status === 'ATTENTION'
? tipoPeriodo === 'semanal'
? 'Semana com pontos de atencao'
: 'Operacao com pontos de atencao'
: tipoPeriodo === 'semanal'
? 'Semana equilibrada'
: 'Operacao estavel';
return `${abertura}, com ${rotuloBacklog}.`;
}
/**
* Monta a mensagem curta final da V2.
*
* @param {object} dados - numeros deterministicos + insights ja sanitizados
* @param {object} config
*/
function formatShortMessage(dados, config) {
const ehSemanal = dados.tipo_periodo === 'semanal';
const unidade = ehSemanal ? 'no periodo' : 'no dia';
const tituloData = ehSemanal
? `${formatDataBr(dados.data_inicio)} a ${formatDataBr(dados.data_fim)}`
: formatDataBr(dados.data_inicio);
const titulo = ehSemanal
? `Resumo semanal do suporte — ${tituloData}`
: `Resumo do suporte — ${tituloData}`;
const status = computeOperationalStatus({
insightsAceitos: dados.insightsAceitos,
tendenciaBacklog: dados.tendenciaBacklog,
});
const destaques = buildDestaques(
{
topCategoria: dados.topCategoria,
concentracoesCliente: dados.concentracoesCliente,
altaPrioridadeDelta: dados.altaPrioridadeDelta,
insightsAceitos: dados.insightsAceitos,
unidade,
},
config
);
const linhaTendencia = buildTrendLine({
status,
tendenciaBacklog: dados.tendenciaBacklog,
tipoPeriodo: dados.tipo_periodo,
});
const blocos = [
titulo,
'',
`${dados.abertos} abertos | ${dados.fechados} fechados | saldo ${formatSaldo(dados.saldo)}`,
`${dados.pendentes} pendentes | ${dados.alta_prioridade_pendentes} alta prioridade`,
'',
ehSemanal ? 'Destaques' : 'Destaques',
];
if (destaques.length === 0) {
blocos.push('• Sem anomalias relevantes no periodo.');
} else {
for (const d of destaques) blocos.push(`• ${d}`);
}
blocos.push('', ehSemanal ? 'Leitura' : 'Tendencia', `→ ${linhaTendencia}`);
return { texto: blocos.join('\n'), status };
}
module.exports = {
formatDataBr,
formatSaldo,
formatFallbackMessage,
computeOperationalStatus,
buildDestaques,
buildTrendLine,
formatShortMessage,
};