fix: visualizzazione delle immagini nelle note e mail inviate. fix: corretto veramente problema del fusorario delle mail. feat: tooltip fullscreen per le immagini.
This commit is contained in:
@@ -49,6 +49,41 @@ const TicketDetailView = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
openImageLightbox(src) {
|
||||||
|
let overlay = document.getElementById('image-lightbox-overlay');
|
||||||
|
if (!overlay) {
|
||||||
|
overlay = document.createElement('div');
|
||||||
|
overlay.id = 'image-lightbox-overlay';
|
||||||
|
overlay.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: zoom-out;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
`;
|
||||||
|
overlay.innerHTML = `
|
||||||
|
<img id="image-lightbox-img" style="max-width: 90vw; max-height: 90vh; border-radius: 4px; box-shadow: 0 8px 32px rgba(0,0,0,0.5); cursor: default; transition: transform 0.2s ease;" />
|
||||||
|
`;
|
||||||
|
overlay.addEventListener('click', () => {
|
||||||
|
overlay.style.opacity = '0';
|
||||||
|
setTimeout(() => overlay.remove(), 200);
|
||||||
|
});
|
||||||
|
overlay.querySelector('img').addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
}
|
||||||
|
const imgEl = overlay.querySelector('img');
|
||||||
|
imgEl.src = src;
|
||||||
|
overlay.offsetHeight;
|
||||||
|
overlay.style.opacity = '1';
|
||||||
|
},
|
||||||
|
|
||||||
async render(id) {
|
async render(id) {
|
||||||
this.ticketId = id;
|
this.ticketId = id;
|
||||||
this.noteAttachments = [];
|
this.noteAttachments = [];
|
||||||
@@ -252,11 +287,33 @@ const TicketDetailView = {
|
|||||||
<div class="articles-timeline">
|
<div class="articles-timeline">
|
||||||
${articles.length > 0 ? articles.map(a => {
|
${articles.length > 0 ? articles.map(a => {
|
||||||
const hasHtml = (a.a_content_type || '').toLowerCase().includes('html') || a.a_body.includes('</') || a.a_body.includes('/>');
|
const hasHtml = (a.a_content_type || '').toLowerCase().includes('html') || a.a_body.includes('</') || a.a_body.includes('/>');
|
||||||
|
|
||||||
|
let processedBody = a.a_body || '';
|
||||||
|
const articleAttachments = (attachments || []).filter(att => att.article_id === a.article_id);
|
||||||
|
|
||||||
|
if (hasHtml) {
|
||||||
|
articleAttachments.forEach(att => {
|
||||||
|
if (att.content_id) {
|
||||||
|
const cleanCid = att.content_id.replace(/[<>]/g, '').trim();
|
||||||
|
if (cleanCid) {
|
||||||
|
const escapedCid = cleanCid.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||||
|
const regex = new RegExp(`cid:<?${escapedCid}>?`, 'gi');
|
||||||
|
processedBody = processedBody.replace(regex, `/api/tickets/attachments/${att.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (att.filename) {
|
||||||
|
const escapedFilename = att.filename.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||||
|
const regexFilename = new RegExp(`cid:<?${escapedFilename}>?`, 'gi');
|
||||||
|
processedBody = processedBody.replace(regexFilename, `/api/tickets/attachments/${att.id}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const displayBody = this.htmlMode && hasHtml
|
const displayBody = this.htmlMode && hasHtml
|
||||||
? `<iframe srcdoc="${a.a_body.replace(/"/g, '"')}" style="width:100%; border:none; background:var(--bg-card); border-radius:var(--radius-md); min-height:220px; font-family:inherit; color-scheme: dark;"></iframe>`
|
? `<iframe srcdoc="${processedBody.replace(/"/g, '"')}" style="width:100%; border:none; background:var(--bg-card); border-radius:var(--radius-md); min-height:220px; font-family:inherit; color-scheme: dark;"></iframe>`
|
||||||
: `<div class="article-body">${App.escapeHtml(a.a_body || '')}</div>`;
|
: `<div class="article-body">${App.escapeHtml(a.a_body || '')}</div>`;
|
||||||
|
|
||||||
const articleAttachments = (attachments || []).filter(att => att.article_id === a.article_id && att.filename !== 'file-1' && att.filename !== 'file-2');
|
const visibleAttachments = articleAttachments.filter(att => att.filename !== 'file-1' && att.filename !== 'file-2');
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="article-card sender-${(a.sender_type || 'system').toLowerCase()}">
|
<div class="article-card sender-${(a.sender_type || 'system').toLowerCase()}">
|
||||||
@@ -301,10 +358,10 @@ const TicketDetailView = {
|
|||||||
${a.a_subject ? `<div class="article-subject">${App.escapeHtml(a.a_subject)}</div>` : ''}
|
${a.a_subject ? `<div class="article-subject">${App.escapeHtml(a.a_subject)}</div>` : ''}
|
||||||
${displayBody}
|
${displayBody}
|
||||||
|
|
||||||
${articleAttachments.length > 0 ? `
|
${visibleAttachments.length > 0 ? `
|
||||||
<div class="article-attachments">
|
<div class="article-attachments">
|
||||||
${articleAttachments.map(att => `
|
${visibleAttachments.map(att => `
|
||||||
<a href="/api/tickets/attachments/${att.id}" class="attachment-badge" target="_blank" download="${att.filename}">
|
<a href="/api/tickets/attachments/${att.id}" class="attachment-badge" target="_blank" download="${att.filename}" data-is-image="${(att.content_type || '').startsWith('image/')}">
|
||||||
<span>📎</span>
|
<span>📎</span>
|
||||||
<strong>${App.escapeHtml(att.filename)}</strong>
|
<strong>${App.escapeHtml(att.filename)}</strong>
|
||||||
<span class="attachment-size">(${Math.round(att.content_size / 1024)} KB)</span>
|
<span class="attachment-size">(${Math.round(att.content_size / 1024)} KB)</span>
|
||||||
@@ -882,7 +939,28 @@ const TicketDetailView = {
|
|||||||
|
|
||||||
if (window.EmailCompose) {
|
if (window.EmailCompose) {
|
||||||
const hasHtml = (article.a_content_type || '').toLowerCase().includes('html') || article.a_body.includes('</') || article.a_body.includes('/>');
|
const hasHtml = (article.a_content_type || '').toLowerCase().includes('html') || article.a_body.includes('</') || article.a_body.includes('/>');
|
||||||
const quotedBody = hasHtml ? article.a_body : App.escapeHtml(article.a_body || '').replace(/\n/g, '<br>');
|
|
||||||
|
let processedQuoted = article.a_body || '';
|
||||||
|
if (hasHtml) {
|
||||||
|
const articleAttachments = (attachments || []).filter(att => att.article_id === article.article_id);
|
||||||
|
articleAttachments.forEach(att => {
|
||||||
|
if (att.content_id) {
|
||||||
|
const cleanCid = att.content_id.replace(/[<>]/g, '').trim();
|
||||||
|
if (cleanCid) {
|
||||||
|
const escapedCid = cleanCid.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||||
|
const regex = new RegExp(`cid:<?${escapedCid}>?`, 'gi');
|
||||||
|
processedQuoted = processedQuoted.replace(regex, `/api/tickets/attachments/${att.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (att.filename) {
|
||||||
|
const escapedFilename = att.filename.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&');
|
||||||
|
const regexFilename = new RegExp(`cid:<?${escapedFilename}>?`, 'gi');
|
||||||
|
processedQuoted = processedQuoted.replace(regexFilename, `/api/tickets/attachments/${att.id}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const quotedBody = hasHtml ? processedQuoted : App.escapeHtml(article.a_body || '').replace(/\n/g, '<br>');
|
||||||
|
|
||||||
const initialBodyHtml = `
|
const initialBodyHtml = `
|
||||||
<p><br></p>
|
<p><br></p>
|
||||||
@@ -1188,5 +1266,41 @@ const TicketDetailView = {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inline image preview & lightbox binding
|
||||||
|
document.querySelectorAll('.articles-timeline iframe').forEach(iframe => {
|
||||||
|
const attachImageClick = () => {
|
||||||
|
try {
|
||||||
|
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
if (doc) {
|
||||||
|
doc.querySelectorAll('img').forEach(img => {
|
||||||
|
img.style.cursor = 'zoom-in';
|
||||||
|
img.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.openImageLightbox(img.src);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Cannot attach click listeners to iframe images', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
iframe.addEventListener('load', attachImageClick);
|
||||||
|
try {
|
||||||
|
const doc = iframe.contentDocument || iframe.contentWindow.document;
|
||||||
|
if (doc && doc.readyState === 'complete') {
|
||||||
|
attachImageClick();
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.attachment-badge[data-is-image="true"]').forEach(badge => {
|
||||||
|
badge.style.cursor = 'zoom-in';
|
||||||
|
badge.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.openImageLightbox(badge.href);
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
+27
-9
@@ -6,6 +6,13 @@ const pool = require('../db');
|
|||||||
const { db } = require('../activityDb');
|
const { db } = require('../activityDb');
|
||||||
const { sendMail } = require('../utils/mailer');
|
const { sendMail } = require('../utils/mailer');
|
||||||
|
|
||||||
|
// Helper to get local timestamp in YYYY-MM-DD HH:mm:ss format
|
||||||
|
function getLocalTimestamp() {
|
||||||
|
const d = new Date();
|
||||||
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||||
|
}
|
||||||
|
|
||||||
// ─── SIGNATURES ────────────────────────────────────────────────────────────────
|
// ─── SIGNATURES ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// GET /api/email/signatures — list signatures for a given agent
|
// GET /api/email/signatures — list signatures for a given agent
|
||||||
@@ -233,7 +240,15 @@ router.post('/send', async (req, res) => {
|
|||||||
let processedBodyHtml = bodyHtml;
|
let processedBodyHtml = bodyHtml;
|
||||||
let cidCounter = 1;
|
let cidCounter = 1;
|
||||||
processedBodyHtml = bodyHtml.replace(/src="data:([^;]+);base64,([^"]+)"/g, (match, contentType, base64Data) => {
|
processedBodyHtml = bodyHtml.replace(/src="data:([^;]+);base64,([^"]+)"/g, (match, contentType, base64Data) => {
|
||||||
const cid = `inline-image-${Date.now()}-${cidCounter++}`;
|
let ext = 'png'; // default fallback
|
||||||
|
if (contentType) {
|
||||||
|
const parts = contentType.split('/');
|
||||||
|
if (parts.length === 2) {
|
||||||
|
ext = parts[1];
|
||||||
|
if (ext === 'jpeg') ext = 'jpg';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cid = `inline-image-${Date.now()}-${cidCounter++}.${ext}`;
|
||||||
extractedInlineImages.push({
|
extractedInlineImages.push({
|
||||||
cid,
|
cid,
|
||||||
content: base64Data,
|
content: base64Data,
|
||||||
@@ -273,15 +288,17 @@ router.post('/send', async (req, res) => {
|
|||||||
|
|
||||||
const toList = to.join(', ');
|
const toList = to.join(', ');
|
||||||
|
|
||||||
|
const localNow = getLocalTimestamp();
|
||||||
|
|
||||||
// Insert article via DB metadata (Email channel=1, Visible to customer=1)
|
// Insert article via DB metadata (Email channel=1, Visible to customer=1)
|
||||||
const artInsert = await pool.query(`
|
const artInsert = await pool.query(`
|
||||||
INSERT INTO article (
|
INSERT INTO article (
|
||||||
ticket_id, article_sender_type_id, communication_channel_id,
|
ticket_id, article_sender_type_id, communication_channel_id,
|
||||||
is_visible_for_customer, create_time, create_by, change_time, change_by
|
is_visible_for_customer, create_time, create_by, change_time, change_by
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, 1, 1, 1, NOW(), $2, NOW(), $2
|
$1, 1, 1, 1, $3, $2, $3, $2
|
||||||
) RETURNING id`,
|
) RETURNING id`,
|
||||||
[ticketId, agentId || 1]
|
[ticketId, agentId || 1, localNow]
|
||||||
);
|
);
|
||||||
|
|
||||||
const articleId = artInsert.rows[0]?.id;
|
const articleId = artInsert.rows[0]?.id;
|
||||||
@@ -290,8 +307,8 @@ router.post('/send', async (req, res) => {
|
|||||||
// Write standard HTML MIME data
|
// Write standard HTML MIME data
|
||||||
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, NOW(), $10, NOW(), $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]
|
[articleId, aFrom, toList, cc.join(', '), bccList.join(', '), subject, processedBodyHtml, messageId, now, agentId || 1, localNow]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Helper to strip HTML tags
|
// Helper to strip HTML tags
|
||||||
@@ -314,8 +331,8 @@ router.post('/send', async (req, res) => {
|
|||||||
// 1. Write standard plain text version for client fallbacks
|
// 1. Write standard plain text version for client fallbacks
|
||||||
await pool.query(`
|
await pool.query(`
|
||||||
INSERT INTO article_data_mime_plain (article_id, body, create_time, create_by, change_time, change_by)
|
INSERT INTO article_data_mime_plain (article_id, body, create_time, create_by, change_time, change_by)
|
||||||
VALUES ($1, $2, NOW(), $3, NOW(), $3)`,
|
VALUES ($1, $2, $3, $4, $3, $4)`,
|
||||||
[articleId, plainBody, agentId || 1]
|
[articleId, plainBody, localNow, agentId || 1]
|
||||||
);
|
);
|
||||||
|
|
||||||
// 2. Populate OTRS fulltext search index (article_search_index)
|
// 2. Populate OTRS fulltext search index (article_search_index)
|
||||||
@@ -372,7 +389,7 @@ router.post('/send', async (req, res) => {
|
|||||||
article_id, filename, content_size, content_type,
|
article_id, filename, content_size, content_type,
|
||||||
content_id, disposition, content,
|
content_id, disposition, content,
|
||||||
create_time, create_by, change_time, change_by
|
create_time, create_by, change_time, change_by
|
||||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8, NOW(), $8)`,
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $9, $8, $9, $8)`,
|
||||||
[
|
[
|
||||||
articleId,
|
articleId,
|
||||||
att.filename,
|
att.filename,
|
||||||
@@ -381,7 +398,8 @@ router.post('/send', async (req, res) => {
|
|||||||
att.contentId,
|
att.contentId,
|
||||||
att.disposition,
|
att.disposition,
|
||||||
att.content, // base64 text directly
|
att.content, // base64 text directly
|
||||||
agentId || 1
|
agentId || 1,
|
||||||
|
localNow
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
} catch (attErr) {
|
} catch (attErr) {
|
||||||
|
|||||||
+25
-16
@@ -3,6 +3,13 @@ const router = express.Router();
|
|||||||
const pool = require('../db');
|
const pool = require('../db');
|
||||||
const { db, logAttivita } = require('../activityDb');
|
const { db, logAttivita } = require('../activityDb');
|
||||||
|
|
||||||
|
// Helper to get local timestamp in YYYY-MM-DD HH:mm:ss format
|
||||||
|
function getLocalTimestamp() {
|
||||||
|
const d = new Date();
|
||||||
|
const pad = (n) => String(n).padStart(2, '0');
|
||||||
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper: resolve agent name from DB (best-effort, non-blocking)
|
// Helper: resolve agent name from DB (best-effort, non-blocking)
|
||||||
async function resolveAgentName(agentId) {
|
async function resolveAgentName(agentId) {
|
||||||
try {
|
try {
|
||||||
@@ -427,7 +434,7 @@ router.get('/:id', async (req, res) => {
|
|||||||
|
|
||||||
// Fetch attachments metadata for all articles in the ticket
|
// Fetch attachments metadata for all articles in the ticket
|
||||||
const attachmentsResult = await pool.query(
|
const attachmentsResult = await pool.query(
|
||||||
`SELECT id, article_id, filename, content_size, content_type, disposition
|
`SELECT id, article_id, filename, content_size, content_type, disposition, content_id
|
||||||
FROM article_data_mime_attachment
|
FROM article_data_mime_attachment
|
||||||
WHERE article_id IN (
|
WHERE article_id IN (
|
||||||
SELECT id FROM article WHERE ticket_id = $1
|
SELECT id FROM article WHERE ticket_id = $1
|
||||||
@@ -1238,6 +1245,8 @@ router.post('/:id/articles', async (req, res) => {
|
|||||||
);
|
);
|
||||||
const channelId = channelResult.rows.length > 0 ? channelResult.rows[0].id : 1;
|
const channelId = channelResult.rows.length > 0 ? channelResult.rows[0].id : 1;
|
||||||
|
|
||||||
|
const localNow = getLocalTimestamp();
|
||||||
|
|
||||||
// Create article
|
// Create article
|
||||||
const articleResult = await client.query(
|
const articleResult = await client.query(
|
||||||
`INSERT INTO article (
|
`INSERT INTO article (
|
||||||
@@ -1245,9 +1254,9 @@ router.post('/:id/articles', async (req, res) => {
|
|||||||
is_visible_for_customer, search_index_needs_rebuild,
|
is_visible_for_customer, search_index_needs_rebuild,
|
||||||
create_time, create_by, change_time, change_by
|
create_time, create_by, change_time, change_by
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, 1, NOW(), $5, NOW(), $5
|
$1, $2, $3, $4, 1, $5, $6, $5, $6
|
||||||
) RETURNING id`,
|
) RETURNING id`,
|
||||||
[id, senderTypeId, channelId, is_visible_for_customer ? 1 : 0, operatorId]
|
[id, senderTypeId, channelId, is_visible_for_customer ? 1 : 0, localNow, operatorId]
|
||||||
);
|
);
|
||||||
|
|
||||||
const articleId = articleResult.rows[0].id;
|
const articleId = articleResult.rows[0].id;
|
||||||
@@ -1269,9 +1278,9 @@ router.post('/:id/articles', async (req, res) => {
|
|||||||
$1, $2, '', '', '', '', $3, $4,
|
$1, $2, '', '', '', '', $3, $4,
|
||||||
'', '', '',
|
'', '', '',
|
||||||
$5, EXTRACT(EPOCH FROM NOW())::INTEGER, $6,
|
$5, EXTRACT(EPOCH FROM NOW())::INTEGER, $6,
|
||||||
NOW(), $7, NOW(), $7
|
$7, $8, $7, $8
|
||||||
)`,
|
)`,
|
||||||
[articleId, fromHeader, subject || 'Nota interna', body, contentType, contentPath, operatorId]
|
[articleId, fromHeader, subject || 'Nota interna', body, contentType, contentPath, localNow, operatorId]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create article_data_mime_attachment for OTRS CE HTML rendering (using file-1 and base64 encoded text)
|
// Create article_data_mime_attachment for OTRS CE HTML rendering (using file-1 and base64 encoded text)
|
||||||
@@ -1286,9 +1295,9 @@ router.post('/:id/articles', async (req, res) => {
|
|||||||
create_time, create_by, change_time, change_by
|
create_time, create_by, change_time, change_by
|
||||||
) VALUES (
|
) VALUES (
|
||||||
$1, 'file-1', $2, 'text/html; charset="utf-8"', '', $3,
|
$1, 'file-1', $2, 'text/html; charset="utf-8"', '', $3,
|
||||||
NOW(), $4, NOW(), $4
|
$4, $5, $4, $5
|
||||||
)`,
|
)`,
|
||||||
[articleId, String(contentSize), base64Body, operatorId]
|
[articleId, String(contentSize), base64Body, localNow, operatorId]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Insert additional attachments if any
|
// Insert additional attachments if any
|
||||||
@@ -1299,14 +1308,15 @@ router.post('/:id/articles', async (req, res) => {
|
|||||||
`INSERT INTO article_data_mime_attachment (
|
`INSERT INTO article_data_mime_attachment (
|
||||||
article_id, filename, content_size, content_type, disposition, content,
|
article_id, filename, content_size, content_type, disposition, content,
|
||||||
create_time, create_by, change_time, change_by
|
create_time, create_by, change_time, change_by
|
||||||
) VALUES ($1, $2, $3, $4, 'attachment', $5, NOW(), $6, NOW(), $6)`,
|
) VALUES ($1, $2, $3, $4, 'attachment', $5, $7, $6, $7, $6)`,
|
||||||
[
|
[
|
||||||
articleId,
|
articleId,
|
||||||
att.filename,
|
att.filename,
|
||||||
contentBuffer.length,
|
contentBuffer.length,
|
||||||
att.content_type || 'application/octet-stream',
|
att.content_type || 'application/octet-stream',
|
||||||
att.content, // base64 string directly
|
att.content, // base64 string directly
|
||||||
operatorId
|
operatorId,
|
||||||
|
localNow
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1320,16 +1330,15 @@ router.post('/:id/articles', async (req, res) => {
|
|||||||
`INSERT INTO time_accounting (
|
`INSERT INTO time_accounting (
|
||||||
ticket_id, article_id, time_unit,
|
ticket_id, article_id, time_unit,
|
||||||
create_time, create_by, change_time, change_by
|
create_time, create_by, change_time, change_by
|
||||||
) VALUES ($1, $2, $3, NOW(), $4, NOW(), $4)`,
|
) VALUES ($1, $2, $3, $5, $4, $5, $4)`,
|
||||||
[id, articleId, parsedTime, operatorId]
|
[id, articleId, parsedTime, operatorId, localNow]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update ticket change_time
|
|
||||||
await client.query(
|
await client.query(
|
||||||
`UPDATE ticket SET change_time = NOW(), change_by = $1 WHERE id = $2`,
|
`UPDATE ticket SET change_time = $1, change_by = $2 WHERE id = $3`,
|
||||||
[operatorId, id]
|
[localNow, operatorId, id]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add history entry
|
// Add history entry
|
||||||
@@ -1351,13 +1360,13 @@ router.post('/:id/articles', async (req, res) => {
|
|||||||
) VALUES (
|
) VALUES (
|
||||||
$1, $2, $3, $4, $5, $6,
|
$1, $2, $3, $4, $5, $6,
|
||||||
$7, $8, $9,
|
$7, $8, $9,
|
||||||
NOW(), $10, NOW(), $10
|
$11, $10, $11, $10
|
||||||
)`,
|
)`,
|
||||||
[
|
[
|
||||||
`%%`,
|
`%%`,
|
||||||
htResult.rows[0].id, id, articleId, t.type_id || 1, t.queue_id,
|
htResult.rows[0].id, id, articleId, t.type_id || 1, t.queue_id,
|
||||||
t.user_id, t.ticket_priority_id, t.ticket_state_id,
|
t.user_id, t.ticket_priority_id, t.ticket_state_id,
|
||||||
operatorId
|
operatorId, localNow
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user