FEATURE: Chamados reabertos possuem uma nova coloração
This commit is contained in:
parent
ce294d4220
commit
41645422fe
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,3 +8,4 @@ servicenow_tickets.json
|
||||
location_mapping.log
|
||||
.env*
|
||||
!.env.example
|
||||
docs/checklist.md
|
||||
@ -6,27 +6,27 @@ const TicketGlpiModel = require('../models/ticketGlpiModel');
|
||||
const { logInfo } = require('../utils/logger');
|
||||
|
||||
const ensureReopenFromSNWhenGlpiSolved = async (ticket) => {
|
||||
if (ticket.glpi_sync_status !== 'solved') {
|
||||
return;
|
||||
const glpiStatus = await TicketGlpiModel.getTicketStatus(ticket.glpi_ticket_id);
|
||||
const glpiIsSolved = glpiStatus === 5;
|
||||
if (!glpiIsSolved) {
|
||||
return { reopened: false, glpiSolved: false };
|
||||
}
|
||||
|
||||
const ticketSn = await TicketSnModel.findById(ticket.sn_ticket_id);
|
||||
if (!ticketSn) {
|
||||
return;
|
||||
return { reopened: false, glpiSolved: true };
|
||||
}
|
||||
|
||||
const liveSnStatus = await fetchTicketLiveStatusFromServiceNow(ticketSn.sys_id, ticketSn.tipo);
|
||||
const snIsFinal = liveSnStatus === 'Resolvido' || liveSnStatus === 'Encerrado' || liveSnStatus === 'Encerrado - Omitido';
|
||||
if (snIsFinal) {
|
||||
return;
|
||||
return { reopened: false, glpiSolved: true };
|
||||
}
|
||||
|
||||
const glpiStatus = await TicketGlpiModel.getTicketStatus(ticket.glpi_ticket_id);
|
||||
if (glpiStatus === 5) {
|
||||
await TicketGlpiModel.updateStatus(ticket.glpi_ticket_id, 'Em Atendimento');
|
||||
await TicketSyncModel.updateStatus(ticket.sn_ticket_id, 'synced', 'synced');
|
||||
logInfo(`Reabertura detectada no SN para ticket ${ticket.sn_ticket_id}. GLPI ${ticket.glpi_ticket_id} retornou para Em Atendimento.`);
|
||||
}
|
||||
return { reopened: true, glpiSolved: true };
|
||||
};
|
||||
|
||||
const processCommentsController = async () => {
|
||||
@ -34,11 +34,18 @@ const processCommentsController = async () => {
|
||||
logInfo(`Tickets em monitoramento: ${ticketsToMonitor.length}`);
|
||||
|
||||
for (const ticket of ticketsToMonitor) {
|
||||
await ensureReopenFromSNWhenGlpiSolved(ticket);
|
||||
const reopenState = await ensureReopenFromSNWhenGlpiSolved(ticket);
|
||||
const reopenedThisCycle = reopenState.reopened;
|
||||
const glpiWasSolved = reopenState.glpiSolved;
|
||||
if (glpiWasSolved && !reopenedThisCycle) {
|
||||
logInfo(`Ticket ${ticket.glpi_ticket_id} permanece Solucionado no GLPI. Comentarios SN -> GLPI bloqueados ate reabertura.`);
|
||||
await syncCommentsGlpitoSN(ticket);
|
||||
continue;
|
||||
}
|
||||
|
||||
const comments = await fetchCommentsFromServiceNow(ticket);
|
||||
if (comments.length !== 0) {
|
||||
await syncCommentsSNtoGlpi(comments, ticket);
|
||||
await syncCommentsSNtoGlpi(comments, ticket, { highlightReopenContext: reopenedThisCycle });
|
||||
}
|
||||
|
||||
await syncCommentsGlpitoSN(ticket);
|
||||
|
||||
@ -28,7 +28,7 @@ const normalizeAuthorName = (rawAuthor) => {
|
||||
return toTitleCase(normalizedRaw);
|
||||
};
|
||||
|
||||
const formatSNUpdateForGlpi = (update) => {
|
||||
const formatSNUpdateForGlpi = (update, options = {}) => {
|
||||
const escapeHtml = (value) => String(value || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
@ -39,6 +39,9 @@ const formatSNUpdateForGlpi = (update) => {
|
||||
const content = escapeHtml(update.content || '');
|
||||
const isAutomaticTask = update.update_type === 'task' && /registro alterado por:/i.test(String(update.content || ''));
|
||||
const isSnClosureMessage = update.update_type === 'comment' && /chamado resolvido|chamado encerrado/i.test(String(update.content || ''));
|
||||
const isSnReopenMessageByText = update.update_type === 'comment' && /reabert|reopen/i.test(String(update.content || ''));
|
||||
const isSnReopenMessageByContext = Boolean(options.isReopenContextComment);
|
||||
const isSnReopenMessage = isSnReopenMessageByText || isSnReopenMessageByContext;
|
||||
|
||||
if (isAutomaticTask) {
|
||||
return `
|
||||
@ -59,6 +62,14 @@ const formatSNUpdateForGlpi = (update) => {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
if (isSnReopenMessage) {
|
||||
return `
|
||||
<div style="border:1px solid #9ae6b4; border-left:4px solid #2f855a; background:#f0fff4; padding:12px; border-radius:4px;">
|
||||
<div style="font-weight:700; color:#22543d; margin-bottom:6px;">Reabertura de chamado no ServiceNow</div>
|
||||
<div style="color:#276749; font-size:12px; white-space: pre-wrap;">${content}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div style="border:1px solid #d6e9ff; border-left:4px solid #2b6cb0; background:#f7fbff; padding:12px; border-radius:4px;">
|
||||
<div style="font-weight:700; color:#1a365d; margin-bottom:6px;">${author}</div>
|
||||
@ -153,7 +164,7 @@ const syncCommentsGlpitoSN = async (ticket) => {
|
||||
};
|
||||
|
||||
|
||||
const syncCommentsSNtoGlpi = async (comments, ticket) => {
|
||||
const syncCommentsSNtoGlpi = async (comments, ticket, options = {}) => {
|
||||
logInfo('INFO: Iniciando sincronizacao de comments do ServiceNow para o GLPI...');
|
||||
|
||||
const orderedComments = [...comments].sort((a, b) => {
|
||||
@ -161,17 +172,22 @@ const syncCommentsSNtoGlpi = async (comments, ticket) => {
|
||||
const bTime = new Date(b.created_at).getTime();
|
||||
return aTime - bTime;
|
||||
});
|
||||
let reopenHighlightPending = Boolean(options.highlightReopenContext);
|
||||
|
||||
for (const comment of orderedComments) {
|
||||
try {
|
||||
const isReopenContextComment = reopenHighlightPending && comment.update_type === 'comment';
|
||||
const formattedPayload = {
|
||||
...comment,
|
||||
content: formatSNUpdateForGlpi(comment)
|
||||
content: formatSNUpdateForGlpi(comment, { isReopenContextComment })
|
||||
};
|
||||
const isTask = comment.update_type === 'task';
|
||||
const insertedId = isTask
|
||||
? await TicketGlpiModel.insertTask(formattedPayload, ticket.glpi_ticket_id)
|
||||
: await TicketGlpiModel.insertComment(formattedPayload, ticket.glpi_ticket_id);
|
||||
if (isReopenContextComment) {
|
||||
reopenHighlightPending = false;
|
||||
}
|
||||
await TicketSyncModel.updateLastSync(ticket.sn_ticket_id, 'glpi');
|
||||
if (ticket.sn_sync_status === 'synced' && ticket.glpi_sync_status === 'synced') {
|
||||
await TicketSyncModel.updateStatusAndSourceLast(ticket.sn_ticket_id, 'synced', 'synced', 'SNOW');
|
||||
|
||||
@ -43,6 +43,12 @@ const normalizeServiceNowDateTime = (value) => {
|
||||
return `${parsed.getFullYear()}-${pad(parsed.getMonth() + 1)}-${pad(parsed.getDate())} ${pad(parsed.getHours())}:${pad(parsed.getMinutes())}:${pad(parsed.getSeconds())}`;
|
||||
};
|
||||
|
||||
const isAutomaticServiceNowTask = (entry) => {
|
||||
if (!entry || entry.element !== 'work_notes') return false;
|
||||
const content = String(entry.value || '');
|
||||
return /registro alterado por:/i.test(content);
|
||||
};
|
||||
|
||||
const fetchTicketsFromServiceNow = async (watermark) => {
|
||||
try {
|
||||
const watermarkDate = new Date(watermark);
|
||||
@ -285,10 +291,13 @@ const fetchCommentsFromServiceNow = async (ticket) => {
|
||||
// Em ambiente de teste, a regra pode ser desligada por env.
|
||||
const filteredComments = allComments.filter((entry) => {
|
||||
if (!shouldIgnoreDefaultUserJournal) {
|
||||
return true;
|
||||
return !isAutomaticServiceNowTask(entry);
|
||||
}
|
||||
const createdBy = String(entry.sys_created_by || '').trim().toLowerCase();
|
||||
const ignoredUser = ignoredJournalUsers.includes(createdBy);
|
||||
if (isAutomaticServiceNowTask(entry)) {
|
||||
return false;
|
||||
}
|
||||
if (entry.element === 'comments') {
|
||||
return !ignoredUser;
|
||||
}
|
||||
@ -322,20 +331,11 @@ const fetchCommentsFromServiceNow = async (ticket) => {
|
||||
continue;
|
||||
} else {
|
||||
|
||||
let normalizedContent = comment.value;
|
||||
if (
|
||||
comment.element === 'work_notes' &&
|
||||
typeof normalizedContent === 'string' &&
|
||||
normalizedContent.includes('Registro alterado por:')
|
||||
) {
|
||||
normalizedContent = `Tarefa automatica originada no ServiceNow (registro automatico).\n\nMensagem original:\n${normalizedContent}`;
|
||||
}
|
||||
|
||||
const commentData = {
|
||||
ticket_sync_id: ticket.id,
|
||||
update_type: comment.element === 'work_notes' ? 'task' : 'comment',
|
||||
source_system: 'servicenow',
|
||||
content: normalizedContent,
|
||||
content: comment.value,
|
||||
created_at: normalizeServiceNowDateTime(comment.sys_created_on),
|
||||
author: comment.sys_created_by || 'ServiceNow',
|
||||
updated_at: normalizeServiceNowDateTime(comment.sys_created_on),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user