|
|
@@ -0,0 +1,178 @@
|
|
|
+import fs from "node:fs";
|
|
|
+import path from "node:path";
|
|
|
+import { fileURLToPath } from "node:url";
|
|
|
+import { AtendimentoAvaliacao } from "../src/models/AtendimentoAvaliacao.model.js";
|
|
|
+import { AtendimentoMensagem } from "../src/models/AtendimentoMensagem.model.js";
|
|
|
+import { avaliarPendentes, contarPendentes } from "../src/services/atendimentoAvaliacaoService.js";
|
|
|
+import { sincronizarTodosProtocolosAbertos } from "../src/services/atendimentosService.js";
|
|
|
+import { EXPEDIENTE_POR_SETOR, estaForaExpediente } from "../src/config/expediente.js";
|
|
|
+
|
|
|
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
+const APLICAR = process.argv.includes("--apply");
|
|
|
+const FORCAR_TODOS = process.argv.includes("--forcar-todos");
|
|
|
+const SEM_SYNC = process.argv.includes("--sem-sync");
|
|
|
+
|
|
|
+const loteIdx = process.argv.indexOf("--lote");
|
|
|
+const LOTE = loteIdx > -1 ? Number(process.argv[loteIdx + 1]) : 20;
|
|
|
+
|
|
|
+const setorIdx = process.argv.indexOf("--setor");
|
|
|
+const SETOR = setorIdx > -1 ? process.argv[setorIdx + 1] : null;
|
|
|
+
|
|
|
+const limiteIdx = process.argv.indexOf("--limite");
|
|
|
+const LIMITE = limiteIdx > -1 ? Number(process.argv[limiteIdx + 1]) : null;
|
|
|
+
|
|
|
+const expectIdx = process.argv.indexOf("--expect-count");
|
|
|
+const EXPECT_COUNT = expectIdx > -1 ? Number(process.argv[expectIdx + 1]) : null;
|
|
|
+
|
|
|
+const SETORES_COM_GRADE = Object.keys(EXPEDIENTE_POR_SETOR);
|
|
|
+
|
|
|
+async function buscarCandidatos() {
|
|
|
+ const query = AtendimentoAvaliacao.query()
|
|
|
+ .where("Avaliavel", true)
|
|
|
+ .join("atendimentos as a", "a.Id", "atendimento_avaliacoes.AtendimentoId")
|
|
|
+ .select("atendimento_avaliacoes.AtendimentoId", "atendimento_avaliacoes.ScoreAtendente", "a.Setor")
|
|
|
+ .orderBy("atendimento_avaliacoes.AtendimentoId", "asc");
|
|
|
+
|
|
|
+ if (SETOR) query.where("a.Setor", SETOR);
|
|
|
+ if (!FORCAR_TODOS) query.whereIn("a.Setor", SETORES_COM_GRADE);
|
|
|
+
|
|
|
+ return query;
|
|
|
+}
|
|
|
+
|
|
|
+async function temMensagemForaExpediente(atendimentoId, setor) {
|
|
|
+ const mensagensCliente = await AtendimentoMensagem.query()
|
|
|
+ .where("AtendimentoId", atendimentoId)
|
|
|
+ .where("Resposta", 0)
|
|
|
+ .select("Timestamp");
|
|
|
+
|
|
|
+ return mensagensCliente.some((m) => estaForaExpediente(setor, m.Timestamp));
|
|
|
+}
|
|
|
+
|
|
|
+async function filtrarAlvos(candidatos) {
|
|
|
+ if (FORCAR_TODOS) return { alvos: candidatos, semMensagemForaExpediente: 0 };
|
|
|
+
|
|
|
+ const alvos = [];
|
|
|
+ let semMensagemForaExpediente = 0;
|
|
|
+
|
|
|
+ for (const c of candidatos) {
|
|
|
+ if (await temMensagemForaExpediente(c.AtendimentoId, c.Setor)) {
|
|
|
+ alvos.push(c);
|
|
|
+ } else {
|
|
|
+ semMensagemForaExpediente += 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return { alvos, semMensagemForaExpediente };
|
|
|
+}
|
|
|
+
|
|
|
+async function main() {
|
|
|
+ const totalQuery = AtendimentoAvaliacao.query()
|
|
|
+ .where("Avaliavel", true)
|
|
|
+ .join("atendimentos as a", "a.Id", "atendimento_avaliacoes.AtendimentoId");
|
|
|
+ if (SETOR) totalQuery.where("a.Setor", SETOR);
|
|
|
+ const totalAvaliavel = await totalQuery.resultSize();
|
|
|
+
|
|
|
+ const candidatos = await buscarCandidatos();
|
|
|
+ const descartadosSetorSemGrade = totalAvaliavel - candidatos.length;
|
|
|
+
|
|
|
+ console.log(`[reavaliar-expediente] ${totalAvaliavel} avaliações Avaliavel=true${SETOR ? ` no setor ${SETOR}` : ""}.`);
|
|
|
+ if (!FORCAR_TODOS) {
|
|
|
+ console.log(`[reavaliar-expediente] setores com grade de horário: ${SETORES_COM_GRADE.join(", ")}.`);
|
|
|
+ console.log(`[reavaliar-expediente] ${descartadosSetorSemGrade} descartados: setor sem grade de horário.`);
|
|
|
+ }
|
|
|
+ console.log(`[reavaliar-expediente] ${candidatos.length} candidatos após filtro de setor.`);
|
|
|
+
|
|
|
+ const { alvos: todosAlvos, semMensagemForaExpediente } = await filtrarAlvos(candidatos);
|
|
|
+ if (!FORCAR_TODOS) {
|
|
|
+ console.log(`[reavaliar-expediente] ${semMensagemForaExpediente} descartados: nenhuma mensagem do cliente fora do expediente.`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const alvos = LIMITE ? todosAlvos.slice(0, LIMITE) : todosAlvos;
|
|
|
+ console.log(`[reavaliar-expediente] ${alvos.length} avaliações serão resetadas para pendente${LIMITE ? ` (--limite ${LIMITE})` : ""}.`);
|
|
|
+
|
|
|
+ if (EXPECT_COUNT !== null && alvos.length !== EXPECT_COUNT) {
|
|
|
+ console.error(`[reavaliar-expediente] abortando: esperava ${EXPECT_COUNT}, achei ${alvos.length}.`);
|
|
|
+ process.exit(1);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!APLICAR) {
|
|
|
+ console.log("[reavaliar-expediente] dry-run (nenhuma alteração feita). Use --apply para executar.");
|
|
|
+ process.exit(0);
|
|
|
+ }
|
|
|
+ if (alvos.length === 0) {
|
|
|
+ console.log("[reavaliar-expediente] nada para reavaliar.");
|
|
|
+ process.exit(0);
|
|
|
+ }
|
|
|
+
|
|
|
+ const backupDir = path.resolve(__dirname, "logs");
|
|
|
+ fs.mkdirSync(backupDir, { recursive: true });
|
|
|
+ const backupPath = path.join(backupDir, `reavaliar-expediente-${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
|
|
|
+ fs.writeFileSync(backupPath, JSON.stringify(alvos, null, 2));
|
|
|
+ console.log(`[reavaliar-expediente] backup dos scores antigos salvo em ${backupPath}`);
|
|
|
+
|
|
|
+ if (!SEM_SYNC) {
|
|
|
+ console.log("[reavaliar-expediente] passo 1/3: sincronizando protocolos abertos (sincronizarTodosProtocolosAbertos)...");
|
|
|
+ try {
|
|
|
+ const sync = await sincronizarTodosProtocolosAbertos();
|
|
|
+ console.log(
|
|
|
+ `[reavaliar-expediente] sync concluído: ${sync.importados}/${sync.processados} importados, ` +
|
|
|
+ `${sync.mensagens} mensagens, ${sync.erros.length} erros, ${sync.paginas} página(s).`
|
|
|
+ );
|
|
|
+ } catch (err) {
|
|
|
+ console.error(`[reavaliar-expediente] sync falhou (${err.message}), seguindo mesmo assim.`);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ console.log("[reavaliar-expediente] passo 1/3: pulado (--sem-sync).");
|
|
|
+ }
|
|
|
+
|
|
|
+ const ids = alvos.map((a) => a.AtendimentoId);
|
|
|
+ const pendentesAntes = await contarPendentes();
|
|
|
+ await AtendimentoAvaliacao.query().whereIn("AtendimentoId", ids).patch({ UltimaMensagemId: null });
|
|
|
+ const pendentesDepois = await contarPendentes();
|
|
|
+ console.log(`[reavaliar-expediente] passo 2/3: ${ids.length} avaliações resetadas — fila de pendentes ${pendentesAntes} -> ${pendentesDepois}.`);
|
|
|
+
|
|
|
+ console.log(`[reavaliar-expediente] passo 3/3: drenando fila de pendentes (mesmo fluxo de avaliarTudo.js, lotes de ${LOTE})...`);
|
|
|
+ let restantes = pendentesDepois;
|
|
|
+ let totalAvaliados = 0;
|
|
|
+ let totalSemDialogo = 0;
|
|
|
+ const erros = [];
|
|
|
+ const inicio = Date.now();
|
|
|
+
|
|
|
+ while (restantes > 0) {
|
|
|
+ const t0 = Date.now();
|
|
|
+ const r = await avaliarPendentes({ limite: LOTE });
|
|
|
+ restantes = await contarPendentes();
|
|
|
+ totalAvaliados += r.avaliados;
|
|
|
+ totalSemDialogo += r.semDialogo;
|
|
|
+ erros.push(...r.erros);
|
|
|
+
|
|
|
+ console.log(
|
|
|
+ `[reavaliar-expediente] +${r.avaliados} avaliados, ${r.semDialogo} sem diálogo, ${r.erros.length} erros ` +
|
|
|
+ `(${((Date.now() - t0) / 1000).toFixed(0)}s) — restam ${restantes}`
|
|
|
+ );
|
|
|
+ if (r.erros.length) r.erros.slice(0, 3).forEach((e) => console.log(` erro: ${e.atendimentoId} ${e.erro}`));
|
|
|
+
|
|
|
+ if (r.avaliados === 0 && r.semDialogo === 0 && r.erros.length === 0) {
|
|
|
+ console.error("[reavaliar-expediente] lote sem nenhum progresso, abortando para evitar loop infinito.");
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log(
|
|
|
+ `[reavaliar-expediente] fila drenada em ${(((Date.now() - inicio)) / 1000 / 60).toFixed(1)}min: ` +
|
|
|
+ `${totalAvaliados} avaliados, ${totalSemDialogo} sem diálogo, ${erros.length} erros.`
|
|
|
+ );
|
|
|
+
|
|
|
+ const idsSet = new Set(ids);
|
|
|
+ const scoreAntigoPorId = new Map(alvos.map((a) => [a.AtendimentoId, a.ScoreAtendente]));
|
|
|
+ const atuais = await AtendimentoAvaliacao.query().whereIn("AtendimentoId", ids).select("AtendimentoId", "ScoreAtendente");
|
|
|
+ const mudouScore = atuais.filter((a) => idsSet.has(a.AtendimentoId) && a.ScoreAtendente !== scoreAntigoPorId.get(a.AtendimentoId)).length;
|
|
|
+ console.log(`[reavaliar-expediente] dos ${ids.length} alvos do backfill, ${mudouScore} tiveram o score alterado.`);
|
|
|
+
|
|
|
+ process.exit(erros.length ? 1 : 0);
|
|
|
+}
|
|
|
+
|
|
|
+main().catch((e) => {
|
|
|
+ console.error("[reavaliar-expediente] falha:", e.message);
|
|
|
+ process.exit(1);
|
|
|
+});
|