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:
1 parent
9ae0cec0ca
commit
cf500aabb3
7 files changed
+743
-85
No files matched your search
+138
-6
@@ -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`;
|
||||
|
||||
Reference in new issue
Block a user