2025-09-17 20:56:32 -03:00
|
|
|
// src/models/ticketGlpiModel.js
|
2025-09-10 20:52:12 -03:00
|
|
|
const glpiPool = require('../data/glpiDataBase');
|
|
|
|
|
const { logInfo, logError } = require('../utils/logger');
|
2025-09-17 20:56:32 -03:00
|
|
|
const LocationMappingModel = require('./locationMappingModel');
|
2025-09-10 20:52:12 -03:00
|
|
|
|
|
|
|
|
class TicketGlpiModel {
|
2025-09-11 20:57:43 -03:00
|
|
|
|
2025-09-15 20:55:19 -03:00
|
|
|
// Mapeamento de status do ServiceNow para o GLPI
|
2025-09-10 20:52:12 -03:00
|
|
|
static mapStatus(snState) {
|
|
|
|
|
const statusMap = {
|
2025-09-15 20:55:19 -03:00
|
|
|
'Aguardando Atendimento': 1,
|
|
|
|
|
'Em Atendimento': 2,
|
|
|
|
|
'Em Espera': 4,
|
2025-10-23 17:30:03 -03:00
|
|
|
'Resolvido': 5,
|
2025-10-24 13:13:08 -03:00
|
|
|
'Encerrado - Omitido': 6,
|
|
|
|
|
'Encerrado': 6 // Mapeia para Fechado no GLPI
|
2025-09-10 20:52:12 -03:00
|
|
|
};
|
|
|
|
|
return statusMap[snState] || 1;
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-23 17:30:03 -03:00
|
|
|
// Mapeamento de status do GLPI (numérico) para ServiceNow (string)
|
|
|
|
|
static mapGlpiStatusToString(glpiStatus) {
|
|
|
|
|
const statusMap = {
|
|
|
|
|
1: 'Aguardando Atendimento', // Novo
|
|
|
|
|
2: 'Em Atendimento', // Em atendimento (atribuído)
|
|
|
|
|
4: 'Em Espera', // Pendente
|
|
|
|
|
5: 'Resolvido', // Solucionado
|
|
|
|
|
6: 'Encerrado - Omitido' // Fechado
|
|
|
|
|
};
|
|
|
|
|
return statusMap[glpiStatus] || 'Desconhecido';
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 20:52:12 -03:00
|
|
|
// Mapeamento de prioridade
|
|
|
|
|
static mapPriority(snPriority) {
|
|
|
|
|
const priorityMap = {
|
2025-09-15 20:55:19 -03:00
|
|
|
'1': 5,
|
|
|
|
|
'2': 4,
|
|
|
|
|
'3': 3,
|
|
|
|
|
'4': 2,
|
|
|
|
|
'5': 1
|
2025-09-10 20:52:12 -03:00
|
|
|
};
|
|
|
|
|
return priorityMap[snPriority] || 3;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mapeamento de categoria
|
|
|
|
|
static mapCategory(shortDescription) {
|
|
|
|
|
const categoryMap = {
|
2025-09-15 20:55:19 -03:00
|
|
|
'Problemas com Telefonia Fixa': 5717,
|
|
|
|
|
'Problemas com Internet': 5707,
|
|
|
|
|
'Problemas com Wi-Fi/Rede Corporativa': 5707,
|
|
|
|
|
'Wi-fi Clientes': 5707
|
2025-09-10 20:52:12 -03:00
|
|
|
};
|
|
|
|
|
return categoryMap[shortDescription] || 5717;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-15 20:55:19 -03:00
|
|
|
// Mapeamento de SLA baseado no tipo e na descrição
|
|
|
|
|
static mapSlasIdTtr(ticketData) {
|
|
|
|
|
const isRequisicao = ticketData.type === 'requisicao';
|
2025-09-17 20:56:32 -03:00
|
|
|
const shortDesc = ticketData.short_description;
|
|
|
|
|
const isTelefonia = shortDesc === 'Problemas com Telefonia Fixa' || shortDesc === 'Solicitações relacionadas a telefonia fixa';
|
|
|
|
|
const isLinkDedicado = shortDesc === 'Problemas com Internet';
|
|
|
|
|
const isWifi = shortDesc === 'Problemas com Wi-Fi/Rede Corporativa' || shortDesc === 'Wi-fi Clientes';
|
2025-09-10 20:52:12 -03:00
|
|
|
|
2025-09-15 20:55:19 -03:00
|
|
|
if (isRequisicao && isTelefonia) {
|
|
|
|
|
return 37;
|
|
|
|
|
}
|
|
|
|
|
if (isRequisicao && (isLinkDedicado || isWifi)) {
|
|
|
|
|
return 37;
|
|
|
|
|
}
|
|
|
|
|
if (!isRequisicao && (isWifi || isLinkDedicado)) {
|
|
|
|
|
return 34;
|
|
|
|
|
}
|
|
|
|
|
if (!isRequisicao && isTelefonia) {
|
|
|
|
|
return 34;
|
|
|
|
|
}
|
|
|
|
|
return 0; // Default se nenhum critério for atendido
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-18 20:52:39 -03:00
|
|
|
// Formata o título do ticket
|
|
|
|
|
static async formatTitle(ticketData) {
|
|
|
|
|
try {
|
|
|
|
|
let entityData = null;
|
|
|
|
|
|
|
|
|
|
if (ticketData.location_id) {
|
|
|
|
|
entityData = await LocationMappingModel.getGlpiEntity(ticketData.location_id);
|
|
|
|
|
} else if (ticketData.location_name) {
|
|
|
|
|
entityData = await LocationMappingModel.getGlpiEntityByName(ticketData.location_name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const entityName = entityData?.name || 'Sem Entidade';
|
|
|
|
|
const tipo = ticketData.type === 'requisicao' ? 'REQUISIÇÃO' : 'INCIDENTE';
|
|
|
|
|
const tituloSn = ticketData.short_description || 'Sem título';
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
title: `${tipo} - ${entityName} - ${tituloSn}`,
|
|
|
|
|
entityId: entityData?.id || 127 // fallback para entity_id default
|
|
|
|
|
};
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logError(error, 'Erro ao formatar título do ticket');
|
|
|
|
|
const tipo = ticketData.type === 'requisicao' ? 'REQUISIÇÃO' : 'INCIDENTE';
|
|
|
|
|
return {
|
|
|
|
|
title: `${tipo} - ${ticketData.short_description || 'Sem título'}`,
|
|
|
|
|
entityId: 127
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2025-09-15 20:55:19 -03:00
|
|
|
static formatDescription(ticketData) {
|
|
|
|
|
// Função auxiliar para extrair informações usando regex
|
|
|
|
|
const extractInfo = (regex, description) => {
|
|
|
|
|
if (!description) return null;
|
|
|
|
|
const match = description.match(regex);
|
|
|
|
|
return match ? match[1] : null;
|
|
|
|
|
};
|
|
|
|
|
const extractEmailFromDescription = (description) => {
|
|
|
|
|
return extractInfo(/([\w-\.]+@([\w-]+\.)+[\w-]{2,4})/i, description);
|
|
|
|
|
};
|
|
|
|
|
const extractPhoneFromDescription = (description) => {
|
|
|
|
|
return extractInfo(/(\+?(\d{2})?\s*\(?\d{2}\)?[\s-]*\d{4,5}[\s-]?\d{4})/i, description);
|
|
|
|
|
};
|
|
|
|
|
const extractRamalFromDescription = (description) => {
|
|
|
|
|
return extractInfo(/(\d{3,4})/i, description);
|
2025-09-12 12:09:41 -03:00
|
|
|
};
|
|
|
|
|
|
2025-09-15 20:55:19 -03:00
|
|
|
// Obtém email, telefone e ramal, usando os valores do ticketData ou extraindo da descrição, se necessário.
|
|
|
|
|
const email = ticketData.caller_email || extractEmailFromDescription(ticketData.description) || 'N/A';
|
|
|
|
|
const phone = ticketData.caller_phone || extractPhoneFromDescription(ticketData.description) || 'N/A';
|
|
|
|
|
const ramal = ticketData.caller_ramal || extractRamalFromDescription(ticketData.description) || 'N/A';
|
|
|
|
|
|
|
|
|
|
// Formata a descrição em HTML
|
|
|
|
|
let htmlDescription = `
|
|
|
|
|
<table style="width:100%; border-collapse: collapse;">
|
|
|
|
|
<tr style="background-color:#f2f2f2;">
|
|
|
|
|
<th style="padding: 8px; border: 1px solid #ddd; text-align: left;">Campo</th>
|
|
|
|
|
<th style="padding: 8px; border: 1px solid #ddd; text-align: left;">Valor</th>
|
|
|
|
|
</tr>
|
2025-09-17 20:56:32 -03:00
|
|
|
<tr>
|
|
|
|
|
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Nome:</strong></td>
|
|
|
|
|
<td style="padding: 8px; border: 1px solid #ddd;">${ticketData.caller_id}</td>
|
|
|
|
|
</tr>
|
2025-09-15 20:55:19 -03:00
|
|
|
<tr>
|
|
|
|
|
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Email:</strong></td>
|
|
|
|
|
<td style="padding: 8px; border: 1px solid #ddd;">${email}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Telefone:</strong></td>
|
|
|
|
|
<td style="padding: 8px; border: 1px solid #ddd;">${phone}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Ramal:</strong></td>
|
|
|
|
|
<td style="padding: 8px; border: 1px solid #ddd;">${ramal}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
<tr>
|
|
|
|
|
<td style="padding: 8px; border: 1px solid #ddd;"><strong>Ticket Number (ServiceNow):</strong></td>
|
|
|
|
|
<td style="padding: 8px; border: 1px solid #ddd;">${ticketData.ticket_number || 'N/A'}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
</table>
|
|
|
|
|
<br>
|
|
|
|
|
<strong>Descrição:</strong>
|
|
|
|
|
<hr>
|
|
|
|
|
${ticketData.description || 'N/A'}
|
|
|
|
|
`;
|
|
|
|
|
return htmlDescription;
|
2025-09-10 20:52:12 -03:00
|
|
|
}
|
|
|
|
|
|
2025-09-15 20:55:19 -03:00
|
|
|
// Cria um ticket no GLPI
|
|
|
|
|
static async createTicket(ticketData) {
|
|
|
|
|
const connection = await glpiPool.getConnection();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
await connection.beginTransaction();
|
|
|
|
|
|
2025-09-17 20:56:32 -03:00
|
|
|
// Buscar entity_id com base no location_id ou location_name
|
2025-09-18 20:52:39 -03:00
|
|
|
let entityId = 127; // Valor padrão (fallback)
|
2025-09-17 20:56:32 -03:00
|
|
|
|
|
|
|
|
if (ticketData.location_id) {
|
|
|
|
|
entityId = await LocationMappingModel.getGlpiEntityId(ticketData.location_id) || entityId;
|
|
|
|
|
} else if (ticketData.location_name) {
|
|
|
|
|
entityId = await LocationMappingModel.getGlpiEntityIdByName(ticketData.location_name) || entityId;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-15 20:55:19 -03:00
|
|
|
// Extrai o email da descrição, se existir e não houver email no ticketData
|
|
|
|
|
let email = ticketData.caller_email;
|
|
|
|
|
if (!email && ticketData.description) {
|
|
|
|
|
const emailRegex = /[\w-\.]+@([\w-]+\.)+[\w-]{2,4}/g;
|
|
|
|
|
const matches = ticketData.description.match(emailRegex);
|
|
|
|
|
if (matches && matches.length > 0) {
|
|
|
|
|
email = matches[0];
|
|
|
|
|
}
|
2025-09-12 12:09:41 -03:00
|
|
|
}
|
2025-09-15 20:55:19 -03:00
|
|
|
|
2025-09-18 20:52:39 -03:00
|
|
|
// Formata o título do ticket
|
|
|
|
|
const { title: formattedTitle } = await TicketGlpiModel.formatTitle(ticketData);
|
2025-09-15 20:55:19 -03:00
|
|
|
// Formata a descrição para HTML
|
|
|
|
|
const formattedDescription = TicketGlpiModel.formatDescription(ticketData);
|
|
|
|
|
// Mapeia o tipo do ticket
|
|
|
|
|
const ticketType = ticketData.type === 'requisicao' ? 2 : 1;
|
|
|
|
|
// Determina o valor de slas_id_ttr
|
|
|
|
|
const slasIdTtr = TicketGlpiModel.mapSlasIdTtr(ticketData);
|
|
|
|
|
|
|
|
|
|
// Prepara os dados para o GLPI
|
|
|
|
|
const glpiTicketData = {
|
2025-09-17 20:56:32 -03:00
|
|
|
entities_id: entityId, // Usar o entity_id mapeado
|
2025-09-18 20:52:39 -03:00
|
|
|
name: formattedTitle,
|
2025-09-15 20:55:19 -03:00
|
|
|
date: ticketData.opened_at || new Date(),
|
|
|
|
|
date_mod: ticketData.updated_at || new Date(),
|
|
|
|
|
status: TicketGlpiModel.mapStatus(ticketData.state || '1'),
|
|
|
|
|
users_id_recipient: 917,
|
|
|
|
|
requesttypes_id: 1,
|
|
|
|
|
content: formattedDescription,
|
|
|
|
|
urgency: 3,
|
|
|
|
|
impact: 3,
|
|
|
|
|
priority: TicketGlpiModel.mapPriority(ticketData.priority || '3'),
|
|
|
|
|
type: ticketType,
|
|
|
|
|
itilcategories_id: TicketGlpiModel.mapCategory(ticketData.short_description),
|
|
|
|
|
global_validation: 1,
|
|
|
|
|
date_creation: ticketData.opened_at || new Date(),
|
|
|
|
|
slas_id_ttr: slasIdTtr
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Constrói a query dinamicamente e insere o ticket principal
|
|
|
|
|
const fields = Object.keys(glpiTicketData).join(', ');
|
|
|
|
|
const placeholders = Object.keys(glpiTicketData).map(() => '?').join(', ');
|
|
|
|
|
const values = Object.values(glpiTicketData);
|
|
|
|
|
|
|
|
|
|
const ticketQuery = `INSERT INTO glpi_tickets (${fields}) VALUES (${placeholders})`;
|
|
|
|
|
const [ticketResult] = await connection.execute(ticketQuery, values);
|
|
|
|
|
const ticketId = ticketResult.insertId;
|
|
|
|
|
|
|
|
|
|
// Adiciona o requerente
|
|
|
|
|
await connection.execute(
|
|
|
|
|
`INSERT INTO glpi_tickets_users
|
|
|
|
|
(tickets_id, users_id, type, use_notification, alternative_email)
|
|
|
|
|
VALUES (?, ?, 1, 1, '')`,
|
|
|
|
|
[ticketId, 917]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Adiciona o grupo
|
|
|
|
|
await connection.execute(
|
|
|
|
|
`INSERT INTO glpi_groups_tickets
|
|
|
|
|
(tickets_id, groups_id, type)
|
|
|
|
|
VALUES (?, ?, 2)`,
|
|
|
|
|
[ticketId, 30]
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
await connection.commit();
|
|
|
|
|
|
|
|
|
|
logInfo(`✅ Ticket GLPI criado: ${ticketId}`);
|
|
|
|
|
return ticketId;
|
|
|
|
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
logError(error, `❌ Erro ao criar ticket GLPI`);
|
|
|
|
|
throw error;
|
|
|
|
|
} finally {
|
|
|
|
|
connection.release();
|
2025-09-10 20:52:12 -03:00
|
|
|
}
|
2025-09-15 20:55:19 -03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Verifica se um ticket já existe no GLPI
|
|
|
|
|
static async ticketExists(snTicketNumber) {
|
|
|
|
|
try {
|
|
|
|
|
const query = `
|
2025-10-17 16:58:14 -03:00
|
|
|
SELECT id FROM glpi_tickets WHERE name LIKE ? OR content LIKE ? LIMIT 1
|
2025-09-15 20:55:19 -03:00
|
|
|
`;
|
|
|
|
|
|
2025-10-17 16:58:14 -03:00
|
|
|
const values = [
|
2025-09-15 20:55:19 -03:00
|
|
|
`%${snTicketNumber}%`,
|
|
|
|
|
`%${snTicketNumber}%`
|
2025-10-17 16:58:14 -03:00
|
|
|
];
|
2025-09-15 20:55:19 -03:00
|
|
|
|
2025-10-17 16:58:14 -03:00
|
|
|
|
|
|
|
|
const [rows] = await glpiPool.execute(query,values);
|
|
|
|
|
|
2025-09-15 20:55:19 -03:00
|
|
|
return rows.length > 0 ? rows[0].id : null;
|
|
|
|
|
} catch (error) {
|
2025-10-17 16:58:14 -03:00
|
|
|
logError(`Erro ao verificar ticket existente: ${error}`);
|
2025-09-15 20:55:19 -03:00
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-23 21:01:01 -03:00
|
|
|
// Buscar followups (notas) de um ticket no GLPI
|
|
|
|
|
static async getFollowupsByItemId(itemId) {
|
2025-10-23 17:30:03 -03:00
|
|
|
const connection = await glpiPool.getConnection();
|
2025-09-23 21:01:01 -03:00
|
|
|
try {
|
2025-10-17 16:58:14 -03:00
|
|
|
const query = 'SELECT id, content, date, date_creation, date_mod FROM glpi_itilfollowups WHERE items_id = ? AND users_id != 971';
|
2025-10-23 17:30:03 -03:00
|
|
|
const [rows] = await connection.execute(query, [itemId]);
|
2025-09-23 21:01:01 -03:00
|
|
|
return rows;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logError(error, 'Erro ao buscar followups do ticket');
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
2025-10-17 16:58:14 -03:00
|
|
|
static async insertComment(comment, items_id) {
|
2025-10-23 17:30:03 -03:00
|
|
|
const connection = await glpiPool.getConnection();
|
2025-10-17 16:58:14 -03:00
|
|
|
try {
|
2025-10-23 17:30:03 -03:00
|
|
|
const query = 'INSERT INTO glpi_itilfollowups(itemtype, items_id, date, users_id, content, date_creation, date_mod, timeline_position) VALUES(?, ?, ?, ?, ?, ?, ?, ?)';
|
2025-10-17 16:58:14 -03:00
|
|
|
const values = ['Ticket', items_id, comment.created_at, 971, comment.content , comment.created_at, comment.created_at, 1];
|
|
|
|
|
|
2025-10-23 17:30:03 -03:00
|
|
|
const [result] = await connection.execute(query, values);
|
|
|
|
|
return [{ id: result.insertId }]; // Retorna o ID inserido
|
2025-10-17 16:58:14 -03:00
|
|
|
}catch(error){
|
2025-10-23 17:30:03 -03:00
|
|
|
logError(`Erro ao inserir o comentário no GLPI: ${error}`);
|
|
|
|
|
} finally {
|
|
|
|
|
if (connection) connection.release();
|
2025-10-17 16:58:14 -03:00
|
|
|
}
|
|
|
|
|
}
|
2025-09-10 20:52:12 -03:00
|
|
|
|
2025-10-23 17:30:03 -03:00
|
|
|
// Busca tickets que foram fechados ou solucionados no GLPI
|
|
|
|
|
static async getClosedTickets(watermark, limit = 100) {
|
|
|
|
|
const connection = await glpiPool.getConnection();
|
|
|
|
|
try {
|
|
|
|
|
// Status 5 = Solucionado, Status 6 = Fechado
|
|
|
|
|
const query = `
|
|
|
|
|
SELECT
|
|
|
|
|
t.id,
|
|
|
|
|
t.solvedate,
|
|
|
|
|
t.closedate,
|
|
|
|
|
s.content as solution
|
|
|
|
|
FROM glpi_tickets t
|
|
|
|
|
LEFT JOIN glpi_itilsolutions s ON t.id = s.items_id AND s.itemtype = 'Ticket'
|
|
|
|
|
WHERE t.status IN (5, 6) AND t.date_mod >= ? LIMIT ?
|
|
|
|
|
`;
|
|
|
|
|
const [rows] = await connection.execute(query, [watermark, limit]);
|
|
|
|
|
return rows;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logError(error, '❌ Erro ao buscar tickets fechados no GLPI');
|
|
|
|
|
throw error;
|
|
|
|
|
} finally {
|
|
|
|
|
connection.release();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fecha um ticket no GLPI, adicionando uma solução
|
|
|
|
|
static async closeTicket(ticketId, solutionContent) {
|
|
|
|
|
const connection = await glpiPool.getConnection();
|
|
|
|
|
try {
|
|
|
|
|
await connection.beginTransaction();
|
|
|
|
|
|
|
|
|
|
// Adiciona a solução
|
|
|
|
|
const solutionQuery = `INSERT INTO glpi_itilsolutions (itemtype, items_id, content, users_id_editor, date_creation, date_mod) VALUES ('Ticket', ?, ?, ?, NOW(), NOW())`;
|
|
|
|
|
await connection.execute(solutionQuery, [ticketId, solutionContent, 971]); // 971 = ID do usuário da integração
|
|
|
|
|
|
|
|
|
|
// Atualiza o status do ticket para "Solucionado" (status 5)
|
|
|
|
|
const updateTicketQuery = `UPDATE glpi_tickets SET status = 5, solvedate = NOW() WHERE id = ?`;
|
|
|
|
|
await connection.execute(updateTicketQuery, [ticketId]);
|
|
|
|
|
|
|
|
|
|
await connection.commit();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
logError(error, `❌ Erro ao fechar ticket ${ticketId} no GLPI`);
|
|
|
|
|
throw error;
|
|
|
|
|
} finally {
|
|
|
|
|
connection.release();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-17 16:58:14 -03:00
|
|
|
|
2025-10-23 17:30:03 -03:00
|
|
|
// Busca a solução de um ticket no GLPI
|
|
|
|
|
static async getTicketSolution(glpiTicketId) {
|
|
|
|
|
const connection = await glpiPool.getConnection();
|
|
|
|
|
try {
|
|
|
|
|
const query = `
|
|
|
|
|
SELECT content, date_mod, solutiontypes_id
|
|
|
|
|
FROM glpi_itilsolutions
|
|
|
|
|
WHERE items_id = ? AND itemtype = 'Ticket'
|
|
|
|
|
ORDER BY date_mod DESC LIMIT 1`;
|
|
|
|
|
const [rows] = await connection.execute(query, [glpiTicketId]);
|
|
|
|
|
return rows[0] || null;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logError(error, `❌ Erro ao buscar solução do ticket GLPI ${glpiTicketId}`);
|
|
|
|
|
throw error;
|
|
|
|
|
} finally {
|
|
|
|
|
connection.release();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Força o fechamento de um ticket (muda status para 6)
|
|
|
|
|
static async forceCloseTicket(ticketId) {
|
|
|
|
|
const connection = await glpiPool.getConnection();
|
|
|
|
|
try {
|
|
|
|
|
// Atualiza o status do ticket para "Fechado" (status 6)
|
|
|
|
|
const updateTicketQuery = `UPDATE glpi_tickets SET status = 6, closedate = NOW() WHERE id = ?`;
|
|
|
|
|
await connection.execute(updateTicketQuery, [ticketId]);
|
|
|
|
|
logInfo(`✅ Ticket GLPI ${ticketId} forçado para o estado Fechado (6).`);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logError(error, `❌ Erro ao forçar fechamento do ticket ${ticketId} no GLPI`);
|
|
|
|
|
// Não lança o erro para não parar o fluxo principal por uma ação secundária
|
|
|
|
|
} finally {
|
|
|
|
|
if (connection) connection.release();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async updateStatus(ticketId, status) {
|
|
|
|
|
const connection = await glpiPool.getConnection();
|
|
|
|
|
try {
|
|
|
|
|
await connection.beginTransaction();
|
|
|
|
|
|
|
|
|
|
status = this.mapStatus(status);
|
|
|
|
|
|
|
|
|
|
const updateQuery = `UPDATE glpi_tickets SET status = ? WHERE id = ?`;
|
|
|
|
|
await connection.execute(updateQuery, [status, ticketId]);
|
|
|
|
|
|
|
|
|
|
await connection.commit();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
await connection.rollback();
|
|
|
|
|
logError(error, `❌ Erro ao atualiza ticket ${ticketId} no GLPI`);
|
|
|
|
|
throw error;
|
|
|
|
|
} finally {
|
|
|
|
|
connection.release();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Busca o status atual de um ticket no GLPI
|
|
|
|
|
static async getTicketStatus(glpiTicketId) {
|
|
|
|
|
const connection = await glpiPool.getConnection();
|
|
|
|
|
try {
|
|
|
|
|
const query = `SELECT status FROM glpi_tickets WHERE id = ?`;
|
|
|
|
|
const [rows] = await connection.execute(query, [glpiTicketId]);
|
|
|
|
|
if (rows.length > 0) {
|
|
|
|
|
return rows[0].status;
|
|
|
|
|
}
|
|
|
|
|
return null;
|
|
|
|
|
} catch (error) {
|
|
|
|
|
logError(error, `❌ Erro ao buscar status do ticket GLPI ${glpiTicketId}`);
|
|
|
|
|
throw error;
|
|
|
|
|
} finally {
|
|
|
|
|
connection.release();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
2025-09-15 20:55:19 -03:00
|
|
|
/**
|
|
|
|
|
* @module TicketGlpiModel
|
|
|
|
|
* @description Este módulo fornece métodos para gerenciar tickets no GLPI.
|
|
|
|
|
* Inclui funcionalidades para mapear status, prioridade e categoria,
|
|
|
|
|
* formatar a descrição do ticket, determinar o SLA, criar tickets e
|
|
|
|
|
* verificar se um ticket já existe no GLPI.
|
|
|
|
|
*
|
|
|
|
|
* O módulo usa expressões regulares para extrair informações da descrição do ticket,
|
|
|
|
|
* como email, número de telefone e ramal, quando essas informações não são
|
|
|
|
|
* fornecidas diretamente. Também formata a descrição em HTML para melhor
|
|
|
|
|
* apresentação no GLPI.
|
|
|
|
|
*
|
|
|
|
|
* O módulo interage com o banco de dados GLPI através da conexão glpiPool.
|
|
|
|
|
*/
|
2025-09-10 20:52:12 -03:00
|
|
|
module.exports = TicketGlpiModel;
|