fix: correzione doppia consuntivazione note. fix: utenti ldap. feat: ricerca per customer useers. feat: possiblità di rimuovere le note

This commit is contained in:
Gabriele Cimaschi
2026-07-08 12:52:30 +02:00
parent 3b06609fcb
commit 9a91c3d9f5
7 changed files with 661 additions and 72 deletions
+210 -42
View File
@@ -19,6 +19,24 @@ async function resolveAgentName(agentId) {
}
// Helper: resolve agent details for a_from header (best-effort, fallback to 'OTRS Turbo Agent')
async function resolveAgentFromHeader(agentId, client = pool) {
try {
const r = await client.query(
`SELECT first_name, last_name, email, login FROM users WHERE id = $1`,
[agentId]
);
if (r.rows.length > 0) {
const u = r.rows[0];
const fullName = [u.first_name, u.last_name].filter(Boolean).join(' ') || u.login || 'Agent';
const email = u.email || 'agent@localhost';
return `"${fullName}" <${email}>`;
}
} catch (_) { /* ignore */ }
return 'OTRS Turbo Agent';
}
// Helper for OTRS CE GenericInterface REST API calls
async function otrsRequest(method, path, bodyData = {}) {
const OTRS_API_USER = process.env.OTRS_API_USER;
@@ -72,7 +90,7 @@ async function otrsRequest(method, path, bodyData = {}) {
router.get('/', async (req, res) => {
try {
const {
queue_id, state_id, priority_id, user_id, type_id,
queue_id, state_id, priority_id, user_id, type_id, customer_user_id,
search, sort_by = 'create_time', sort_dir = 'DESC',
page = 1, per_page = 50,
date_from, date_to
@@ -82,6 +100,23 @@ router.get('/', async (req, res) => {
const params = [];
let paramIdx = 1;
if (customer_user_id) {
let logins = [];
if (Array.isArray(customer_user_id)) {
logins = customer_user_id.map(l => String(l).trim()).filter(Boolean);
} else if (typeof customer_user_id === 'string') {
logins = customer_user_id.split(',').map(l => l.trim()).filter(Boolean);
} else {
logins = [String(customer_user_id).trim()];
}
if (logins.length > 0) {
const placeholders = logins.map(() => `$${paramIdx++}`).join(', ');
conditions.push(`t.customer_user_id IN (${placeholders})`);
params.push(...logins);
}
}
if (queue_id) {
conditions.push(`t.queue_id = $${paramIdx++}`);
params.push(parseInt(queue_id));
@@ -336,7 +371,11 @@ router.get('/:id', async (req, res) => {
LEFT JOIN communication_channel cc ON a.communication_channel_id = cc.id
LEFT JOIN article_data_mime adm ON a.id = adm.article_id
LEFT JOIN users creator ON a.create_by = creator.id
LEFT JOIN time_accounting ta ON a.id = ta.article_id
LEFT JOIN (
SELECT article_id, SUM(time_unit) AS time_unit
FROM time_accounting
GROUP BY article_id
) ta ON a.id = ta.article_id
WHERE a.ticket_id = $1
ORDER BY a.create_time DESC, a.id DESC`,
[id]
@@ -725,22 +764,6 @@ router.patch('/:id', async (req, res) => {
}
const result = await otrsRequest('PATCH', `/Ticket/${id}`, reqBody);
// Option 1: Log the time directly to the DB if the API succeeded but OTRS didn't save it
if (!isNaN(timeUnit) && timeUnit > 0 && result && result.ArticleID) {
try {
await pool.query(
`INSERT INTO time_accounting (
ticket_id, article_id, time_unit,
create_time, create_by, change_time, change_by
) VALUES ($1, $2, $3, NOW(), $4, NOW(), $4)`,
[id, result.ArticleID, timeUnit, operatorId]
);
console.log(`[Time Accounting] Successfully logged ${timeUnit} minutes for ticket ${id} via DB insert.`);
} catch (timeDbErr) {
console.error('[Time Accounting] Failed to log time unit in database:', timeDbErr.message);
}
}
const isClosing = updates.ticket_state_id && current.ticket_state_id !== updates.ticket_state_id;
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({
@@ -911,6 +934,8 @@ router.patch('/:id', async (req, res) => {
const contentSize = Buffer.byteLength(htmlBody, 'utf-8');
const base64Body = binaryBody.toString('base64');
const fromHeader = await resolveAgentFromHeader(operatorId, client);
// Create article_data_mime
await client.query(
`INSERT INTO article_data_mime (
@@ -924,7 +949,7 @@ router.patch('/:id', async (req, res) => {
'text/html; charset=utf-8', EXTRACT(EPOCH FROM NOW())::INTEGER, $5,
NOW(), $6, NOW(), $6
)`,
[articleId, 'OTRS Turbo Agent', 'Consuntivazione', htmlBody, contentPath, operatorId]
[articleId, fromHeader, 'Consuntivazione', htmlBody, contentPath, operatorId]
);
// Create article_data_mime_attachment for OTRS CE HTML rendering
@@ -995,7 +1020,11 @@ router.get('/:id/articles', async (req, res) => {
LEFT JOIN communication_channel cc ON a.communication_channel_id = cc.id
LEFT JOIN article_data_mime adm ON a.id = adm.article_id
LEFT JOIN users creator ON a.create_by = creator.id
LEFT JOIN time_accounting ta ON a.id = ta.article_id
LEFT JOIN (
SELECT article_id, SUM(time_unit) AS time_unit
FROM time_accounting
GROUP BY article_id
) ta ON a.id = ta.article_id
WHERE a.ticket_id = $1
ORDER BY a.create_time DESC, a.id DESC`,
[id]
@@ -1047,21 +1076,7 @@ router.post('/:id/articles', async (req, res) => {
const result = await otrsRequest('PATCH', `/Ticket/${id}`, payload);
// Option 1: Log the time directly to the DB if the API succeeded but OTRS didn't save it
if (time_unit && result && result.ArticleID) {
try {
await pool.query(
`INSERT INTO time_accounting (
ticket_id, article_id, time_unit,
create_time, create_by, change_time, change_by
) VALUES ($1, $2, $3, NOW(), $4, NOW(), $4)`,
[id, result.ArticleID, parseFloat(time_unit), operatorId]
);
console.log(`[Time Accounting] Successfully logged ${time_unit} minutes for ticket ${id} via DB insert.`);
} catch (timeDbErr) {
console.error('[Time Accounting] Failed to log time unit in database:', timeDbErr.message);
}
}
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({
@@ -1072,14 +1087,24 @@ router.post('/:id/articles', async (req, res) => {
esito: 'successo',
});
});
return res.status(201).json({
message: 'Nota aggiunta! (via API REST)',
article_id: result.ArticleID,
result
});
} catch (restErr) {
console.warn('Failed to add article via REST API, falling back to database insert:', restErr.message);
// Fall through to standard direct database update below
console.error('Failed to add article via REST API:', restErr.message);
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({
agente_id: operatorId,
agente_nome,
titolo_azione: 'Aggiunta Nota',
azione: { ticket_id: id, error: restErr.message },
esito: 'errore'
});
});
return res.status(500).json({ error: 'Errore durante l\'aggiunta della nota tramite API: ' + restErr.message });
}
}
@@ -1090,6 +1115,62 @@ router.post('/:id/articles', async (req, res) => {
const { subject, body, is_visible_for_customer = 0, time_unit, attachments } = 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)
const thirtySecondsAgo = new Date(Date.now() - 30000);
const recentCheck = await client.query(
`SELECT a.id
FROM article a
JOIN article_data_mime adm ON a.id = adm.article_id
WHERE a.ticket_id = $1
AND adm.a_subject = $2
AND adm.a_body = $3
AND a.create_time >= $4`,
[id, subject || 'Nota interna', body, thirtySecondsAgo]
);
if (recentCheck.rows.length > 0) {
const existingArticleId = recentCheck.rows[0].id;
console.log(`[Fallback Check] Found existing recent article (ID: ${existingArticleId}) in database. Skipping duplicate insert.`);
await client.query('BEGIN');
// If time_unit is provided, insert into time_accounting if not already present
if (time_unit !== undefined && time_unit !== null && time_unit !== '') {
const parsedTime = parseFloat(time_unit);
if (!isNaN(parsedTime) && parsedTime > 0) {
const timeCheck = await client.query(
`SELECT id FROM time_accounting WHERE ticket_id = $1 AND article_id = $2`,
[id, existingArticleId]
);
if (timeCheck.rows.length === 0) {
await client.query(
`INSERT INTO time_accounting (
ticket_id, article_id, time_unit,
create_time, create_by, change_time, change_by
) VALUES ($1, $2, $3, NOW(), $4, NOW(), $4)`,
[id, existingArticleId, parsedTime, operatorId]
);
console.log(`[Fallback Check] Logged missing time accounting (${parsedTime} min) for existing article.`);
}
}
}
await client.query('COMMIT');
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({
agente_id: operatorId,
agente_nome,
titolo_azione: 'Aggiunta Nota',
azione: { ticket_id: id, subject, time_unit, has_attachments: !!(attachments && attachments.length) },
esito: 'successo',
});
});
return res.status(201).json({
article_id: existingArticleId,
message: 'Nota aggiunta! (rilevata in DB)',
});
}
await client.query('BEGIN');
// Verify ticket exists
@@ -1129,6 +1210,8 @@ router.post('/:id/articles', async (req, res) => {
const now = new Date();
const contentPath = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')}`;
const fromHeader = await resolveAgentFromHeader(operatorId, client);
// Create article_data_mime
await client.query(
`INSERT INTO article_data_mime (
@@ -1142,7 +1225,7 @@ router.post('/:id/articles', async (req, res) => {
$5, EXTRACT(EPOCH FROM NOW())::INTEGER, $6,
NOW(), $7, NOW(), $7
)`,
[articleId, 'OTRS Turbo Agent', subject || 'Nota interna', body, contentType, contentPath, operatorId]
[articleId, fromHeader, subject || 'Nota interna', body, contentType, contentPath, operatorId]
);
// Create article_data_mime_attachment for OTRS CE HTML rendering (using file-1 and base64 encoded text)
@@ -1433,6 +1516,90 @@ router.post('/articles/:articleId/retrodata-article', async (req, res) => {
}
});
// DELETE /api/tickets/articles/:articleId — Delete an article/note
router.delete('/articles/:articleId', async (req, res) => {
const { articleId } = req.params;
const operatorId = parseInt(req.headers['x-agent-id'], 10) || 1;
const client = await pool.connect();
try {
await client.query('BEGIN');
// 1. Set article_id in ticket_history to NULL to avoid constraint violation
await client.query(
`UPDATE ticket_history SET article_id = NULL WHERE article_id = $1`,
[articleId]
);
// Delete flags from article_flag
await client.query(
`DELETE FROM article_flag WHERE article_id = $1`,
[articleId]
);
// Delete search index entries from article_search_index
await client.query(
`DELETE FROM article_search_index WHERE article_id = $1`,
[articleId]
);
// 2. Delete time accounting entries
await client.query(
`DELETE FROM time_accounting WHERE article_id = $1`,
[articleId]
);
// 3. Delete attachments
await client.query(
`DELETE FROM article_data_mime_attachment WHERE article_id = $1`,
[articleId]
);
// 4. Delete mime data
await client.query(
`DELETE FROM article_data_mime WHERE article_id = $1`,
[articleId]
);
// 5. Delete article itself
const deleteRes = await client.query(
`DELETE FROM article WHERE id = $1 RETURNING ticket_id`,
[articleId]
);
const ticketId = deleteRes.rows.length > 0 ? deleteRes.rows[0].ticket_id : null;
await client.query('COMMIT');
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({
agente_id: operatorId,
agente_nome,
titolo_azione: 'Eliminazione Articolo',
azione: { article_id: articleId, ticket_id: ticketId },
esito: 'successo',
});
});
res.json({ message: 'Articolo eliminato con successo!' });
} catch (err) {
await client.query('ROLLBACK');
console.error('Error deleting article:', err);
resolveAgentName(operatorId).then(agente_nome => {
logAttivita({
agente_id: operatorId,
agente_nome,
titolo_azione: 'Eliminazione Articolo',
azione: { article_id: articleId, error: err.message },
esito: 'errore',
});
});
res.status(500).json({ error: err.message });
} finally {
client.release();
}
});
// GET /api/tickets/attachments/:id — Download/View attachment
router.get('/attachments/:id', async (req, res) => {
try {
@@ -1902,6 +2069,7 @@ router.post('/auto-time', async (req, res) => {
const binaryBody = Buffer.from(htmlBody, 'utf-8');
const contentSize = Buffer.byteLength(htmlBody, 'utf-8');
const base64Body = binaryBody.toString('base64');
const fromHeader = await resolveAgentFromHeader(operatorId, client);
await client.query(
`INSERT INTO article_data_mime (
@@ -1909,11 +2077,11 @@ router.post('/auto-time', async (req, res) => {
a_content_type, incoming_time, content_path,
create_time, create_by, change_time, change_by
) VALUES (
$1, 'OTRS Turbo Agent', '', $2, $3,
'text/html; charset=utf-8', EXTRACT(EPOCH FROM NOW())::INTEGER, $4,
NOW(), $5, NOW(), $5
$1, $2, '', $3, $4,
'text/html; charset=utf-8', EXTRACT(EPOCH FROM NOW())::INTEGER, $5,
NOW(), $6, NOW(), $6
)`,
[articleId, subject, htmlBody, contentPath, operatorId]
[articleId, fromHeader, subject, htmlBody, contentPath, operatorId]
);
// Create article attachment (file-1) for OTRS CE HTML display