feat(v2): candidate pool SQL (admin exclusion preserved, no hard LIMIT 10) + verbatim legacy SQL for shadow mode
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
60015c0972
commit
00bb89f4f4
@@ -1372,7 +1372,7 @@ ORDER BY tickets_id, date ASC;
|
||||
`n8n/nodes/build-sql-candidatos.node.js` (assume `parseConfig` já inlined no escopo pelo build script, Task 12):
|
||||
|
||||
```js
|
||||
// @requires logic/config.js
|
||||
// @requires config.js
|
||||
const config = parseConfig($env);
|
||||
const glpiBaseUrl = String($('Config - Coordenador GLPI v2').first().json.glpi_base_url || '').replace(/\/$/, '').replace(/'/g, "''");
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// @requires config.js
|
||||
const config = parseConfig($env);
|
||||
const glpiBaseUrl = String($('Config - Coordenador GLPI v2').first().json.glpi_base_url || '').replace(/\/$/, '').replace(/'/g, "''");
|
||||
|
||||
const queryTemplate = /* __SQL_CANDIDATOS_V2__ */;
|
||||
|
||||
const query = queryTemplate
|
||||
.replaceAll('{DIAS_SEM_INTERACAO_ALERTA}', String(config.diasSemInteracaoAlerta))
|
||||
.replaceAll('{HORAS_SLA_RISCO}', String(config.horasSlaRisco))
|
||||
.replaceAll('{GLPI_BASE_URL}', glpiBaseUrl)
|
||||
.replaceAll('{CANDIDATE_POOL_SIZE}', String(config.candidatePoolSize));
|
||||
|
||||
return [{ json: { query, config } }];
|
||||
@@ -0,0 +1,19 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
test('SQL de candidatos V2 preserva a exclusao administrativa do V1', () => {
|
||||
const sql = fs.readFileSync(path.join(__dirname, '..', '..', 'sql', 'candidatos_v2_glpi.sql'), 'utf8');
|
||||
assert.ok(sql.includes("NOT LIKE 'ADMINISTRATIVO%'"));
|
||||
assert.ok(sql.includes("NOT LIKE 'DESENVOLVIMENTO%'"));
|
||||
assert.ok(sql.includes("NOT LIKE 'INFRA%'"));
|
||||
assert.ok(sql.includes('is_deleted = false'));
|
||||
assert.ok(sql.includes('status NOT IN (5, 6)'));
|
||||
});
|
||||
|
||||
test('SQL de candidatos V2 NAO tem mais LIMIT fixo em 10 (funil maior)', () => {
|
||||
const sql = fs.readFileSync(path.join(__dirname, '..', '..', 'sql', 'candidatos_v2_glpi.sql'), 'utf8');
|
||||
assert.ok(sql.includes('LIMIT {CANDIDATE_POOL_SIZE}'));
|
||||
assert.ok(!sql.includes('LIMIT 10'));
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
const config = $('Config - Coordenador GLPI v2').first().json;
|
||||
const glpiBaseUrl = String(config.glpi_base_url || '').replace(/\/$/, '').replace(/'/g, "''");
|
||||
|
||||
const queryTemplate = /* __SQL_LEGADO_V1_VERBATIM__ */;
|
||||
|
||||
const query = queryTemplate
|
||||
.replaceAll('{DIAS_SEM_INTERACAO_ALERTA}', '2')
|
||||
.replaceAll('{HORAS_SLA_RISCO}', '8')
|
||||
.replaceAll('{GLPI_BASE_URL}', glpiBaseUrl);
|
||||
|
||||
return [{ json: { query } }];
|
||||
@@ -0,0 +1,82 @@
|
||||
-- Candidatos V2: mesma base/exclusao do V1, sem corte em blocos.
|
||||
-- Parametros substituidos pelo node: {DIAS_SEM_INTERACAO_ALERTA} {HORAS_SLA_RISCO} {GLPI_BASE_URL} {CANDIDATE_POOL_SIZE}
|
||||
|
||||
WITH ultima_interacao AS (
|
||||
SELECT tickets_id, MAX(data_interacao) AS ultima_interacao_em
|
||||
FROM (
|
||||
SELECT tickets_id, MAX(data_interacao) AS data_interacao
|
||||
FROM (
|
||||
SELECT items_id AS tickets_id, date AS data_interacao FROM glpi_itilfollowups WHERE itemtype = 'Ticket' AND is_private = 0
|
||||
UNION ALL
|
||||
SELECT items_id AS tickets_id, date_mod AS data_interacao FROM glpi_itilfollowups WHERE itemtype = 'Ticket' AND is_private = 0
|
||||
UNION ALL
|
||||
SELECT items_id AS tickets_id, date_creation AS data_interacao FROM glpi_itilfollowups WHERE itemtype = 'Ticket' AND is_private = 0
|
||||
) followup_datas
|
||||
WHERE data_interacao IS NOT NULL
|
||||
GROUP BY tickets_id
|
||||
UNION ALL
|
||||
SELECT tickets_id, MAX(data_interacao) AS data_interacao
|
||||
FROM (
|
||||
SELECT tickets_id, date AS data_interacao FROM glpi_tickettasks
|
||||
UNION ALL
|
||||
SELECT tickets_id, date_mod AS data_interacao FROM glpi_tickettasks
|
||||
UNION ALL
|
||||
SELECT tickets_id, date_creation AS data_interacao FROM glpi_tickettasks
|
||||
) task_datas
|
||||
WHERE data_interacao IS NOT NULL
|
||||
GROUP BY tickets_id
|
||||
) interacoes
|
||||
GROUP BY tickets_id
|
||||
), tecnicos AS (
|
||||
SELECT tu.tickets_id,
|
||||
GROUP_CONCAT(TRIM(CONCAT(COALESCE(u.firstname, ''), ' ', COALESCE(u.realname, u.name))) SEPARATOR ', ') AS tecnicos
|
||||
FROM glpi_tickets_users tu
|
||||
LEFT JOIN glpi_users u ON u.id = tu.users_id
|
||||
WHERE tu.type = 2
|
||||
GROUP BY tu.tickets_id
|
||||
), chamados_base AS (
|
||||
SELECT
|
||||
t.id, t.name AS titulo, t.status,
|
||||
CASE t.status WHEN 1 THEN 'Novo' WHEN 2 THEN 'Em atendimento' WHEN 3 THEN 'Planejado' WHEN 4 THEN 'Pendente' ELSE CONCAT('Status ', t.status) END AS status_nome,
|
||||
t.date AS aberto_em, t.date_mod AS modificado_em, t.time_to_resolve AS sla_resolver_ate,
|
||||
t.priority, t.urgency, t.impact,
|
||||
COALESCE(e.completename, e.name, 'Nao informado') AS entidade,
|
||||
COALESCE(c.completename, c.name, 'Sem categoria') AS categoria,
|
||||
COALESCE(tecnicos.tecnicos, 'Sem tecnico atribuido') AS tecnico_atribuido,
|
||||
GREATEST(COALESCE(ultima_interacao.ultima_interacao_em, '1970-01-01 00:00:00'), COALESCE(t.date_mod, '1970-01-01 00:00:00'), COALESCE(t.date, '1970-01-01 00:00:00')) AS ultima_interacao_em,
|
||||
TIMESTAMPDIFF(DAY, t.date, NOW()) AS dias_aberto,
|
||||
GREATEST(TIMESTAMPDIFF(DAY, GREATEST(COALESCE(ultima_interacao.ultima_interacao_em, '1970-01-01 00:00:00'), COALESCE(t.date_mod, '1970-01-01 00:00:00'), COALESCE(t.date, '1970-01-01 00:00:00')), NOW()), 0) AS dias_sem_interacao,
|
||||
TIMESTAMPDIFF(HOUR, NOW(), t.time_to_resolve) AS horas_ate_sla,
|
||||
CASE WHEN t.time_to_resolve IS NOT NULL AND t.time_to_resolve < NOW() THEN 1 ELSE 0 END AS sla_vencido,
|
||||
CASE WHEN t.time_to_resolve IS NOT NULL AND t.time_to_resolve >= NOW() AND TIMESTAMPDIFF(HOUR, NOW(), t.time_to_resolve) <= {HORAS_SLA_RISCO} THEN 1 ELSE 0 END AS sla_em_risco,
|
||||
CASE WHEN tecnicos.tecnicos IS NULL THEN 1 ELSE 0 END AS sem_tecnico,
|
||||
CONCAT('{GLPI_BASE_URL}', '/front/ticket.form.php?id=', t.id) AS link_glpi
|
||||
FROM glpi_tickets t
|
||||
LEFT JOIN glpi_entities e ON e.id = t.entities_id
|
||||
LEFT JOIN glpi_itilcategories c ON c.id = t.itilcategories_id
|
||||
LEFT JOIN ultima_interacao ON ultima_interacao.tickets_id = t.id
|
||||
LEFT JOIN tecnicos ON tecnicos.tickets_id = t.id
|
||||
WHERE t.is_deleted = false
|
||||
AND t.status NOT IN (5, 6)
|
||||
AND COALESCE(c.completename, c.name, '') NOT LIKE 'ADMINISTRATIVO%'
|
||||
AND COALESCE(c.completename, c.name, '') NOT LIKE 'DESENVOLVIMENTO%'
|
||||
AND COALESCE(c.completename, c.name, '') NOT LIKE 'INFRA%'
|
||||
), chamados_ranked AS (
|
||||
SELECT
|
||||
id, titulo, status, status_nome, aberto_em, modificado_em, ultima_interacao_em,
|
||||
dias_aberto, dias_sem_interacao, sla_resolver_ate, horas_ate_sla, sla_vencido, sla_em_risco,
|
||||
priority AS prioridade, urgency AS urgencia, impact AS impacto,
|
||||
entidade, categoria, tecnico_atribuido, sem_tecnico, link_glpi,
|
||||
(
|
||||
CASE WHEN sla_vencido = 1 THEN 100 ELSE 0 END
|
||||
+ CASE WHEN sla_em_risco = 1 THEN 45 ELSE 0 END
|
||||
+ CASE WHEN dias_sem_interacao >= 7 THEN 35 WHEN dias_sem_interacao >= 3 THEN 25 WHEN dias_sem_interacao >= {DIAS_SEM_INTERACAO_ALERTA} THEN 10 ELSE 0 END
|
||||
+ LEAST(dias_aberto, 30)
|
||||
+ (priority * 8) + (urgency * 5) + (impact * 5)
|
||||
+ CASE WHEN sem_tecnico = 1 THEN 25 ELSE 0 END
|
||||
) AS score_criticidade
|
||||
FROM chamados_base
|
||||
)
|
||||
SELECT * FROM chamados_ranked
|
||||
ORDER BY score_criticidade DESC, sla_vencido DESC, dias_sem_interacao DESC, dias_aberto DESC
|
||||
LIMIT {CANDIDATE_POOL_SIZE};
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Followups publicos dos candidatos (IDs interpolados pelo node, validados como inteiros).
|
||||
SELECT items_id AS tickets_id, id, date, content AS texto
|
||||
FROM glpi_itilfollowups
|
||||
WHERE itemtype = 'Ticket' AND is_private = 0 AND items_id IN ({IDS})
|
||||
ORDER BY items_id, date ASC;
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Tarefas dos candidatos. Coluna de status a confirmar via DESCRIBE glpi_tickettasks
|
||||
-- quando a credencial MySQL estiver disponivel (Task 14) -- ver nota no plano.
|
||||
SELECT tickets_id, id, date, state AS status, content AS texto
|
||||
FROM glpi_tickettasks
|
||||
WHERE tickets_id IN ({IDS})
|
||||
ORDER BY tickets_id, date ASC;
|
||||
Reference in New Issue
Block a user