FEATURE: Novo Cammpo adicionado!

- Campo de Justificativa adicionado para as requisições
This commit is contained in:
Rafael Alves Lopes 2026-02-19 18:22:38 -03:00
parent e84647a2ba
commit a4f6797c8f
8 changed files with 92 additions and 47 deletions

View File

@ -39,6 +39,8 @@ GLPI_DB_CHARSET=utf8mb4
# ID do usuario padrao do GLPI para criacao de tickets e comentarios
GLPI_DEFAULT_USER_ID=11111
GLPI_REQ_DEFAULT_JUSTIFICATIVA=Chamado aberto por colaborador CAOA sem Justificativa
GLPI_REQ_DEFAULT_DESCRIPTION=Chamado aberto por colaborador CAOA sem Descricao
# Banco intermediario (PostgreSQL)
SNGLPI_DB_HOST=10.0.120.75

View File

@ -44,6 +44,8 @@ GLPI_DB_CHARSET=utf8mb4
# ID do usuario padrao do GLPI para criacao de tickets e comentarios
GLPI_DEFAULT_USER_ID=1118
GLPI_REQ_DEFAULT_JUSTIFICATIVA=Chamado aberto por colaborador CAOA sem Justificativa
GLPI_REQ_DEFAULT_DESCRIPTION=Chamado aberto por colaborador CAOA sem Descricao
# --- Configuracao do Banco de Dados Intermediario (PostgreSQL) ---
SNGLPI_DB_HOST=localhost

View File

@ -30,28 +30,28 @@ const processAndSaveTickets = async (tickets, type) => {
let latestUpdateTimestamp = new Date(0);
for (const ticket of tickets) {
try {
if (type === 'requisicao' && (!ticket.description || ticket.description.trim() === '')) {
logInfo(`Descricao vazia para requisicao ${ticket.number}. Buscando em sc_item_option...`);
if (type === 'requisicao') {
const ticketNumber = typeof ticket.number === 'object' ? ticket.number?.value : ticket.number;
logInfo(`Buscando variaveis de catalogo para requisicao ${ticketNumber}.`);
const extraData = await fetchScItemOptionValue(ticket.sys_id, 'justificativa');
const vars = {};
(extraData || []).forEach(item => {
const question = item['sc_item_option.item_option_new.question_text'];
const value = item['sc_item_option.value'];
if (question === 'Select what you want') vars.whatYouWant = value;
if (question === 'Please justify the need') vars.justification = value;
if (question === 'Telephone') vars.telephone = value;
(extraData || []).forEach((item) => {
const question = item.question_text;
const name = item.variable_name;
const value = item.value;
if (name === 'select_what_you_want' || question === 'Select what you want') vars.whatYouWant = value;
if (name === 'please_justify_the_need' || question === 'Please justify the need') vars.justification = value;
if (name === 'telephone_favorecido_ti' || question === 'Telephone') vars.telephone = value;
});
if (vars.justification) {
ticket.description = vars.justification;
logInfo(`Descricao da requisicao ${ticket.number} atualizada com valor de sc_item_option.`);
}
ticket.justificativa = vars.justification || null;
if (vars.telephone) {
ticket.telefone = vars.telephone;
logInfo(`Telefone da requisicao ${ticket.number} atualizado com valor de sc_item_option.`);
}
logInfo(`Variaveis da requisicao ${ticketNumber}: justificativa=${ticket.justificativa ? 'ok' : 'vazia'}, telefone=${ticket.telefone ? 'ok' : 'vazio'}.`);
}
const { id: idTableTicketSn, wasInserted } = await TicketSnModel.saveTicket(ticket, type);

View File

@ -4,6 +4,8 @@ const LocationMappingModel = require('./locationMappingModel');
// Carrega o ID do usuário padrão do GLPI a partir das variáveis de ambiente
const GLPI_DEFAULT_USER_ID = parseInt(process.env.GLPI_DEFAULT_USER_ID, 10) || 1118;
const GLPI_REQ_DEFAULT_JUSTIFICATIVA = process.env.GLPI_REQ_DEFAULT_JUSTIFICATIVA || 'Chamado aberto por colaborador CAOA sem Justificativa';
const GLPI_REQ_DEFAULT_DESCRIPTION = process.env.GLPI_REQ_DEFAULT_DESCRIPTION || 'Chamado aberto por colaborador CAOA sem Descricao';
class TicketGlpiModel {
@ -118,9 +120,31 @@ class TicketGlpiModel {
return extractInfo(/(\d{3,4})/i, description);
};
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';
const rawDescription = ticketData.description;
const isRequest = ticketData.type === 'requisicao';
const justificationForDisplay = isRequest
? (ticketData.justificativa && ticketData.justificativa.trim()
? ticketData.justificativa
: GLPI_REQ_DEFAULT_JUSTIFICATIVA)
: null;
const descriptionForDisplay = isRequest
? (rawDescription && rawDescription.trim()
? rawDescription
: GLPI_REQ_DEFAULT_DESCRIPTION)
: (rawDescription || 'N/A');
const email = ticketData.caller_email || extractEmailFromDescription(rawDescription) || 'N/A';
const phone = ticketData.caller_phone || extractPhoneFromDescription(rawDescription) || 'N/A';
const ramal = ticketData.caller_ramal || extractRamalFromDescription(rawDescription) || 'N/A';
const requestExtraHtml = isRequest
? `
<div style="margin-top: 12px; padding: 12px; border: 1px solid #d9e2ec; border-left: 4px solid #2b6cb0; background-color: #f8fbff;">
<div style="font-weight: 700; margin-bottom: 8px;">Justificativa</div>
<div style="white-space: pre-wrap;">${justificationForDisplay}</div>
</div>
`
: '';
let htmlDescription = `
<table style="width:100%; border-collapse: collapse;">
@ -149,10 +173,11 @@ class TicketGlpiModel {
<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'}
${requestExtraHtml}
<div style="margin-top: 12px; padding: 12px; border: 1px solid #e2e8f0; border-left: 4px solid #4a5568; background-color: #fcfcfc;">
<div style="font-weight: 700; margin-bottom: 8px;">Descricao</div>
<div style="white-space: pre-wrap;">${descriptionForDisplay}</div>
</div>
`;
return htmlDescription;
}

View File

@ -1,5 +1,5 @@
const pool = require('../data/database');
const { logInfo, logError, logWarning } = require('../utils/logger');
const { logInfo, logError } = require('../utils/logger');
class TicketSnModel {
@ -7,8 +7,8 @@ class TicketSnModel {
const query = `
INSERT INTO tickets_sn
(ticket_number, sys_id, short_description, status, description, caller_id, caller_email,
location_id, location_name, ramal, telefone, opened_at, tipo)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
location_id, location_name, ramal, telefone, justificativa, opened_at, tipo)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
ON CONFLICT (ticket_number)
DO UPDATE SET
sys_id = EXCLUDED.sys_id,
@ -21,12 +21,13 @@ class TicketSnModel {
location_name = EXCLUDED.location_name,
ramal = EXCLUDED.ramal,
telefone = EXCLUDED.telefone,
justificativa = EXCLUDED.justificativa,
opened_at = EXCLUDED.opened_at,
tipo = EXCLUDED.tipo
RETURNING id, (xmax = 0) AS "wasInserted"`;
const ticketNumber = typeof ticketData.number === 'object' ? ticketData.number?.value : ticketData.number;
let callerName, callerEmail, ramal, telefone, sysId, shortDescription, state, description, locationId, locationName, openedAt;
let callerName, callerEmail, ramal, telefone, justificativa, sysId, shortDescription, state, description, locationId, locationName, openedAt;
if (typeTicket === 'incidente') {
@ -39,6 +40,7 @@ class TicketSnModel {
shortDescription = ticketData.short_description || null;
state = ticketData.state || null;
description = ticketData.description || null;
justificativa = null;
const locationLink = ticketData.location?.link;
locationId = locationLink ? locationLink.substring(locationLink.lastIndexOf('/') + 1) : null;
locationName = ticketData.location?.display_value || null;
@ -47,11 +49,12 @@ class TicketSnModel {
callerName = ticketData.opened_by?.display_value || null;
callerEmail = ticketData['opened_by.email'] || null;
ramal = ticketData['variables.numero_ramal'] || null;
telefone = ticketData['variables.telephone_favorecido_rh'] || ticketData.telefone || null;
telefone = ticketData['variables.telephone_favorecido_rh'] || ticketData['variables.telephone_favorecido_ti'] || ticketData.telefone || null;
sysId = ticketData.sys_id || null;
shortDescription = ticketData.short_description || null;
state = ticketData.state || null;
description = ticketData.description || null;
justificativa = ticketData.justificativa || null;
const locationLink = ticketData.location?.link;
locationId = locationLink ? locationLink.substring(locationLink.lastIndexOf('/') + 1) : null;
locationName = ticketData.location?.display_value || null;
@ -70,6 +73,7 @@ class TicketSnModel {
locationName,
ramal,
telefone,
justificativa,
openedAt
? (typeof openedAt === 'string' && openedAt.includes('/')
? new Date(openedAt.replace(/(\d{2})\/(\d{2})\/(\d{4})/, '$3-$2-$1'))
@ -119,6 +123,7 @@ class TicketSnModel {
ts.location_name,
ts.ramal,
ts.telefone,
ts.justificativa,
ts.opened_at,
ts.tipo,
ts.created_at,

View File

@ -34,6 +34,7 @@ CREATE TABLE tickets_sn (
location_name VARCHAR(255), -- Nome da localização no ServiceNow
ramal VARCHAR(100), -- Ramal do solicitante
telefone VARCHAR(100), -- Telefone do solicitante
justificativa TEXT, -- Justificativa da requisicao (catalog variable)
opened_at TIMESTAMP, -- Data e hora de abertura no ServiceNow
created_at TIMESTAMP DEFAULT NOW(), -- Data e hora de criação do registro
updated_at TIMESTAMP DEFAULT NOW() -- Data e hora da última atualização do registro
@ -225,6 +226,7 @@ COMMENT ON COLUMN tickets_sn.location_id IS 'ID da localização no ServiceNow';
COMMENT ON COLUMN tickets_sn.location_name IS 'Nome da localização no ServiceNow';
COMMENT ON COLUMN tickets_sn.ramal IS 'Ramal do solicitante';
COMMENT ON COLUMN tickets_sn.telefone IS 'Telefone do solicitante';
COMMENT ON COLUMN tickets_sn.justificativa IS 'Justificativa da requisicao vinda das variaveis de catalogo';
COMMENT ON COLUMN tickets_sn.opened_at IS 'Data e hora de abertura no ServiceNow';
COMMENT ON COLUMN tickets_sn.created_at IS 'Data e hora de criação do registro';
COMMENT ON COLUMN tickets_sn.updated_at IS 'Data e hora da última atualização do registro';
@ -293,3 +295,8 @@ DELETE FROM tickets_sn WHERE status != 'Encerrado' AND status != 'Encerrado - Om
UPDATE sync_control
SET last_watermark = '1970-01-01 00:00:00+00';
-- Migracao para bases existentes:
ALTER TABLE tickets_sn ADD COLUMN IF NOT EXISTS justificativa TEXT;

View File

@ -2,7 +2,7 @@ const TicketSnModel = require('../models/ticketSnModel');
const TicketGlpiModel = require('../models/ticketGlpiModel');
const TicketUpdateModel = require('../models/ticketUpdateModel');
const TicketSyncModel = require('../models/ticketSyncModel');
const { logInfo, logError, logWarning } = require('../utils/logger');
const { logInfo, logError } = require('../utils/logger');
const { addWorkNoteToServiceNow } = require('./servicenowService');
const syncTicketsToGlpi = async () => {
@ -61,6 +61,7 @@ const processSingleTicket = async (ticket) => {
ticket_number: ticket.ticket_number,
short_description: ticket.short_description,
description: ticket.description,
justificativa: ticket.justificativa,
caller_id: ticket.caller_id,
caller_email: ticket.caller_email,
caller_phone: ticket.telefone,
@ -73,18 +74,6 @@ const processSingleTicket = async (ticket) => {
opened_at: ticket.opened_at,
updated_at: ticket.updated_at
};
if (glpiData.type === 'requisicao' && (!glpiData.description || glpiData.description.trim() === '')) {
logInfo(`Descrição vazia para requisição ${ticket.ticket_number}. Tentando buscar de sc_item_option.`);
const scItemOptionDescription = await require('./servicenowService').fetchScItemOptionValue(ticket.sys_id, 'justificativa');
if (scItemOptionDescription) {
glpiData.description = scItemOptionDescription;
logInfo(`Descrição atualizada para requisição ${ticket.ticket_number} a partir de sc_item_option.`);
} else {
logWarning(`Não foi possível obter descrição de sc_item_option para requisição ${ticket.ticket_number}.`);
}
}
const glpiTicketId = await TicketGlpiModel.createTicket(glpiData);
await TicketSyncModel.updateLastSync(ticket.id, 'glpi');
await TicketSyncModel.updateStatusAndGlpiTicket(ticket.id, 'synced', 'synced', glpiTicketId);

View File

@ -495,14 +495,29 @@ const fetchScItemOptionValue = async (ritmSysId, variableName) => {
try {
const baseUrl = apiConfig.snScItemOptionConfig.baseUrl.replace(/\/$/, "");
const url = `${baseUrl}?sysparm_query=request_item=${ritmSysId}^sc_item_option.item_option_new.question_textINPlease justify the need,Telephone,Select what you want&sysparm_fields=sc_item_option.item_option_new.question_text,sc_item_option.value`;
const url = `${baseUrl}?sysparm_query=request_item=${ritmSysId}^sc_item_option.item_option_new.question_textINPlease justify the need,Telephone,Select what you want&sysparm_fields=sc_item_option.item_option_new.question_text,sc_item_option.item_option_new.name,sc_item_option.value`;
const response = await axios.get(url, {
auth: apiConfig.servicenowAuthentication.auth
});
const result = response.data.result;
return (result && result.length > 0) ? result : null;
const result = response.data.result || [];
if (result.length === 0) return null;
const normalize = (value) => {
if (value === null || value === undefined) return '';
if (typeof value === 'string') return value;
if (typeof value === 'object') {
return value.value || value.display_value || '';
}
return String(value);
};
return result.map((item) => ({
question_text: normalize(item['sc_item_option.item_option_new.question_text']),
variable_name: normalize(item['sc_item_option.item_option_new.name']),
value: normalize(item['sc_item_option.value'])
}));
} catch (error) {
logError(error, `ERRO: Falha ao buscar valor da variavel '${variableName}' para RITM ${ritmSysId}`);
return null;