tutto di troppo: fix visualizzazione email. style: riviste icone invio ed elimina, postato aggiungi ticket al grppo e informazioni laterali da a cc.

This commit is contained in:
Gabriele Cimaschi committed 2026-09-09 11:04:14 +02:00
1 parent 9ae0cec0ca
commit cf500aabb3
7 files changed
+743 -85

No files matched your search

+138 -6
View File
@@ -443,6 +443,62 @@ router.get('/:id', async (req, res) => {
[id]
);
// Fetch HTML body attachment content for articles if present (e.g. file-1, file-1.html, text/html)
const htmlAttachmentsRes = await pool.query(
`SELECT id, article_id, filename, content_type, content
FROM article_data_mime_attachment
WHERE article_id IN (SELECT id FROM article WHERE ticket_id = $1)
AND (
filename LIKE 'file-%'
OR content_type LIKE '%html%'
OR disposition = 'inline'
)
ORDER BY id ASC`,
[id]
);
const htmlBodyMap = {};
for (const row of htmlAttachmentsRes.rows) {
if (row.content) {
let contentStr = '';
if (Buffer.isBuffer(row.content)) {
contentStr = row.content.toString('utf-8');
} else if (typeof row.content === 'string') {
contentStr = row.content;
}
const cleaned = contentStr.replace(/\s+/g, '');
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
if (cleaned.length > 20 && cleaned.length % 4 === 0 && base64Regex.test(cleaned)) {
try {
const decoded = Buffer.from(cleaned, 'base64').toString('utf-8');
if (decoded && /<(div|p|br|table|html|body|span|img|a|style)\b/i.test(decoded)) {
contentStr = decoded;
}
} catch (e) {}
}
if (contentStr && /<(div|p|br|table|html|body|span|img|a|style)\b/i.test(contentStr)) {
const fname = (row.filename || '').toLowerCase();
if (!htmlBodyMap[row.article_id] || fname.startsWith('file-1') || fname.startsWith('file-2')) {
htmlBodyMap[row.article_id] = contentStr;
}
}
}
}
articlesResult.rows.forEach(a => {
if (htmlBodyMap[a.article_id]) {
a.html_body = htmlBodyMap[a.article_id];
a.has_html = true;
} else if (a.a_body && ((a.a_content_type || '').toLowerCase().includes('html') || /<(div|p|br|table|html|body|span|img|a|style)\b/i.test(a.a_body))) {
a.html_body = a.a_body;
a.has_html = true;
} else {
a.has_html = false;
}
});
let otrsWebUrl = null;
const apiUrl = process.env.OTRS_API_URL;
if (apiUrl) {
@@ -1111,7 +1167,7 @@ router.get('/:id/articles', async (req, res) => {
// ============================================================
router.post('/:id/articles', async (req, res) => {
const { id } = req.params;
const { subject, body, is_visible_for_customer = 0, time_unit, attachments } = req.body;
const { subject, body, is_visible_for_customer = 0, time_unit, attachments, ticket_state_id } = req.body;
const operatorId = parseInt(req.headers['x-agent-id'], 10) || 1;
const isHtml = body && /<[a-z][\s\S]*>/i.test(body);
@@ -1131,6 +1187,27 @@ router.post('/:id/articles', async (req, res) => {
}
};
if (ticket_state_id) {
payload.Ticket = { StateID: parseInt(ticket_state_id, 10) };
try {
const stateTypeRes = await pool.query(
`SELECT tst.name AS type_name
FROM ticket_state ts
JOIN ticket_state_type tst ON ts.type_id = tst.id
WHERE ts.id = $1`,
[ticket_state_id]
);
if (stateTypeRes.rows.length > 0) {
const typeName = stateTypeRes.rows[0].type_name.toLowerCase();
if (typeName.includes('closed') || typeName === 'closed successful' || typeName === 'closed unsuccessful') {
payload.Ticket.LockID = 1;
}
}
} catch (err) {
console.error('Error checking state type for REST API payload:', err);
}
}
if (time_unit) {
payload.Article.TimeUnit = parseFloat(time_unit);
payload.Article.TimeUnits = parseFloat(time_unit);
@@ -1182,7 +1259,7 @@ router.post('/:id/articles', async (req, res) => {
const client = await pool.connect();
try {
const { id } = req.params;
const { subject, body, is_visible_for_customer = 0, time_unit, attachments } = req.body;
const { subject, body, is_visible_for_customer = 0, time_unit, attachments, ticket_state_id } = req.body;
const operatorId = parseInt(req.headers['x-agent-id'], 10) || 1;
// Check if this article was already inserted (e.g., via REST API or concurrent request)
@@ -1243,12 +1320,67 @@ router.post('/:id/articles', async (req, res) => {
await client.query('BEGIN');
// Verify ticket exists
const ticketCheck = await client.query('SELECT id FROM ticket WHERE id = $1', [id]);
// Verify ticket exists & get current state
const ticketCheck = await client.query(
'SELECT id, ticket_state_id, ticket_priority_id, queue_id, user_id, type_id FROM ticket WHERE id = $1',
[id]
);
if (ticketCheck.rows.length === 0) {
await client.query('ROLLBACK');
return res.status(404).json({ error: 'Ticket not found' });
}
const currentTicket = ticketCheck.rows[0];
// Check if ticket state should be updated
if (ticket_state_id && parseInt(ticket_state_id, 10) !== currentTicket.ticket_state_id) {
const newStateId = parseInt(ticket_state_id, 10);
const stateTypeRes = await client.query(
`SELECT tst.name AS type_name
FROM ticket_state ts
JOIN ticket_state_type tst ON ts.type_id = tst.id
WHERE ts.id = $1`,
[newStateId]
);
let lockIdToSet = null;
if (stateTypeRes.rows.length > 0) {
const typeName = stateTypeRes.rows[0].type_name.toLowerCase();
if (typeName.includes('closed') || typeName === 'closed successful' || typeName === 'closed unsuccessful') {
lockIdToSet = 1;
}
}
if (lockIdToSet !== null) {
await client.query(
`UPDATE ticket SET ticket_state_id = $1, ticket_lock_id = $2, change_time = $3, change_by = $4 WHERE id = $5`,
[newStateId, lockIdToSet, getLocalTimestamp(), operatorId, id]
);
} else {
await client.query(
`UPDATE ticket SET ticket_state_id = $1, change_time = $2, change_by = $3 WHERE id = $4`,
[newStateId, getLocalTimestamp(), operatorId, id]
);
}
const htResult = await client.query(
`SELECT id FROM ticket_history_type WHERE name = 'StateUpdate'`
);
if (htResult.rows.length > 0) {
await client.query(
`INSERT INTO ticket_history (
name, history_type_id, ticket_id, type_id, queue_id,
owner_id, priority_id, state_id, create_time, create_by, change_time, change_by
) VALUES (
'%%', $1, $2, $3, $4,
$5, $6, $7, $8, $9, $8, $9
)`,
[
htResult.rows[0].id, id, currentTicket.type_id || 1, currentTicket.queue_id,
currentTicket.user_id, currentTicket.ticket_priority_id, newStateId,
getLocalTimestamp(), operatorId
]
);
}
}
// Get sender type for "agent"
const senderResult = await client.query(
@@ -1946,9 +2078,9 @@ router.get('/time-accounting/summary', async (req, res) => {
const targetDate = new Date(`${dateStr}T00:00:00Z`);
const startDateObj = new Date(targetDate);
startDateObj.setDate(startDateObj.getDate() - 7);
startDateObj.setDate(startDateObj.getDate() - 14);
const endDateObj = new Date(targetDate);
endDateObj.setDate(endDateObj.getDate() + 7);
endDateObj.setDate(endDateObj.getDate() + 14);
const startTime = `${startDateObj.toISOString().slice(0, 10)} 00:00:00`;
const endTime = `${endDateObj.toISOString().slice(0, 10)} 23:59:59`;