fix: tentativo di fix per il riconoscimento del ticket ma otrs è stronzo-merda. feat: nella pagina di anteprima del ticket, aggiunto pulsante per aprire in una scheda interna.
This commit is contained in:
@@ -433,10 +433,8 @@ const EmailCompose = (() => {
|
|||||||
} else {
|
} else {
|
||||||
const tn = options.ticketTn || '';
|
const tn = options.ticketTn || '';
|
||||||
const title = options.ticketTitle || '';
|
const title = options.ticketTitle || '';
|
||||||
subjectEl.value = tn ? `Re: [Ticket#${tn}] ${title}` : title;
|
subjectEl.value = tn ? `[Ticket#${tn}] Re: ${title}` : title;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Signature and groups select
|
|
||||||
const sigSelect = document.getElementById('ec-signature-select');
|
const sigSelect = document.getElementById('ec-signature-select');
|
||||||
const groupsSelect = document.getElementById('ec-groups-select');
|
const groupsSelect = document.getElementById('ec-groups-select');
|
||||||
const agentId = App.currentAgentId || 0;
|
const agentId = App.currentAgentId || 0;
|
||||||
|
|||||||
@@ -161,6 +161,7 @@ const TicketDetailView = {
|
|||||||
<h2 class="ticket-detail-title" style="display:inline-flex; align-items:center; gap:6px; flex-wrap:wrap;">
|
<h2 class="ticket-detail-title" style="display:inline-flex; align-items:center; gap:6px; flex-wrap:wrap;">
|
||||||
<span class="copy-ticket-btn" data-tn="${App.escapeHtml(ticket.title || '')}" style="cursor: pointer; font-size: 0.85rem;" title="Copia titolo ticket">📋</span>
|
<span class="copy-ticket-btn" data-tn="${App.escapeHtml(ticket.title || '')}" style="cursor: pointer; font-size: 0.85rem;" title="Copia titolo ticket">📋</span>
|
||||||
${App.escapeHtml(ticket.title || '(senza titolo)')}
|
${App.escapeHtml(ticket.title || '(senza titolo)')}
|
||||||
|
<button class="open-tab-btn" data-id="${ticket.id}" data-tn="${ticket.tn}" data-title="${App.escapeHtml(ticket.title || '(senza titolo)')}" onclick="App.openTab(${ticket.id}, '${ticket.tn}', this.dataset.title); event.stopPropagation();" title="Apri in scheda" style="display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; background:var(--accent-primary); color:#fff; border:none; border-radius:50%; font-size:10px; font-weight:800; font-family:sans-serif; margin-left:6px; cursor:pointer; line-height:16px;">+</button>
|
||||||
</h2>
|
</h2>
|
||||||
<div class="ticket-meta-badges">
|
<div class="ticket-meta-badges">
|
||||||
<span class="badge badge-state" data-state-type="${(ticket.state_type || '').toLowerCase()}">${ticket.state_name}</span>
|
<span class="badge badge-state" data-state-type="${(ticket.state_type || '').toLowerCase()}">${ticket.state_name}</span>
|
||||||
@@ -566,7 +567,7 @@ const TicketDetailView = {
|
|||||||
this.updateNoteAttachmentList();
|
this.updateNoteAttachmentList();
|
||||||
}
|
}
|
||||||
|
|
||||||
this.bindEvents(ticket, articles, container, groupsData);
|
this.bindEvents(ticket, articles, container, groupsData, attachments);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
@@ -580,7 +581,7 @@ const TicketDetailView = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
bindEvents(ticket, articles, container, groupsData) {
|
bindEvents(ticket, articles, container, groupsData, attachments) {
|
||||||
// Quick-edit change detection
|
// Quick-edit change detection
|
||||||
const fields = document.querySelectorAll('.quick-edit-select:not(#qe-queue-search), #qe-queue, #qe-customer-user-id, #qe-customer-id');
|
const fields = document.querySelectorAll('.quick-edit-select:not(#qe-queue-search), #qe-queue, #qe-customer-user-id, #qe-customer-id');
|
||||||
const saveBtn = document.getElementById('qe-save');
|
const saveBtn = document.getElementById('qe-save');
|
||||||
|
|||||||
+4
-3
@@ -226,7 +226,7 @@ router.post('/send', async (req, res) => {
|
|||||||
if (!ticketResult.rows.length) return res.status(404).json({ error: 'Ticket non trovato' });
|
if (!ticketResult.rows.length) return res.status(404).json({ error: 'Ticket non trovato' });
|
||||||
|
|
||||||
const { tn, title } = ticketResult.rows[0];
|
const { tn, title } = ticketResult.rows[0];
|
||||||
const subject = customSubject || `Re: [Ticket#${tn}] ${title}`;
|
const subject = customSubject || `[Ticket#${tn}] Re: ${title}`;
|
||||||
|
|
||||||
// 2. Build BCC list (include OTRS system mailbox if keepHelpdeskCopy is true)
|
// 2. Build BCC list (include OTRS system mailbox if keepHelpdeskCopy is true)
|
||||||
const bccList = [...bcc];
|
const bccList = [...bcc];
|
||||||
@@ -263,7 +263,8 @@ router.post('/send', async (req, res) => {
|
|||||||
const messageId = `<${Date.now()}.${Math.random().toString(36).substring(2)}@pharmaidea.com>`;
|
const messageId = `<${Date.now()}.${Math.random().toString(36).substring(2)}@pharmaidea.com>`;
|
||||||
|
|
||||||
// 3. Send via configured mailer (Graph API or SMTP)
|
// 3. Send via configured mailer (Graph API or SMTP)
|
||||||
await sendMail({ to, cc, bcc: bccList, subject, bodyHtml: processedBodyHtml, attachments, inlineImages: finalInlineImages, inReplyTo, references, messageId });
|
const mailResult = await sendMail({ to, cc, bcc: bccList, subject, bodyHtml: processedBodyHtml, attachments, inlineImages: finalInlineImages, inReplyTo, references, messageId });
|
||||||
|
const finalMessageId = (mailResult && mailResult.internetMessageId) || messageId;
|
||||||
|
|
||||||
// 4. Log article in OTRS ticket via DB as a standard Email article
|
// 4. Log article in OTRS ticket via DB as a standard Email article
|
||||||
try {
|
try {
|
||||||
@@ -308,7 +309,7 @@ router.post('/send', async (req, res) => {
|
|||||||
await pool.query(`
|
await pool.query(`
|
||||||
INSERT INTO article_data_mime (article_id, a_from, a_to, a_cc, a_bcc, a_subject, a_body, a_content_type, a_message_id, incoming_time, create_time, create_by, change_time, change_by)
|
INSERT INTO article_data_mime (article_id, a_from, a_to, a_cc, a_bcc, a_subject, a_body, a_content_type, a_message_id, incoming_time, create_time, create_by, change_time, change_by)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7, 'text/html; charset=utf-8', $8, $9, $11, $10, $11, $10)`,
|
VALUES ($1, $2, $3, $4, $5, $6, $7, 'text/html; charset=utf-8', $8, $9, $11, $10, $11, $10)`,
|
||||||
[articleId, aFrom, toList, cc.join(', '), bccList.join(', '), subject, processedBodyHtml, messageId, now, agentId || 1, localNow]
|
[articleId, aFrom, toList, cc.join(', '), bccList.join(', '), subject, processedBodyHtml, finalMessageId, now, agentId || 1, localNow]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Helper to strip HTML tags
|
// Helper to strip HTML tags
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
require('dotenv').config();
|
require('dotenv').config({ path: require('path').resolve(__dirname, '.env') });
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|||||||
+26
-3
@@ -60,7 +60,7 @@ async function getAccessToken() {
|
|||||||
* @param {Array} [options.inlineImages] - [{ cid, content (base64), contentType }]
|
* @param {Array} [options.inlineImages] - [{ cid, content (base64), contentType }]
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function sendMail({ to, cc = [], bcc = [], subject, bodyHtml, attachments = [], inlineImages = [], inReplyTo, references }) {
|
async function sendMail({ to, cc = [], bcc = [], subject, bodyHtml, attachments = [], inlineImages = [], inReplyTo, references, messageId }) {
|
||||||
const sender = process.env.AZURE_MAIL_SENDER;
|
const sender = process.env.AZURE_MAIL_SENDER;
|
||||||
if (!sender) throw new Error('AZURE_MAIL_SENDER non configurato nel .env');
|
if (!sender) throw new Error('AZURE_MAIL_SENDER non configurato nel .env');
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ async function sendMail({ to, cc = [], bcc = [], subject, bodyHtml, attachments
|
|||||||
attachments: allAttachments,
|
attachments: allAttachments,
|
||||||
internetMessageHeaders: headers.length ? headers : undefined,
|
internetMessageHeaders: headers.length ? headers : undefined,
|
||||||
},
|
},
|
||||||
saveToSentItems: false,
|
saveToSentItems: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(sender)}/sendMail`;
|
const url = `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(sender)}/sendMail`;
|
||||||
@@ -130,7 +130,30 @@ async function sendMail({ to, cc = [], bcc = [], subject, bodyHtml, attachments
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 202) {
|
if (res.status === 202) {
|
||||||
return; // Successo (Graph API risponde con 202 No Content)
|
let actualMessageId = messageId;
|
||||||
|
try {
|
||||||
|
// Wait 1.5 seconds for Exchange to process and place it in Sent Items
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
|
const searchUrl = `https://graph.microsoft.com/v1.0/users/${encodeURIComponent(sender)}/mailFolders/sentItems/messages?$filter=subject eq '${subject.replace(/'/g, "''")}'&$top=1&$select=internetMessageId`;
|
||||||
|
const searchRes = await fetch(searchUrl, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (searchRes.ok) {
|
||||||
|
const searchData = await searchRes.json();
|
||||||
|
if (searchData.value && searchData.value.length > 0) {
|
||||||
|
actualMessageId = searchData.value[0].internetMessageId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (searchErr) {
|
||||||
|
console.warn('[Graph Mailer] Failed to retrieve actual InternetMessageId from Sent Items:', searchErr.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { internetMessageId: actualMessageId };
|
||||||
}
|
}
|
||||||
|
|
||||||
const errText = await res.text();
|
const errText = await res.text();
|
||||||
|
|||||||
+3
-1
@@ -29,7 +29,7 @@ function getTransporter() {
|
|||||||
* Invia una email tramite SMTP (nodemailer).
|
* Invia una email tramite SMTP (nodemailer).
|
||||||
* Stessa interfaccia di graphMailer.sendMail.
|
* Stessa interfaccia di graphMailer.sendMail.
|
||||||
*/
|
*/
|
||||||
async function sendMail({ to, cc = [], bcc = [], subject, bodyHtml, attachments = [], inlineImages = [], inReplyTo, references }) {
|
async function sendMail({ to, cc = [], bcc = [], subject, bodyHtml, attachments = [], inlineImages = [], inReplyTo, references, messageId }) {
|
||||||
const transporter = getTransporter();
|
const transporter = getTransporter();
|
||||||
|
|
||||||
const mailOptions = {
|
const mailOptions = {
|
||||||
@@ -41,6 +41,7 @@ async function sendMail({ to, cc = [], bcc = [], subject, bodyHtml, attachments
|
|||||||
html: bodyHtml,
|
html: bodyHtml,
|
||||||
inReplyTo,
|
inReplyTo,
|
||||||
references,
|
references,
|
||||||
|
messageId,
|
||||||
attachments: [
|
attachments: [
|
||||||
...attachments.map(a => ({
|
...attachments.map(a => ({
|
||||||
filename: a.filename,
|
filename: a.filename,
|
||||||
@@ -57,6 +58,7 @@ async function sendMail({ to, cc = [], bcc = [], subject, bodyHtml, attachments
|
|||||||
};
|
};
|
||||||
|
|
||||||
await transporter.sendMail(mailOptions);
|
await transporter.sendMail(mailOptions);
|
||||||
|
return { internetMessageId: messageId };
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { sendMail };
|
module.exports = { sendMail };
|
||||||
|
|||||||
Reference in New Issue
Block a user