| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- import fs from "node:fs";
- import path from "node:path";
- import { fileURLToPath } from "node:url";
- import { AtendimentoAvaliacao } from "../src/models/AtendimentoAvaliacao.model.js";
- import { AtendimentoGoldenLabel } from "../src/models/AtendimentoGoldenLabel.model.js";
- import { SETOR_NOMES, setorAvaliavel } from "../src/config/setores.js";
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
- const APLICAR = process.argv.includes("--apply");
- const RESET = process.argv.includes("--reset");
- const tamanhoIdx = process.argv.indexOf("--tamanho");
- const TAMANHO = tamanhoIdx > -1 ? Number(process.argv[tamanhoIdx + 1]) : 40;
- const minimoIdx = process.argv.indexOf("--minimo");
- const MINIMO = minimoIdx > -1 ? Number(process.argv[minimoIdx + 1]) : 3;
- const expectIdx = process.argv.indexOf("--expect-count");
- const EXPECT_COUNT = expectIdx > -1 ? Number(process.argv[expectIdx + 1]) : null;
- const SETORES = Object.keys(SETOR_NOMES).filter(setorAvaliavel);
- const FAIXAS_SCORE = [
- { nome: "baixa", min: 1, max: 4 },
- { nome: "media", min: 5, max: 7 },
- { nome: "alta", min: 8, max: 10 }
- ];
- function distribuirComLimite(quantidade, chaves, limitePorChave) {
- const alocacao = Object.fromEntries(chaves.map((c) => [c, 0]));
- let restante = quantidade;
- let pendentes = chaves.filter((c) => limitePorChave[c] > 0);
- while (restante > 0 && pendentes.length > 0) {
- const fatia = Math.max(1, Math.floor(restante / pendentes.length));
- const proximaRodada = [];
- for (const c of pendentes) {
- if (restante <= 0) break;
- const espaco = limitePorChave[c] - alocacao[c];
- if (espaco <= 0) continue;
- const add = Math.min(fatia, espaco, restante);
- alocacao[c] += add;
- restante -= add;
- if (alocacao[c] < limitePorChave[c]) proximaRodada.push(c);
- }
- if (proximaRodada.length === pendentes.length && proximaRodada.every((c) => limitePorChave[c] - alocacao[c] < 1)) break;
- pendentes = proximaRodada;
- }
- return alocacao;
- }
- async function volumePorSetor() {
- const linhas = await AtendimentoAvaliacao.query()
- .where("Avaliavel", true)
- .join("atendimentos as a", "a.Id", "atendimento_avaliacoes.AtendimentoId")
- .whereIn("a.Setor", SETORES)
- .groupBy("a.Setor")
- .select("a.Setor")
- .count("atendimento_avaliacoes.AtendimentoId as total");
- const volume = Object.fromEntries(SETORES.map((s) => [s, 0]));
- for (const l of linhas) volume[l.Setor] = Number(l.total);
- return volume;
- }
- async function jaAmostradoPorSetor() {
- const linhas = await AtendimentoGoldenLabel.query()
- .join("atendimentos as a", "a.Id", "atendimento_golden_labels.AtendimentoId")
- .whereIn("a.Setor", SETORES)
- .groupBy("a.Setor")
- .select("a.Setor")
- .count("atendimento_golden_labels.AtendimentoId as total");
- const jaAmostrado = Object.fromEntries(SETORES.map((s) => [s, 0]));
- for (const l of linhas) jaAmostrado[l.Setor] = Number(l.total);
- return jaAmostrado;
- }
- function calcularAlocacao(volume) {
- const alocacao = Object.fromEntries(SETORES.map((s) => [s, Math.min(MINIMO, volume[s])]));
- let restante = TAMANHO - Object.values(alocacao).reduce((a, b) => a + b, 0);
- if (restante < 0) restante = 0;
- const volumeTotal = SETORES.reduce((a, s) => a + volume[s], 0) || 1;
- const fracoes = SETORES.map((s) => {
- const proporcional = (volume[s] / volumeTotal) * restante;
- const inteiro = Math.floor(proporcional);
- return { setor: s, inteiro, resto: proporcional - inteiro };
- });
- for (const f of fracoes) alocacao[f.setor] += f.inteiro;
- let distribuido = fracoes.reduce((a, f) => a + f.inteiro, 0);
- let sobra = restante - distribuido;
- fracoes.sort((a, b) => b.resto - a.resto);
- for (const f of fracoes) {
- if (sobra <= 0) break;
- if (alocacao[f.setor] >= volume[f.setor]) continue;
- alocacao[f.setor] += 1;
- sobra -= 1;
- }
- for (const s of SETORES) alocacao[s] = Math.min(alocacao[s], volume[s]);
- return alocacao;
- }
- function baseQuery(setor, faixa) {
- const jaNaAmostra = AtendimentoGoldenLabel.query().select("AtendimentoId");
- let query = AtendimentoAvaliacao.query()
- .where("Avaliavel", true)
- .join("atendimentos as a", "a.Id", "atendimento_avaliacoes.AtendimentoId")
- .where("a.Setor", setor)
- .whereNotIn("atendimento_avaliacoes.AtendimentoId", jaNaAmostra);
- if (faixa) query = query.whereBetween("atendimento_avaliacoes.ScoreAtendente", [faixa.min, faixa.max]);
- return query;
- }
- async function volumePorFaixa(setor) {
- const volume = {};
- for (const f of FAIXAS_SCORE) {
- const { c } = await baseQuery(setor, f).count("* as c").first();
- volume[f.nome] = Number(c);
- }
- return volume;
- }
- async function sortearPorSetor(setor, quantidade) {
- if (quantidade <= 0) return [];
- const volumeFaixas = await volumePorFaixa(setor);
- const alocacaoFaixas = distribuirComLimite(
- quantidade,
- FAIXAS_SCORE.map((f) => f.nome),
- volumeFaixas
- );
- const resultado = [];
- for (const f of FAIXAS_SCORE) {
- const qtd = alocacaoFaixas[f.nome];
- if (qtd <= 0) continue;
- let query = baseQuery(setor, f)
- .orderByRaw("RAND()")
- .limit(qtd)
- .select(
- "atendimento_avaliacoes.AtendimentoId as AtendimentoId",
- "a.Codigo as Codigo",
- "a.Setor as Setor",
- "atendimento_avaliacoes.ScoreAtendente as ScoreAtendente"
- );
- if (resultado.length) query = query.whereNotIn("atendimento_avaliacoes.AtendimentoId", resultado.map((r) => r.AtendimentoId));
- resultado.push(...(await query));
- }
- return resultado;
- }
- async function resetarAmostra() {
- const existentes = await AtendimentoGoldenLabel.query();
- console.log(`[gerar-amostra-golden-set] --reset: descartando ${existentes.length} linhas existentes (backup antes de apagar).`);
- const backupDir = path.resolve(__dirname, "logs");
- fs.mkdirSync(backupDir, { recursive: true });
- const backupPath = path.join(backupDir, `reset-golden-set-${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
- fs.writeFileSync(backupPath, JSON.stringify(existentes, null, 2));
- console.log(`[gerar-amostra-golden-set] backup da amostra descartada salvo em ${backupPath}`);
- if (APLICAR && existentes.length > 0) {
- await AtendimentoGoldenLabel.query().delete();
- console.log("[gerar-amostra-golden-set] linhas apagadas.");
- }
- }
- async function main() {
- if (RESET) await resetarAmostra();
- const volume = await volumePorSetor();
- const jaAmostrado = RESET ? Object.fromEntries(SETORES.map((s) => [s, 0])) : await jaAmostradoPorSetor();
- const alocacao = calcularAlocacao(volume);
- console.log(`[gerar-amostra-golden-set] alvo total: ${TAMANHO} | mínimo por setor: ${MINIMO}`);
- console.log("[gerar-amostra-golden-set] volume disponível / já amostrado / alocação-alvo por setor:");
- for (const s of SETORES) {
- console.log(` ${s}: volume=${volume[s]} | já amostrado=${jaAmostrado[s]} | alocação-alvo=${alocacao[s]}`);
- }
- const amostraFinal = [];
- const faltantePorSetor = {};
- for (const s of SETORES) {
- const faltam = Math.max(0, alocacao[s] - jaAmostrado[s]);
- faltantePorSetor[s] = faltam;
- if (faltam > 0) {
- const sorteados = await sortearPorSetor(s, faltam);
- amostraFinal.push(...sorteados);
- }
- }
- console.log(`[gerar-amostra-golden-set] ${amostraFinal.length} atendimentos novos serão adicionados à amostra.`);
- if (EXPECT_COUNT !== null && amostraFinal.length !== EXPECT_COUNT) {
- console.error(`[gerar-amostra-golden-set] abortando: esperava ${EXPECT_COUNT}, achei ${amostraFinal.length}.`);
- process.exit(1);
- }
- const backupDir = path.resolve(__dirname, "logs");
- fs.mkdirSync(backupDir, { recursive: true });
- const backupPath = path.join(backupDir, `gerar-amostra-golden-set-${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
- fs.writeFileSync(
- backupPath,
- JSON.stringify(
- {
- config: { tamanho: TAMANHO, minimo: MINIMO },
- volumePorSetor: volume,
- jaAmostradoPorSetor: jaAmostrado,
- alocacaoAlvoPorSetor: alocacao,
- faltantePorSetor,
- amostraFinal
- },
- null,
- 2
- )
- );
- console.log(`[gerar-amostra-golden-set] detalhe do sorteio salvo em ${backupPath}`);
- if (!APLICAR) {
- console.log("[gerar-amostra-golden-set] dry-run (nenhuma alteração feita). Use --apply para gravar.");
- process.exit(0);
- }
- if (amostraFinal.length === 0) {
- console.log("[gerar-amostra-golden-set] nada para inserir.");
- process.exit(0);
- }
- for (const { AtendimentoId } of amostraFinal) {
- await AtendimentoGoldenLabel.query().insert({ AtendimentoId, Status: "pendente" });
- }
- console.log(`[gerar-amostra-golden-set] ${amostraFinal.length} linhas inseridas com Status='pendente'.`);
- process.exit(0);
- }
- main().catch((err) => {
- console.error("[gerar-amostra-golden-set] falha fatal:", err);
- process.exit(1);
- });
|