Initial GLPI daily report workflow
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
N8N_PORT=5678
|
||||
N8N_HOST=localhost
|
||||
N8N_PROTOCOL=http
|
||||
GENERIC_TIMEZONE=America/Sao_Paulo
|
||||
TZ=America/Sao_Paulo
|
||||
|
||||
N8N_BASIC_AUTH_ACTIVE=true
|
||||
N8N_BASIC_AUTH_USER=admin
|
||||
N8N_BASIC_AUTH_PASSWORD=troque-esta-senha
|
||||
|
||||
# Use uma chave fixa para manter credenciais do N8N legiveis entre reinicios.
|
||||
# Sugestao: gere uma string aleatoria com pelo menos 32 caracteres.
|
||||
N8N_ENCRYPTION_KEY=troque-esta-chave-com-32-caracteres
|
||||
@@ -0,0 +1 @@
|
||||
.env
|
||||
@@ -0,0 +1,230 @@
|
||||
# Resumo Diário GLPI - MVP
|
||||
|
||||
Workflow N8N inicial para gerar um resumo diário de chamados usando dados simulados.
|
||||
|
||||
## Subir o N8N com Docker
|
||||
|
||||
1. Copie `.env.example` para `.env`.
|
||||
2. Edite `.env` e troque `N8N_BASIC_AUTH_PASSWORD` e `N8N_ENCRYPTION_KEY`.
|
||||
3. Suba o container:
|
||||
|
||||
```powershell
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
4. Acesse:
|
||||
|
||||
```text
|
||||
http://localhost:5678
|
||||
```
|
||||
|
||||
5. Para parar:
|
||||
|
||||
```powershell
|
||||
docker compose down
|
||||
```
|
||||
|
||||
## Arquivo
|
||||
|
||||
- `Resumo Diario GLPI - MVP.n8n.json`
|
||||
- `Resumo Diario GLPI - Real Slack.n8n.json`
|
||||
- `Resumo Diario GLPI - SQL Slack.n8n.json`
|
||||
- `Resumo Semanal GLPI - SQL Slack.n8n.json`
|
||||
- `sql/indicadores_diario_glpi.sql`
|
||||
- `sql/indicadores_semana_glpi.sql`
|
||||
|
||||
## Workflow Diário Recomendado
|
||||
|
||||
Use este workflow para o relatório diário correto:
|
||||
|
||||
- `Resumo Diario GLPI - SQL Slack.n8n.json`
|
||||
|
||||
Essa versão usa a regra aprendida com a query semanal, mas aplicada por dia:
|
||||
|
||||
- chamados abertos no dia: `DATE(glpi_tickets.date) = data_alvo`
|
||||
- chamados fechados no dia: `DATE(glpi_tickets.closedate) = data_alvo`
|
||||
- chamados fechados no dia podem ter sido abertos em qualquer data anterior
|
||||
- filtro `is_deleted = false`
|
||||
- filtro de categorias pelas árvores `7`, `16`, `17` e `18` usando `ancestors_cache`
|
||||
|
||||
### Como configurar o diário SQL
|
||||
|
||||
1. Importe `Resumo Diario GLPI - SQL Slack.n8n.json`.
|
||||
2. Configure a credencial MySQL no node `MySQL - Indicadores diario GLPI`.
|
||||
3. Abra `Config - Diario SQL` e preencha:
|
||||
|
||||
- `data_alvo`: deixe vazio para o dia atual ou use `2026-07-03`
|
||||
- `slack_webhook_url`
|
||||
- `openai_api_key`
|
||||
- `openai_model`, padrão `gpt-5.5`
|
||||
|
||||
4. Execute pelo `Manual Trigger`.
|
||||
|
||||
Se a OpenAI falhar, o workflow envia o resumo fallback sem IA com os números SQL.
|
||||
|
||||
## Workflow SQL Recomendado
|
||||
|
||||
Para bater exatamente com os indicadores do GLPI, use:
|
||||
|
||||
- `Resumo Semanal GLPI - SQL Slack.n8n.json`
|
||||
|
||||
Essa versão consulta diretamente o banco MySQL/MariaDB do GLPI e executa a mesma lógica da query de referência:
|
||||
|
||||
- abertos pela coluna `glpi_tickets.date`
|
||||
- fechados pela coluna `glpi_tickets.closedate`
|
||||
- filtro `is_deleted = false`
|
||||
- filtro de categorias pelas árvores `7`, `16`, `17` e `18` usando `ancestors_cache`
|
||||
|
||||
Isso é diferente da API REST, que pode não trazer todos os chamados necessários para reproduzir o indicador semanal, principalmente porque chamados fechados na semana podem ter sido abertos em semanas anteriores.
|
||||
|
||||
### Como configurar
|
||||
|
||||
1. Importe `Resumo Semanal GLPI - SQL Slack.n8n.json`.
|
||||
2. Configure a credencial MySQL no node `MySQL - Indicadores semana GLPI`.
|
||||
3. Abra `Config - Slack e OpenAI` e preencha:
|
||||
|
||||
- `slack_webhook_url`
|
||||
- `openai_api_key`
|
||||
- `openai_model`, padrão `gpt-5.5`
|
||||
|
||||
4. Execute pelo `Manual Trigger`.
|
||||
|
||||
Se a OpenAI falhar, o workflow envia um resumo fallback sem IA com os números da query.
|
||||
|
||||
## Como usar
|
||||
|
||||
1. No N8N, vá em `Workflows`.
|
||||
2. Clique em `Import from File`.
|
||||
3. Selecione `Resumo Diario GLPI - MVP.n8n.json`.
|
||||
4. Abra o node `OpenAI - Gerar resumo`.
|
||||
5. Selecione sua credencial da OpenAI.
|
||||
6. Execute o workflow pelo `Manual Trigger`.
|
||||
|
||||
## Fluxo
|
||||
|
||||
```text
|
||||
Manual Trigger
|
||||
↓
|
||||
Set - Dados simulados
|
||||
↓
|
||||
Code - Calcular indicadores
|
||||
↓
|
||||
OpenAI - Gerar resumo
|
||||
```
|
||||
|
||||
## Próxima etapa
|
||||
|
||||
Depois que o MVP estiver gerando o texto corretamente, substitua o node `Set - Dados simulados` pelos nodes HTTP Request do GLPI.
|
||||
|
||||
## Workflow Real com GLPI e Slack
|
||||
|
||||
Importe o arquivo:
|
||||
|
||||
- `Resumo Diario GLPI - Real Slack.n8n.json`
|
||||
|
||||
Depois da importação, abra o node `Config - GLPI e Slack` e preencha:
|
||||
|
||||
- `glpi_url`: URL base do GLPI, exemplo `https://glpi.suaempresa.com.br`
|
||||
- `glpi_app_token`: App Token do GLPI
|
||||
- `glpi_user_token`: User Token do usuário de integração
|
||||
- `slack_webhook_url`: URL do Incoming Webhook do Slack
|
||||
- `range_tickets`: quantidade buscada na API, exemplo `0-999`
|
||||
- `page_size`: tamanho de cada pagina buscada no GLPI, padrao `1000`
|
||||
- `page_count`: quantidade de paginas por ordenacao, padrao `2`
|
||||
- `data_alvo`: data do relatório. Deixe vazio para usar o dia atual, ou informe uma data como `2026-07-03` ou `03/07/26`
|
||||
- `openai_api_key`: chave da OpenAI usada pelo node `OpenAI - Gerar resumo`
|
||||
- `openai_model`: modelo usado na OpenAI, padrão `gpt-5.5`
|
||||
|
||||
O node `OpenAI - Gerar resumo` usa `HTTP Request` direto para `https://api.openai.com/v1/chat/completions`.
|
||||
|
||||
O workflow busca tickets em paginas e combina duas ordenacoes:
|
||||
|
||||
- `id DESC`, para capturar chamados abertos recentemente
|
||||
- `date_mod DESC`, para capturar chamados antigos que foram modificados, solucionados ou fechados recentemente
|
||||
|
||||
Depois ele remove duplicados por `id` e filtra no N8N pela `data_alvo`. O cálculo separa:
|
||||
|
||||
- chamados abertos na data, usando `date_creation`, `date` ou `data_abertura`
|
||||
- chamados fechados na data, usando `solvedate`, `closedate` ou `data_fechamento`
|
||||
- pendentes, usando o status atual do ticket filtrado
|
||||
|
||||
Na base atual, o GLPI informou `1216` tickets. Por isso o padrão recomendado é `page_size=1000` e `page_count=2`, cobrindo `0-1999` por ordenação sem estourar o total. Se aumentar `page_count`, o GLPI pode retornar `ERROR_RANGE_EXCEED_TOTAL` para páginas acima do total existente.
|
||||
|
||||
Execute pelo `Manual Trigger` primeiro. Depois que validar, troque por `Schedule Trigger`.
|
||||
|
||||
Para gerar o relatório da sexta-feira `03/07/2026`, use no campo `data_alvo`:
|
||||
|
||||
```text
|
||||
2026-07-03
|
||||
```
|
||||
|
||||
### Criar Webhook no Slack
|
||||
|
||||
1. Acesse `https://api.slack.com/apps`.
|
||||
2. Crie ou abra um app do Slack.
|
||||
3. Ative `Incoming Webhooks`.
|
||||
4. Crie um webhook para o canal desejado.
|
||||
5. Copie a URL para o campo `slack_webhook_url`.
|
||||
|
||||
### Fluxo Real
|
||||
|
||||
```text
|
||||
Manual Trigger
|
||||
↓
|
||||
Config - GLPI e Slack
|
||||
↓
|
||||
HTTP - GLPI initSession
|
||||
↓
|
||||
HTTP - GLPI buscar tickets
|
||||
↓
|
||||
Code - Filtrar chamados de hoje
|
||||
↓
|
||||
Code - Calcular indicadores
|
||||
↓
|
||||
OpenAI - Gerar resumo
|
||||
↓
|
||||
HTTP - Slack enviar mensagem
|
||||
↓
|
||||
HTTP - GLPI killSession
|
||||
```
|
||||
|
||||
### Observação
|
||||
|
||||
Os campos retornados pelo GLPI podem variar conforme versão e configuração. Se o node `Code - Filtrar chamados de hoje` retornar categoria ou cliente como ID em vez de nome, ajuste o mapeamento depois de ver o retorno real da API.
|
||||
|
||||
### Erro Slack `400 - no_text`
|
||||
|
||||
Se o node `HTTP - Slack enviar mensagem` retornar `400 - no_text`, o corpo da requisição foi enviado sem o campo `text`.
|
||||
|
||||
No node `HTTP - Slack enviar mensagem`, configure:
|
||||
|
||||
- `Method`: `POST`
|
||||
- `Send Body`: ativo
|
||||
- `Body Content Type`: `JSON`
|
||||
- `Specify Body`: `Using JSON`
|
||||
- `JSON Body`:
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "{{ $json.text }}"
|
||||
}
|
||||
```
|
||||
|
||||
### Erro OpenAI `too many requests`
|
||||
|
||||
Se o node `OpenAI - Gerar resumo` retornar `The service is receiving too many requests from you`, a OpenAI respondeu com limite de uso/rate limit.
|
||||
|
||||
Verifique:
|
||||
|
||||
- Se a chave `openai_api_key` está correta
|
||||
- Se a conta OpenAI tem crédito/billing ativo
|
||||
- Se houve muitas execuções seguidas em pouco tempo
|
||||
- Se o modelo `gpt-4o-mini` está disponível para a chave usada
|
||||
|
||||
O workflow importável já está configurado com retry:
|
||||
|
||||
- 3 tentativas
|
||||
- 30 segundos entre tentativas
|
||||
- limite de resposta de 700 tokens
|
||||
|
||||
Se a OpenAI continuar falhando, o workflow continua a execução e envia ao Slack um resumo técnico gerado pelo node `Code - Preparar mensagem Slack`, sem IA.
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"name": "Resumo Diário GLPI - MVP",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "manual-trigger",
|
||||
"name": "Manual Trigger",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
260,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "chamados-simulados",
|
||||
"name": "chamados",
|
||||
"type": "array",
|
||||
"value": "=[{\"id\":101,\"cliente\":\"Cliente A\",\"categoria\":\"VPN\",\"status\":\"Aberto\",\"prioridade\":\"Média\",\"data_abertura\":\"2026-07-04 08:20\",\"data_fechamento\":null},{\"id\":102,\"cliente\":\"Cliente B\",\"categoria\":\"Impressora\",\"status\":\"Fechado\",\"prioridade\":\"Baixa\",\"data_abertura\":\"2026-07-04 09:10\",\"data_fechamento\":\"2026-07-04 10:30\"},{\"id\":103,\"cliente\":\"Cliente A\",\"categoria\":\"VPN\",\"status\":\"Pendente\",\"prioridade\":\"Alta\",\"data_abertura\":\"2026-07-04 11:00\",\"data_fechamento\":null},{\"id\":104,\"cliente\":\"Cliente C\",\"categoria\":\"Acesso remoto\",\"status\":\"Fechado\",\"prioridade\":\"Média\",\"data_abertura\":\"2026-07-04 13:45\",\"data_fechamento\":\"2026-07-04 15:00\"},{\"id\":105,\"cliente\":\"Cliente A\",\"categoria\":\"VPN\",\"status\":\"Aberto\",\"prioridade\":\"Alta\",\"data_abertura\":\"2026-07-04 16:10\",\"data_fechamento\":null}]"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "set-dados-simulados",
|
||||
"name": "Set - Dados simulados",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
500,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const chamados = $input.first().json.chamados || [];\n\nfunction contarPorCampo(lista, campo) {\n return lista.reduce((acc, item) => {\n const valor = item[campo] || 'Não informado';\n acc[valor] = (acc[valor] || 0) + 1;\n return acc;\n }, {});\n}\n\nfunction ordenarContagem(objeto) {\n return Object.entries(objeto)\n .sort((a, b) => b[1] - a[1])\n .reduce((acc, [chave, valor]) => {\n acc[chave] = valor;\n return acc;\n }, {});\n}\n\nconst statusAbertosOuPendentes = ['Aberto', 'Novo', 'Processando', 'Pendente'];\nconst statusFechados = ['Fechado', 'Solucionado', 'Encerrado'];\nconst prioridadesAltas = ['Alta', 'Muito alta', 'Crítica'];\n\nconst totalChamados = chamados.length;\n\nconst abertosOuPendentes = chamados.filter(c =>\n statusAbertosOuPendentes.includes(c.status)\n).length;\n\nconst fechados = chamados.filter(c =>\n statusFechados.includes(c.status)\n).length;\n\nconst pendentes = chamados.filter(c =>\n !statusFechados.includes(c.status)\n).length;\n\nconst altaPrioridadePendentes = chamados.filter(c =>\n prioridadesAltas.includes(c.prioridade) &&\n !statusFechados.includes(c.status)\n).length;\n\nconst porCategoria = ordenarContagem(contarPorCampo(chamados, 'categoria'));\nconst porCliente = ordenarContagem(contarPorCampo(chamados, 'cliente'));\nconst porPrioridade = ordenarContagem(contarPorCampo(chamados, 'prioridade'));\nconst porStatus = ordenarContagem(contarPorCampo(chamados, 'status'));\n\nreturn [\n {\n json: {\n data_relatorio: new Date().toISOString(),\n total_chamados: totalChamados,\n chamados_abertos_ou_pendentes: abertosOuPendentes,\n chamados_fechados: fechados,\n chamados_pendentes: pendentes,\n chamados_alta_prioridade_pendentes: altaPrioridadePendentes,\n chamados_por_categoria: porCategoria,\n chamados_por_cliente: porCliente,\n chamados_por_prioridade: porPrioridade,\n chamados_por_status: porStatus,\n chamados_detalhados: chamados\n }\n }\n];"
|
||||
},
|
||||
"id": "code-calcular-indicadores",
|
||||
"name": "Code - Calcular indicadores",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
760,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "chat",
|
||||
"operation": "message",
|
||||
"model": "gpt-4o-mini",
|
||||
"messages": {
|
||||
"values": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Você é um analista de infraestrutura gerando um resumo diário de suporte para a equipe técnica. Use apenas os dados fornecidos. Não invente números, clientes, categorias ou chamados. Gere um resumo em português do Brasil, objetivo, profissional e fácil de entender."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Estrutura obrigatória:\n\n1. Resumo geral do dia\n2. Chamados abertos, fechados e pendentes\n3. Categorias mais recorrentes\n4. Clientes com maior volume\n5. Chamados de maior atenção\n6. Recomendações para o próximo dia\n\nDados do dia:\n\n{{ JSON.stringify($json) }}\n\nO resultado deve ser algo pronto para enviar no grupo, e-mail ou relatório."
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"temperature": 0.2
|
||||
}
|
||||
},
|
||||
"id": "openai-gerar-resumo",
|
||||
"name": "OpenAI - Gerar resumo",
|
||||
"type": "n8n-nodes-base.openAi",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
1020,
|
||||
300
|
||||
],
|
||||
"credentials": {
|
||||
"openAiApi": {
|
||||
"id": "",
|
||||
"name": "OpenAI account"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Manual Trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Set - Dados simulados",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Set - Dados simulados": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code - Calcular indicadores",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code - Calcular indicadores": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "OpenAI - Gerar resumo",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": false
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
{
|
||||
"name": "Resumo Diário GLPI - Real Slack",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "manual-trigger-real",
|
||||
"name": "Manual Trigger",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
180,
|
||||
320
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "glpi-url",
|
||||
"name": "glpi_url",
|
||||
"type": "string",
|
||||
"value": "https://SEU-GLPI-AQUI"
|
||||
},
|
||||
{
|
||||
"id": "glpi-app-token",
|
||||
"name": "glpi_app_token",
|
||||
"type": "string",
|
||||
"value": "COLE_SEU_APP_TOKEN_AQUI"
|
||||
},
|
||||
{
|
||||
"id": "glpi-user-token",
|
||||
"name": "glpi_user_token",
|
||||
"type": "string",
|
||||
"value": "COLE_SEU_USER_TOKEN_AQUI"
|
||||
},
|
||||
{
|
||||
"id": "slack-webhook-url",
|
||||
"name": "slack_webhook_url",
|
||||
"type": "string",
|
||||
"value": "https://hooks.slack.com/services/SEU/WEBHOOK/AQUI"
|
||||
},
|
||||
{
|
||||
"id": "range-tickets",
|
||||
"name": "range_tickets",
|
||||
"type": "string",
|
||||
"value": "0-999"
|
||||
},
|
||||
{
|
||||
"id": "data-alvo",
|
||||
"name": "data_alvo",
|
||||
"type": "string",
|
||||
"value": ""
|
||||
},
|
||||
{
|
||||
"id": "openai-api-key",
|
||||
"name": "openai_api_key",
|
||||
"type": "string",
|
||||
"value": "COLE_SUA_OPENAI_API_KEY_AQUI"
|
||||
},
|
||||
{
|
||||
"id": "openai-model",
|
||||
"name": "openai_model",
|
||||
"type": "string",
|
||||
"value": "gpt-5.5"
|
||||
},
|
||||
{
|
||||
"id": "page-size",
|
||||
"name": "page_size",
|
||||
"type": "number",
|
||||
"value": 1000
|
||||
},
|
||||
{
|
||||
"id": "page-count",
|
||||
"name": "page_count",
|
||||
"type": "number",
|
||||
"value": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "config-glpi-slack",
|
||||
"name": "Config - GLPI e Slack",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
420,
|
||||
320
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "={{ $json.glpi_url.replace(/\\/$/, '') + '/apirest.php/initSession' }}",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "App-Token",
|
||||
"value": "={{ $json.glpi_app_token }}"
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"value": "={{ 'user_token ' + $json.glpi_user_token }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "http-init-session",
|
||||
"name": "HTTP - GLPI initSession",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
680,
|
||||
320
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const config = $('Config - GLPI e Slack').first().json;\n\nconst pageSize = Number(config.page_size || 1000);\nconst pageCount = Number(config.page_count || 2);\nconst sorts = ['id', 'date_mod'];\nconst items = [];\n\nfor (const sort of sorts) {\n for (let page = 0; page < pageCount; page++) {\n const start = page * pageSize;\n const end = start + pageSize - 1;\n\n items.push({\n json: {\n ...config,\n sort_field: sort,\n range_tickets: `${start}-${end}`\n }\n });\n }\n}\n\nreturn items;"
|
||||
},
|
||||
"id": "code-gerar-paginas-glpi",
|
||||
"name": "Code - Gerar paginas GLPI",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
820,
|
||||
320
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "={{ $('Config - GLPI e Slack').first().json.glpi_url.replace(/\\/$/, '') + '/apirest.php/Ticket/' }}",
|
||||
"sendQuery": true,
|
||||
"queryParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "range",
|
||||
"value": "={{ $json.range_tickets }}"
|
||||
},
|
||||
{
|
||||
"name": "sort",
|
||||
"value": "={{ $json.sort_field }}"
|
||||
},
|
||||
{
|
||||
"name": "order",
|
||||
"value": "DESC"
|
||||
},
|
||||
{
|
||||
"name": "expand_dropdowns",
|
||||
"value": "true"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "Session-Token",
|
||||
"value": "={{ $('HTTP - GLPI initSession').first().json.session_token }}"
|
||||
},
|
||||
{
|
||||
"name": "App-Token",
|
||||
"value": "={{ $('Config - GLPI e Slack').first().json.glpi_app_token }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "http-buscar-tickets",
|
||||
"name": "HTTP - GLPI buscar tickets",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
1060,
|
||||
320
|
||||
],
|
||||
"continueOnFail": true
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const inputItems = $input.all();\n\nconst statusMap = {\n 1: 'Novo',\n 2: 'Processando',\n 3: 'Planejado',\n 4: 'Pendente',\n 5: 'Solucionado',\n 6: 'Fechado'\n};\n\nconst prioridadeMap = {\n 1: 'Muito baixa',\n 2: 'Baixa',\n 3: 'Média',\n 4: 'Alta',\n 5: 'Muito alta',\n 6: 'Crítica'\n};\n\nfunction coletarTickets(items) {\n const porId = new Map();\n let totalRecebido = 0;\n\n for (const item of items) {\n const data = item.json;\n let lista = [];\n\n if (Array.isArray(data)) {\n lista = data;\n } else if (Array.isArray(data.data)) {\n lista = data.data;\n } else if (data.id) {\n lista = [data];\n }\n\n totalRecebido += lista.length;\n\n for (const ticket of lista) {\n if (ticket.id === undefined || ticket.id === null) continue;\n porId.set(String(ticket.id), ticket);\n }\n }\n\n return {\n tickets: [...porId.values()],\n totalRecebido,\n totalUnico: porId.size\n };\n}\n\nfunction limparTexto(valor) {\n return String(valor)\n .replace(/>/g, '>')\n .replace(/>/g, '>')\n .replace(/</g, '<')\n .replace(/</g, '<')\n .replace(/&/g, '&')\n .trim();\n}\n\nfunction textoValor(valor, mapa) {\n if (valor === null || valor === undefined || valor === '') return 'Não informado';\n return limparTexto(mapa[valor] || mapa[String(valor)] || valor);\n}\n\nfunction extrairNome(valor) {\n if (valor === null || valor === undefined || valor === '') return 'Não informado';\n if (typeof valor === 'object') return limparTexto(valor.name || valor.completename || valor.id || 'Não informado');\n return limparTexto(valor);\n}\n\nfunction normalizarDataAlvo(valor) {\n const texto = String(valor || '').trim();\n\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(texto)) return texto;\n\n const matchPtBr = texto.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{2}|\\d{4})$/);\n if (matchPtBr) {\n const [, dia, mes, ano] = matchPtBr;\n const anoCompleto = ano.length === 2 ? `20${ano}` : ano;\n return `${anoCompleto}-${mes}-${dia}`;\n }\n\n return new Date().toISOString().slice(0, 10);\n}\n\nconst coleta = coletarTickets(inputItems);\nconst config = $('Config - GLPI e Slack').first().json;\nconst dataAlvo = normalizarDataAlvo(config.data_alvo);\n\nconst chamadosDoPeriodo = coleta.tickets.filter(ticket => {\n const dataAbertura = ticket.date_creation || ticket.date || ticket.data_abertura || '';\n const dataSolucao = ticket.solvedate || ticket.closedate || ticket.data_fechamento || '';\n\n return String(dataAbertura).startsWith(dataAlvo) || String(dataSolucao).startsWith(dataAlvo);\n});\n\nconst chamados = chamadosDoPeriodo.map(ticket => {\n const dataAbertura = ticket.date_creation || ticket.date || ticket.data_abertura || '';\n const dataFechamento = ticket.solvedate || ticket.closedate || ticket.data_fechamento || '';\n\n return {\n id: ticket.id,\n titulo: extrairNome(ticket.name || ticket.titulo || 'Sem título'),\n status: textoValor(ticket.status, statusMap),\n prioridade: textoValor(ticket.priority, prioridadeMap),\n categoria: extrairNome(ticket.itilcategories_id || ticket.categoria),\n cliente: extrairNome(ticket.entities_id || ticket.cliente),\n data_abertura: dataAbertura || null,\n data_fechamento: dataFechamento || null,\n aberto_no_periodo: String(dataAbertura).startsWith(dataAlvo),\n fechado_no_periodo: String(dataFechamento).startsWith(dataAlvo)\n };\n});\n\nreturn [\n {\n json: {\n data_filtro: dataAlvo,\n paginas_consultadas: inputItems.length,\n total_recebido_glpi: coleta.totalRecebido,\n total_retornado_glpi: coleta.totalUnico,\n chamados\n }\n }\n];"
|
||||
},
|
||||
"id": "code-filtrar-hoje",
|
||||
"name": "Code - Filtrar chamados de hoje",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1300,
|
||||
320
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const chamados = $input.first().json.chamados || [];\n\nfunction contarPorCampo(lista, campo) {\n return lista.reduce((acc, item) => {\n const valor = item[campo] || 'Não informado';\n acc[valor] = (acc[valor] || 0) + 1;\n return acc;\n }, {});\n}\n\nfunction ordenarContagem(objeto) {\n return Object.entries(objeto)\n .sort((a, b) => b[1] - a[1])\n .reduce((acc, [chave, valor]) => {\n acc[chave] = valor;\n return acc;\n }, {});\n}\n\nconst statusFechados = ['Fechado', 'Solucionado', 'Encerrado'];\nconst prioridadesAltas = ['Alta', 'Muito alta', 'Crítica'];\n\nconst chamadosAbertosNoPeriodo = chamados.filter(c => c.aberto_no_periodo);\nconst chamadosFechadosNoPeriodo = chamados.filter(c => c.fechado_no_periodo);\nconst chamadosPendentes = chamados.filter(c => !statusFechados.includes(c.status));\n\nconst altaPrioridadePendentes = chamadosPendentes.filter(c =>\n prioridadesAltas.includes(c.prioridade)\n).length;\n\nconst porCategoria = ordenarContagem(contarPorCampo(chamados, 'categoria'));\nconst porCliente = ordenarContagem(contarPorCampo(chamados, 'cliente'));\nconst porPrioridade = ordenarContagem(contarPorCampo(chamados, 'prioridade'));\nconst porStatus = ordenarContagem(contarPorCampo(chamados, 'status'));\n\nreturn [\n {\n json: {\n data_relatorio: new Date().toISOString(),\n data_filtro: $input.first().json.data_filtro,\n paginas_consultadas: $input.first().json.paginas_consultadas,\n total_recebido_glpi: $input.first().json.total_recebido_glpi,\n total_retornado_glpi: $input.first().json.total_retornado_glpi,\n total_chamados: chamados.length,\n chamados_abertos_hoje: chamadosAbertosNoPeriodo.length,\n chamados_fechados_hoje: chamadosFechadosNoPeriodo.length,\n chamados_abertos_ou_pendentes: chamadosPendentes.length,\n chamados_fechados: chamadosFechadosNoPeriodo.length,\n chamados_pendentes: chamadosPendentes.length,\n chamados_alta_prioridade_pendentes: altaPrioridadePendentes,\n chamados_por_categoria: porCategoria,\n chamados_por_cliente: porCliente,\n chamados_por_prioridade: porPrioridade,\n chamados_por_status: porStatus,\n chamados_detalhados: chamados\n }\n }\n];"
|
||||
},
|
||||
"id": "code-calcular-real",
|
||||
"name": "Code - Calcular indicadores",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1540,
|
||||
320
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://api.openai.com/v1/chat/completions",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"value": "={{ 'Bearer ' + $('Config - GLPI e Slack').first().json.openai_api_key }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"contentType": "json",
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ { model: $('Config - GLPI e Slack').first().json.openai_model || 'gpt-5.5', temperature: 0.2, max_tokens: 700, messages: [ { role: 'system', content: 'Você é um analista de infraestrutura gerando um resumo diário de suporte para a equipe técnica. Use apenas os dados fornecidos. Não invente números, clientes, categorias ou chamados. Gere um resumo em português do Brasil, objetivo, profissional e fácil de entender para envio no Slack.' }, { role: 'user', content: 'Estrutura obrigatória:\\n\\n1. Resumo geral do dia\\n2. Chamados abertos, fechados e pendentes\\n3. Categorias mais recorrentes\\n4. Clientes com maior volume\\n5. Chamados de maior atenção\\n6. Recomendações para o próximo dia\\n\\nDados do dia:\\n\\n' + JSON.stringify($json) + '\\n\\nO resultado deve estar pronto para enviar em um canal do Slack.' } ] } }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "openai-gerar-resumo-real",
|
||||
"name": "OpenAI - Gerar resumo",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
1780,
|
||||
320
|
||||
],
|
||||
"continueOnFail": true,
|
||||
"retryOnFail": true,
|
||||
"maxTries": 3,
|
||||
"waitBetweenTries": 30000
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const data = $input.first().json;\n\nconst textoIa =\n data.choices?.[0]?.message?.content ||\n data.message?.content ||\n data.text ||\n data.output ||\n data.content;\n\nif (textoIa && !String(textoIa).includes('Welcome to the OpenAI API')) {\n return [\n {\n json: {\n text: textoIa\n }\n }\n ];\n}\n\nconst indicadores = $('Code - Calcular indicadores').first().json;\n\nfunction formatarRanking(titulo, dados, sufixo = '') {\n const linhas = Object.entries(dados || {})\n .slice(0, 5)\n .map(([nome, total]) => `- ${nome}: ${total}${sufixo}`);\n\n return [`*${titulo}*`, ...(linhas.length ? linhas : ['- Sem registros'])].join('\\n');\n}\n\nconst erroOpenAi = data.error?.message || data.message || 'OpenAI indisponível ou com limite de uso no momento.';\n\nconst texto = [\n '*Resumo diário do suporte*',\n '',\n `Data do relatório: ${indicadores.data_filtro || 'Não informada'}`,\n '',\n '*Resumo geral*',\n `- Chamados abertos no dia: ${indicadores.chamados_abertos_hoje}`,\n `- Chamados fechados no dia: ${indicadores.chamados_fechados_hoje}`,\n `- Pendentes envolvidos no período: ${indicadores.chamados_pendentes}`,\n `- Alta prioridade ainda pendentes: ${indicadores.chamados_alta_prioridade_pendentes}`,\n '',\n formatarRanking('Categorias mais recorrentes', indicadores.chamados_por_categoria),\n '',\n formatarRanking('Clientes com maior volume', indicadores.chamados_por_cliente, ' chamados'),\n '',\n '*Diagnóstico da coleta*',\n `- Páginas consultadas: ${indicadores.paginas_consultadas}`,\n `- Registros recebidos do GLPI: ${indicadores.total_recebido_glpi}`,\n `- Tickets únicos analisados: ${indicadores.total_retornado_glpi}`,\n '',\n '*Pontos de atenção*',\n indicadores.chamados_alta_prioridade_pendentes > 0\n ? `- Existem ${indicadores.chamados_alta_prioridade_pendentes} chamados de alta prioridade ainda pendentes.`\n : '- Não há chamados de alta prioridade pendentes nos dados filtrados.',\n '',\n '*Recomendação para amanhã*',\n '- Priorizar chamados pendentes e de maior prioridade.',\n '- Verificar categorias e clientes com maior recorrência.',\n '',\n `_Resumo gerado sem IA porque a OpenAI retornou erro: ${erroOpenAi}_`\n].join('\\n');\n\nreturn [\n {\n json: {\n text: texto\n }\n }\n];"
|
||||
},
|
||||
"id": "code-preparar-slack",
|
||||
"name": "Code - Preparar mensagem Slack",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
2020,
|
||||
320
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $('Config - GLPI e Slack').first().json.slack_webhook_url }}",
|
||||
"sendBody": true,
|
||||
"contentType": "json",
|
||||
"specifyBody": "keypair",
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "text",
|
||||
"value": "={{ $json.text }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "http-slack",
|
||||
"name": "HTTP - Slack enviar mensagem",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
2260,
|
||||
320
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"url": "={{ $('Config - GLPI e Slack').first().json.glpi_url.replace(/\\/$/, '') + '/apirest.php/killSession' }}",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "Session-Token",
|
||||
"value": "={{ $('HTTP - GLPI initSession').first().json.session_token }}"
|
||||
},
|
||||
{
|
||||
"name": "App-Token",
|
||||
"value": "={{ $('Config - GLPI e Slack').first().json.glpi_app_token }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "http-kill-session",
|
||||
"name": "HTTP - GLPI killSession",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
2500,
|
||||
320
|
||||
]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Manual Trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Config - GLPI e Slack",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Config - GLPI e Slack": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "HTTP - GLPI initSession",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"HTTP - GLPI initSession": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code - Gerar paginas GLPI",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"HTTP - GLPI buscar tickets": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code - Filtrar chamados de hoje",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code - Filtrar chamados de hoje": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code - Calcular indicadores",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code - Calcular indicadores": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "OpenAI - Gerar resumo",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"OpenAI - Gerar resumo": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code - Preparar mensagem Slack",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code - Preparar mensagem Slack": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "HTTP - Slack enviar mensagem",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"HTTP - Slack enviar mensagem": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "HTTP - GLPI killSession",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code - Gerar paginas GLPI": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "HTTP - GLPI buscar tickets",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": false
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
{
|
||||
"name": "Resumo Diário GLPI - SQL Slack",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "manual-trigger-diario-sql",
|
||||
"name": "Manual Trigger",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [180, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{"id": "data-alvo", "name": "data_alvo", "type": "string", "value": ""},
|
||||
{"id": "slack-webhook-url", "name": "slack_webhook_url", "type": "string", "value": "https://hooks.slack.com/services/SEU/WEBHOOK/AQUI"},
|
||||
{"id": "openai-api-key", "name": "openai_api_key", "type": "string", "value": "COLE_SUA_OPENAI_API_KEY_AQUI"},
|
||||
{"id": "openai-model", "name": "openai_model", "type": "string", "value": "gpt-5.5"}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "config-diario-sql",
|
||||
"name": "Config - Diario SQL",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [420, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "function normalizarDataAlvo(valor) {\n const texto = String(valor || '').trim();\n\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(texto)) return texto;\n\n const matchPtBr = texto.match(/^(\\d{2})\\/(\\d{2})\\/(\\d{2}|\\d{4})$/);\n if (matchPtBr) {\n const [, dia, mes, ano] = matchPtBr;\n const anoCompleto = ano.length === 2 ? `20${ano}` : ano;\n return `${anoCompleto}-${mes}-${dia}`;\n }\n\n return new Date().toISOString().slice(0, 10);\n}\n\nconst config = $('Config - Diario SQL').first().json;\nconst dataAlvo = normalizarDataAlvo(config.data_alvo);\n\nconst filtroCategorias = `\n SELECT id FROM glpi_itilcategories\n WHERE JSON_CONTAINS(ancestors_cache, '{\"7\":7}') OR id = 7\n OR JSON_CONTAINS(ancestors_cache, '{\"16\":16}') OR id = 16\n OR JSON_CONTAINS(ancestors_cache, '{\"17\":17}') OR id = 17\n OR JSON_CONTAINS(ancestors_cache, '{\"18\":18}') OR id = 18\n`;\n\nconst query = `\nSELECT\n '${dataAlvo}' AS data_alvo,\n COALESCE(abertos.abertos, 0) AS abertos,\n COALESCE(fechados.fechados, 0) AS fechados,\n COALESCE(fechados.fechados, 0) - COALESCE(abertos.abertos, 0) AS saldo,\n COALESCE(pendentes.pendentes, 0) AS pendentes,\n COALESCE(alta_prioridade.alta_prioridade_pendentes, 0) AS alta_prioridade_pendentes,\n COALESCE(categorias.categorias, 'Sem registros') AS categorias_recorrentes,\n COALESCE(clientes.clientes, 'Sem registros') AS clientes_volume\nFROM\n (SELECT COUNT(*) AS abertos FROM glpi_tickets WHERE is_deleted = false AND DATE(date) = '${dataAlvo}' AND itilcategories_id IN (${filtroCategorias})) abertos\nCROSS JOIN\n (SELECT COUNT(*) AS fechados FROM glpi_tickets WHERE closedate IS NOT NULL AND is_deleted = false AND DATE(closedate) = '${dataAlvo}' AND itilcategories_id IN (${filtroCategorias})) fechados\nCROSS JOIN\n (SELECT COUNT(*) AS pendentes FROM glpi_tickets WHERE is_deleted = false AND status NOT IN (5, 6) AND itilcategories_id IN (${filtroCategorias})) pendentes\nCROSS JOIN\n (SELECT COUNT(*) AS alta_prioridade_pendentes FROM glpi_tickets WHERE is_deleted = false AND status NOT IN (5, 6) AND priority >= 4 AND itilcategories_id IN (${filtroCategorias})) alta_prioridade\nCROSS JOIN\n (SELECT COALESCE(GROUP_CONCAT(CONCAT(nome, ': ', total) ORDER BY total DESC SEPARATOR '\\n'), 'Sem registros') AS categorias FROM (SELECT COALESCE(c.completename, c.name, 'Nao informado') AS nome, COUNT(*) AS total FROM glpi_tickets t LEFT JOIN glpi_itilcategories c ON c.id = t.itilcategories_id WHERE t.is_deleted = false AND (DATE(t.date) = '${dataAlvo}' OR DATE(t.closedate) = '${dataAlvo}') AND t.itilcategories_id IN (${filtroCategorias}) GROUP BY t.itilcategories_id, c.completename, c.name ORDER BY total DESC LIMIT 5) ranking_categorias) categorias\nCROSS JOIN\n (SELECT COALESCE(GROUP_CONCAT(CONCAT(nome, ': ', total, ' chamados') ORDER BY total DESC SEPARATOR '\\n'), 'Sem registros') AS clientes FROM (SELECT COALESCE(e.completename, e.name, 'Nao informado') AS nome, COUNT(*) AS total FROM glpi_tickets t LEFT JOIN glpi_entities e ON e.id = t.entities_id WHERE t.is_deleted = false AND (DATE(t.date) = '${dataAlvo}' OR DATE(t.closedate) = '${dataAlvo}') AND t.itilcategories_id IN (${filtroCategorias}) GROUP BY t.entities_id, e.completename, e.name ORDER BY total DESC LIMIT 5) ranking_clientes) clientes;`;\n\nreturn [{ json: { ...config, data_alvo_normalizada: dataAlvo, query } }];"
|
||||
},
|
||||
"id": "code-montar-sql-diario",
|
||||
"name": "Code - Montar SQL diario",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [680, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "={{ $json.query }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "mysql-diario-glpi",
|
||||
"name": "MySQL - Indicadores diario GLPI",
|
||||
"type": "n8n-nodes-base.mySql",
|
||||
"typeVersion": 2.4,
|
||||
"position": [940, 300],
|
||||
"credentials": {"mySql": {"id": "", "name": "MySQL GLPI"}}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const row = $input.first().json;\n\nconst textoBase = [\n '*Resumo diário do suporte GLPI*',\n '',\n `Data do relatório: ${row.data_alvo}`,\n '',\n '*Resumo geral*',\n `- Chamados abertos no dia: ${row.abertos}`,\n `- Chamados fechados no dia: ${row.fechados}`,\n `- Pendentes atuais: ${row.pendentes}`,\n `- Alta prioridade ainda pendentes: ${row.alta_prioridade_pendentes}`,\n `- Saldo fechados - abertos: ${row.saldo}`,\n '',\n '*Categorias mais recorrentes*',\n String(row.categorias_recorrentes || 'Sem registros').split('\\n').map(l => `- ${l}`).join('\\n'),\n '',\n '*Clientes com maior volume*',\n String(row.clientes_volume || 'Sem registros').split('\\n').map(l => `- ${l}`).join('\\n'),\n '',\n '*Observação*',\n '- Chamados fechados no dia podem ter sido abertos em dias ou semanas anteriores.'\n].join('\\n');\n\nreturn [{ json: { ...row, texto_base: textoBase } }];"
|
||||
},
|
||||
"id": "code-preparar-diario",
|
||||
"name": "Code - Preparar indicadores",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1200, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://api.openai.com/v1/chat/completions",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {"parameters": [{"name": "Content-Type", "value": "application/json"}, {"name": "Authorization", "value": "={{ 'Bearer ' + $('Config - Diario SQL').first().json.openai_api_key }}"}]},
|
||||
"sendBody": true,
|
||||
"contentType": "json",
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ { model: $('Config - Diario SQL').first().json.openai_model || 'gpt-5.5', messages: [ { role: 'system', content: 'Você é um analista de suporte. Gere um resumo diário curto em português do Brasil para Slack. Use somente os números fornecidos.' }, { role: 'user', content: 'Dados diários GLPI:\\n' + JSON.stringify($json) + '\\n\\nExplique que fechados podem ter sido abertos em datas anteriores. Seja objetivo.' } ] } }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "openai-diario-sql",
|
||||
"name": "OpenAI - Gerar resumo",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1460, 300],
|
||||
"continueOnFail": true,
|
||||
"retryOnFail": true,
|
||||
"maxTries": 3,
|
||||
"waitBetweenTries": 30000
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const data = $input.first().json;\nconst textoIa = data.choices?.[0]?.message?.content;\nconst fallback = $('Code - Preparar indicadores').first().json.texto_base;\n\nreturn [{ json: { text: textoIa || fallback } }];"
|
||||
},
|
||||
"id": "code-slack-diario",
|
||||
"name": "Code - Preparar mensagem Slack",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [1720, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $('Config - Diario SQL').first().json.slack_webhook_url }}",
|
||||
"sendBody": true,
|
||||
"contentType": "json",
|
||||
"specifyBody": "keypair",
|
||||
"bodyParameters": {"parameters": [{"name": "text", "value": "={{ $json.text }}"}]},
|
||||
"options": {}
|
||||
},
|
||||
"id": "http-slack-diario-sql",
|
||||
"name": "HTTP - Slack enviar mensagem",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [1980, 300]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Manual Trigger": {"main": [[{"node": "Config - Diario SQL", "type": "main", "index": 0}]]},
|
||||
"Config - Diario SQL": {"main": [[{"node": "Code - Montar SQL diario", "type": "main", "index": 0}]]},
|
||||
"Code - Montar SQL diario": {"main": [[{"node": "MySQL - Indicadores diario GLPI", "type": "main", "index": 0}]]},
|
||||
"MySQL - Indicadores diario GLPI": {"main": [[{"node": "Code - Preparar indicadores", "type": "main", "index": 0}]]},
|
||||
"Code - Preparar indicadores": {"main": [[{"node": "OpenAI - Gerar resumo", "type": "main", "index": 0}]]},
|
||||
"OpenAI - Gerar resumo": {"main": [[{"node": "Code - Preparar mensagem Slack", "type": "main", "index": 0}]]},
|
||||
"Code - Preparar mensagem Slack": {"main": [[{"node": "HTTP - Slack enviar mensagem", "type": "main", "index": 0}]]}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {"executionOrder": "v1"},
|
||||
"versionId": "",
|
||||
"meta": {"templateCredsSetupCompleted": false},
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"name": "Resumo Semanal GLPI - SQL Slack",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "manual-trigger-sql",
|
||||
"name": "Manual Trigger",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
180,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "slack-webhook-url",
|
||||
"name": "slack_webhook_url",
|
||||
"type": "string",
|
||||
"value": "https://hooks.slack.com/services/SEU/WEBHOOK/AQUI"
|
||||
},
|
||||
{
|
||||
"id": "openai-api-key",
|
||||
"name": "openai_api_key",
|
||||
"type": "string",
|
||||
"value": "COLE_SUA_OPENAI_API_KEY_AQUI"
|
||||
},
|
||||
{
|
||||
"id": "openai-model",
|
||||
"name": "openai_model",
|
||||
"type": "string",
|
||||
"value": "gpt-5.5"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "config-sql-slack",
|
||||
"name": "Config - Slack e OpenAI",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [
|
||||
420,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "SELECT\n CONCAT(SUBSTR(DATE_FORMAT(NOW(), '%x%v'), 1, 4), '/', SUBSTR(DATE_FORMAT(NOW(), '%x%v'), 5, 2)) AS anomes,\n COALESCE(fechados_por_semana.fechados, 0) - COALESCE(abertos_por_semana.abertos, 0) AS saldo,\n COALESCE(abertos_por_semana.abertos, 0) AS abertos,\n COALESCE(fechados_por_semana.fechados, 0) AS fechados\nFROM (\n SELECT COUNT(*) AS abertos\n FROM glpi_tickets\n WHERE is_deleted = false\n AND DATE_FORMAT(date, '%x%v') = DATE_FORMAT(NOW(), '%x%v')\n AND itilcategories_id IN (\n SELECT id\n FROM glpi_itilcategories\n WHERE JSON_CONTAINS(ancestors_cache, '{\"7\":7}', '$') OR id = 7\n OR JSON_CONTAINS(ancestors_cache, '{\"16\":16}', '$') OR id = 16\n OR JSON_CONTAINS(ancestors_cache, '{\"17\":17}', '$') OR id = 17\n OR JSON_CONTAINS(ancestors_cache, '{\"18\":18}', '$') OR id = 18\n )\n) AS abertos_por_semana\nCROSS JOIN (\n SELECT COUNT(*) AS fechados\n FROM glpi_tickets\n WHERE closedate IS NOT NULL\n AND is_deleted = false\n AND DATE_FORMAT(closedate, '%x%v') = DATE_FORMAT(NOW(), '%x%v')\n AND itilcategories_id IN (\n SELECT id\n FROM glpi_itilcategories\n WHERE JSON_CONTAINS(ancestors_cache, '{\"7\":7}', '$') OR id = 7\n OR JSON_CONTAINS(ancestors_cache, '{\"16\":16}', '$') OR id = 16\n OR JSON_CONTAINS(ancestors_cache, '{\"17\":17}', '$') OR id = 17\n OR JSON_CONTAINS(ancestors_cache, '{\"18\":18}', '$') OR id = 18\n )\n) AS fechados_por_semana;",
|
||||
"options": {}
|
||||
},
|
||||
"id": "mysql-indicadores-semana",
|
||||
"name": "MySQL - Indicadores semana GLPI",
|
||||
"type": "n8n-nodes-base.mySql",
|
||||
"typeVersion": 2.4,
|
||||
"position": [
|
||||
680,
|
||||
300
|
||||
],
|
||||
"credentials": {
|
||||
"mySql": {
|
||||
"id": "",
|
||||
"name": "MySQL GLPI"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const row = $input.first().json;\n\nconst textoBase = [\n '*Resumo semanal do suporte GLPI*',\n '',\n `Semana: ${row.anomes}`,\n '',\n '*Indicadores*',\n `- Chamados abertos na semana: ${row.abertos}`,\n `- Chamados fechados na semana: ${row.fechados}`,\n `- Saldo fechados - abertos: ${row.saldo}`,\n '',\n '*Observação*',\n '- Chamados fechados podem ter sido abertos em semanas anteriores.',\n '- Os números seguem o filtro de categorias da query SQL: árvores 7, 16, 17 e 18.'\n].join('\\n');\n\nreturn [{ json: { ...row, texto_base: textoBase } }];"
|
||||
},
|
||||
"id": "code-preparar-indicadores",
|
||||
"name": "Code - Preparar indicadores",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
940,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "https://api.openai.com/v1/chat/completions",
|
||||
"sendHeaders": true,
|
||||
"headerParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Content-Type",
|
||||
"value": "application/json"
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"value": "={{ 'Bearer ' + $('Config - Slack e OpenAI').first().json.openai_api_key }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"sendBody": true,
|
||||
"contentType": "json",
|
||||
"specifyBody": "json",
|
||||
"jsonBody": "={{ { model: $('Config - Slack e OpenAI').first().json.openai_model || 'gpt-5.5', temperature: 0.2, max_tokens: 500, messages: [ { role: 'system', content: 'Você é um analista de suporte. Gere um resumo executivo curto em português do Brasil para Slack. Use somente os números fornecidos.' }, { role: 'user', content: 'Dados da semana GLPI:\\n' + JSON.stringify($json) + '\\n\\nExplique que fechados podem ter sido abertos em semanas anteriores. Seja objetivo.' } ] } }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "openai-resumo-sql",
|
||||
"name": "OpenAI - Gerar resumo",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
1200,
|
||||
300
|
||||
],
|
||||
"continueOnFail": true,
|
||||
"retryOnFail": true,
|
||||
"maxTries": 3,
|
||||
"waitBetweenTries": 30000
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "const data = $input.first().json;\nconst textoIa = data.choices?.[0]?.message?.content;\nconst fallback = $('Code - Preparar indicadores').first().json.texto_base;\n\nreturn [{ json: { text: textoIa || fallback } }];"
|
||||
},
|
||||
"id": "code-preparar-slack-sql",
|
||||
"name": "Code - Preparar mensagem Slack",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
1460,
|
||||
300
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"method": "POST",
|
||||
"url": "={{ $('Config - Slack e OpenAI').first().json.slack_webhook_url }}",
|
||||
"sendBody": true,
|
||||
"contentType": "json",
|
||||
"specifyBody": "keypair",
|
||||
"bodyParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "text",
|
||||
"value": "={{ $json.text }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "http-slack-sql",
|
||||
"name": "HTTP - Slack enviar mensagem",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"position": [
|
||||
1720,
|
||||
300
|
||||
]
|
||||
}
|
||||
],
|
||||
"pinData": {},
|
||||
"connections": {
|
||||
"Manual Trigger": {
|
||||
"main": [[{"node": "Config - Slack e OpenAI", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Config - Slack e OpenAI": {
|
||||
"main": [[{"node": "MySQL - Indicadores semana GLPI", "type": "main", "index": 0}]]
|
||||
},
|
||||
"MySQL - Indicadores semana GLPI": {
|
||||
"main": [[{"node": "Code - Preparar indicadores", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Code - Preparar indicadores": {
|
||||
"main": [[{"node": "OpenAI - Gerar resumo", "type": "main", "index": 0}]]
|
||||
},
|
||||
"OpenAI - Gerar resumo": {
|
||||
"main": [[{"node": "Code - Preparar mensagem Slack", "type": "main", "index": 0}]]
|
||||
},
|
||||
"Code - Preparar mensagem Slack": {
|
||||
"main": [[{"node": "HTTP - Slack enviar mensagem", "type": "main", "index": 0}]]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "",
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": false
|
||||
},
|
||||
"tags": []
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
services:
|
||||
n8n:
|
||||
image: docker.n8n.io/n8nio/n8n:latest
|
||||
container_name: n8n-glpi-mvp
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${N8N_PORT:-5678}:5678"
|
||||
environment:
|
||||
- GENERIC_TIMEZONE=${GENERIC_TIMEZONE:-America/Sao_Paulo}
|
||||
- TZ=${TZ:-America/Sao_Paulo}
|
||||
- N8N_HOST=${N8N_HOST:-localhost}
|
||||
- N8N_PORT=5678
|
||||
- N8N_PROTOCOL=${N8N_PROTOCOL:-http}
|
||||
- N8N_BASIC_AUTH_ACTIVE=${N8N_BASIC_AUTH_ACTIVE:-true}
|
||||
- N8N_BASIC_AUTH_USER=${N8N_BASIC_AUTH_USER:-admin}
|
||||
- N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_AUTH_PASSWORD:-troque-esta-senha}
|
||||
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY:-troque-esta-chave-com-32-caracteres}
|
||||
volumes:
|
||||
- n8n_data:/home/node/.n8n
|
||||
|
||||
volumes:
|
||||
n8n_data:
|
||||
@@ -0,0 +1,116 @@
|
||||
-- Query diaria base. No workflow N8N, a data alvo e substituida dinamicamente.
|
||||
-- Regra:
|
||||
-- - abertos: glpi_tickets.date na data alvo
|
||||
-- - fechados: glpi_tickets.closedate na data alvo
|
||||
-- - fechados podem ter sido abertos em dias/semanas anteriores
|
||||
-- - categorias filtradas pelas arvores 7, 16, 17 e 18
|
||||
|
||||
SELECT
|
||||
'{DATA_ALVO}' AS data_alvo,
|
||||
COALESCE(abertos.abertos, 0) AS abertos,
|
||||
COALESCE(fechados.fechados, 0) AS fechados,
|
||||
COALESCE(fechados.fechados, 0) - COALESCE(abertos.abertos, 0) AS saldo,
|
||||
COALESCE(pendentes.pendentes, 0) AS pendentes,
|
||||
COALESCE(alta_prioridade.alta_prioridade_pendentes, 0) AS alta_prioridade_pendentes,
|
||||
COALESCE(categorias.categorias, 'Sem registros') AS categorias_recorrentes,
|
||||
COALESCE(clientes.clientes, 'Sem registros') AS clientes_volume
|
||||
FROM
|
||||
(
|
||||
SELECT COUNT(*) AS abertos
|
||||
FROM glpi_tickets
|
||||
WHERE is_deleted = false
|
||||
AND DATE(date) = '{DATA_ALVO}'
|
||||
AND itilcategories_id IN (
|
||||
SELECT id FROM glpi_itilcategories
|
||||
WHERE JSON_CONTAINS(ancestors_cache, '{"7":7}') OR id = 7
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"16":16}') OR id = 16
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"17":17}') OR id = 17
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"18":18}') OR id = 18
|
||||
)
|
||||
) abertos
|
||||
CROSS JOIN
|
||||
(
|
||||
SELECT COUNT(*) AS fechados
|
||||
FROM glpi_tickets
|
||||
WHERE closedate IS NOT NULL
|
||||
AND is_deleted = false
|
||||
AND DATE(closedate) = '{DATA_ALVO}'
|
||||
AND itilcategories_id IN (
|
||||
SELECT id FROM glpi_itilcategories
|
||||
WHERE JSON_CONTAINS(ancestors_cache, '{"7":7}') OR id = 7
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"16":16}') OR id = 16
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"17":17}') OR id = 17
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"18":18}') OR id = 18
|
||||
)
|
||||
) fechados
|
||||
CROSS JOIN
|
||||
(
|
||||
SELECT COUNT(*) AS pendentes
|
||||
FROM glpi_tickets
|
||||
WHERE is_deleted = false
|
||||
AND status NOT IN (5, 6)
|
||||
AND itilcategories_id IN (
|
||||
SELECT id FROM glpi_itilcategories
|
||||
WHERE JSON_CONTAINS(ancestors_cache, '{"7":7}') OR id = 7
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"16":16}') OR id = 16
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"17":17}') OR id = 17
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"18":18}') OR id = 18
|
||||
)
|
||||
) pendentes
|
||||
CROSS JOIN
|
||||
(
|
||||
SELECT COUNT(*) AS alta_prioridade_pendentes
|
||||
FROM glpi_tickets
|
||||
WHERE is_deleted = false
|
||||
AND status NOT IN (5, 6)
|
||||
AND priority >= 4
|
||||
AND itilcategories_id IN (
|
||||
SELECT id FROM glpi_itilcategories
|
||||
WHERE JSON_CONTAINS(ancestors_cache, '{"7":7}') OR id = 7
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"16":16}') OR id = 16
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"17":17}') OR id = 17
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"18":18}') OR id = 18
|
||||
)
|
||||
) alta_prioridade
|
||||
CROSS JOIN
|
||||
(
|
||||
SELECT COALESCE(GROUP_CONCAT(CONCAT(nome, ': ', total) ORDER BY total DESC SEPARATOR '\n'), 'Sem registros') AS categorias
|
||||
FROM (
|
||||
SELECT COALESCE(c.completename, c.name, 'Nao informado') AS nome, COUNT(*) AS total
|
||||
FROM glpi_tickets t
|
||||
LEFT JOIN glpi_itilcategories c ON c.id = t.itilcategories_id
|
||||
WHERE t.is_deleted = false
|
||||
AND (DATE(t.date) = '{DATA_ALVO}' OR DATE(t.closedate) = '{DATA_ALVO}')
|
||||
AND t.itilcategories_id IN (
|
||||
SELECT id FROM glpi_itilcategories
|
||||
WHERE JSON_CONTAINS(ancestors_cache, '{"7":7}') OR id = 7
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"16":16}') OR id = 16
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"17":17}') OR id = 17
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"18":18}') OR id = 18
|
||||
)
|
||||
GROUP BY t.itilcategories_id, c.completename, c.name
|
||||
ORDER BY total DESC
|
||||
LIMIT 5
|
||||
) ranking_categorias
|
||||
) categorias
|
||||
CROSS JOIN
|
||||
(
|
||||
SELECT COALESCE(GROUP_CONCAT(CONCAT(nome, ': ', total, ' chamados') ORDER BY total DESC SEPARATOR '\n'), 'Sem registros') AS clientes
|
||||
FROM (
|
||||
SELECT COALESCE(e.completename, e.name, 'Nao informado') AS nome, COUNT(*) AS total
|
||||
FROM glpi_tickets t
|
||||
LEFT JOIN glpi_entities e ON e.id = t.entities_id
|
||||
WHERE t.is_deleted = false
|
||||
AND (DATE(t.date) = '{DATA_ALVO}' OR DATE(t.closedate) = '{DATA_ALVO}')
|
||||
AND t.itilcategories_id IN (
|
||||
SELECT id FROM glpi_itilcategories
|
||||
WHERE JSON_CONTAINS(ancestors_cache, '{"7":7}') OR id = 7
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"16":16}') OR id = 16
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"17":17}') OR id = 17
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"18":18}') OR id = 18
|
||||
)
|
||||
GROUP BY t.entities_id, e.completename, e.name
|
||||
ORDER BY total DESC
|
||||
LIMIT 5
|
||||
) ranking_clientes
|
||||
) clientes;
|
||||
@@ -0,0 +1,34 @@
|
||||
SELECT
|
||||
CONCAT(SUBSTR(DATE_FORMAT(NOW(), '%x%v'), 1, 4), '/', SUBSTR(DATE_FORMAT(NOW(), '%x%v'), 5, 2)) AS anomes,
|
||||
COALESCE(fechados_por_semana.fechados, 0) - COALESCE(abertos_por_semana.abertos, 0) AS saldo,
|
||||
COALESCE(abertos_por_semana.abertos, 0) AS abertos,
|
||||
COALESCE(fechados_por_semana.fechados, 0) AS fechados
|
||||
FROM (
|
||||
SELECT COUNT(*) AS abertos
|
||||
FROM glpi_tickets
|
||||
WHERE is_deleted = false
|
||||
AND DATE_FORMAT(date, '%x%v') = DATE_FORMAT(NOW(), '%x%v')
|
||||
AND itilcategories_id IN (
|
||||
SELECT id
|
||||
FROM glpi_itilcategories
|
||||
WHERE JSON_CONTAINS(ancestors_cache, '{"7":7}', '$') OR id = 7
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"16":16}', '$') OR id = 16
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"17":17}', '$') OR id = 17
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"18":18}', '$') OR id = 18
|
||||
)
|
||||
) AS abertos_por_semana
|
||||
CROSS JOIN (
|
||||
SELECT COUNT(*) AS fechados
|
||||
FROM glpi_tickets
|
||||
WHERE closedate IS NOT NULL
|
||||
AND is_deleted = false
|
||||
AND DATE_FORMAT(closedate, '%x%v') = DATE_FORMAT(NOW(), '%x%v')
|
||||
AND itilcategories_id IN (
|
||||
SELECT id
|
||||
FROM glpi_itilcategories
|
||||
WHERE JSON_CONTAINS(ancestors_cache, '{"7":7}', '$') OR id = 7
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"16":16}', '$') OR id = 16
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"17":17}', '$') OR id = 17
|
||||
OR JSON_CONTAINS(ancestors_cache, '{"18":18}', '$') OR id = 18
|
||||
)
|
||||
) AS fechados_por_semana;
|
||||
@@ -0,0 +1,81 @@
|
||||
import json
|
||||
import sqlite3
|
||||
import sys
|
||||
|
||||
|
||||
def decode_flatted(table):
|
||||
memo = {}
|
||||
|
||||
def decode(value):
|
||||
if isinstance(value, str) and value.isdigit():
|
||||
return decode_index(int(value))
|
||||
if isinstance(value, list):
|
||||
return [decode(item) for item in value]
|
||||
if isinstance(value, dict):
|
||||
return {key: decode(item) for key, item in value.items()}
|
||||
return value
|
||||
|
||||
def decode_index(index):
|
||||
if index in memo:
|
||||
return memo[index]
|
||||
|
||||
raw = table[index]
|
||||
|
||||
if isinstance(raw, dict):
|
||||
obj = {}
|
||||
memo[index] = obj
|
||||
obj.update({key: decode(value) for key, value in raw.items()})
|
||||
return obj
|
||||
|
||||
if isinstance(raw, list):
|
||||
obj = []
|
||||
memo[index] = obj
|
||||
obj.extend(decode(value) for value in raw)
|
||||
return obj
|
||||
|
||||
memo[index] = raw
|
||||
return raw
|
||||
|
||||
return decode_index(0)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) not in (3, 4):
|
||||
raise SystemExit("usage: inspect_n8n_execution.py <database.sqlite> <execution_id> [node_name]")
|
||||
|
||||
db_path = sys.argv[1]
|
||||
execution_id = sys.argv[2]
|
||||
|
||||
con = sqlite3.connect(db_path)
|
||||
row = con.execute(
|
||||
"select data from execution_data where executionId = ?",
|
||||
(execution_id,),
|
||||
).fetchone()
|
||||
|
||||
if not row:
|
||||
raise SystemExit(f"execution {execution_id} not found")
|
||||
|
||||
decoded = decode_flatted(json.loads(row[0]))
|
||||
result_data = decoded.get("resultData", {})
|
||||
|
||||
if len(sys.argv) == 4:
|
||||
node_name = sys.argv[3]
|
||||
run_data = result_data.get("runData", {})
|
||||
node_runs = run_data.get(node_name)
|
||||
|
||||
if node_runs is None:
|
||||
print("available nodes:")
|
||||
for name in run_data:
|
||||
print("-", name)
|
||||
raise SystemExit(f"node {node_name!r} not found")
|
||||
|
||||
print(json.dumps(node_runs, ensure_ascii=False, indent=2)[:12000])
|
||||
return
|
||||
|
||||
print("lastNodeExecuted:", result_data.get("lastNodeExecuted"))
|
||||
print("error:")
|
||||
print(json.dumps(result_data.get("error"), ensure_ascii=False, indent=2))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user