feat: aggiunta possibilità di salvare delle note interne da caricare successivamente su OTRS.

This commit is contained in:
Gabriele Cimaschi committed 2026-08-26 21:32:41 +02:00
1 parent 146d4ed661
commit 3f3885130a
6 files changed
+837

No files matched your search

+221
View File
@@ -0,0 +1,221 @@
const express = require('express');
const router = express.Router();
const { db, logAttivita } = require('../activityDb');
const pool = require('../db');
// Helper to resolve ticket info from PostgreSQL given ID or TN
async function resolveTicketInfo(ticketId, ticketTn) {
try {
if (ticketId) {
const res = await pool.query('SELECT id, tn, title FROM ticket WHERE id = $1', [ticketId]);
if (res.rows.length > 0) return res.rows[0];
} else if (ticketTn) {
const res = await pool.query('SELECT id, tn, title FROM ticket WHERE tn = $1', [ticketTn.trim()]);
if (res.rows.length > 0) return res.rows[0];
}
} catch (err) {
console.warn('[Notes API] Error resolving ticket info:', err.message);
}
return null;
}
// GET /api/notes - List all draft notes for current agent
router.get('/', (req, res) => {
try {
const agentId = parseInt(req.headers['x-agent-id'] || '1', 10);
const notes = db.prepare(`
SELECT * FROM ticket_draft_notes
WHERE agent_id = ?
ORDER BY updated_at DESC
`).all(agentId);
res.json(notes);
} catch (err) {
console.error('[Notes API] GET / failed:', err);
res.status(500).json({ error: 'Errore nel caricamento delle note temporanee', message: err.message });
}
});
// GET /api/notes/:id - Get single draft note
router.get('/:id', (req, res) => {
try {
const note = db.prepare('SELECT * FROM ticket_draft_notes WHERE id = ?').get(req.params.id);
if (!note) {
return res.status(404).json({ error: 'Nota temporanea non trovata' });
}
res.json(note);
} catch (err) {
res.status(500).json({ error: 'Errore nel recupero della nota', message: err.message });
}
});
// POST /api/notes - Create or update a temporary draft note
router.post('/', async (req, res) => {
try {
const agentId = parseInt(req.headers['x-agent-id'] || '1', 10);
let { id, ticket_id, ticket_tn, ticket_title, subject, body, time_units, note_type } = req.body;
if (!body || body.trim() === '') {
return res.status(400).json({ error: 'Il corpo della nota non può essere vuoto' });
}
// Resolve ticket info if missing TN or ID
const resolved = await resolveTicketInfo(ticket_id, ticket_tn);
if (resolved) {
ticket_id = resolved.id;
ticket_tn = resolved.tn;
ticket_title = resolved.title;
}
const now = new Date().toISOString();
if (id) {
// Update existing note
db.prepare(`
UPDATE ticket_draft_notes
SET ticket_id = ?, ticket_tn = ?, ticket_title = ?, subject = ?, body = ?, time_units = ?, note_type = ?, updated_at = ?
WHERE id = ? AND agent_id = ?
`).run(ticket_id || null, ticket_tn || null, ticket_title || null, subject || '', body, parseFloat(time_units) || 0, note_type || 'note-internal', now, id, agentId);
const updated = db.prepare('SELECT * FROM ticket_draft_notes WHERE id = ?').get(id);
return res.json(updated);
} else {
// Insert new note
const info = db.prepare(`
INSERT INTO ticket_draft_notes (agent_id, ticket_id, ticket_tn, ticket_title, subject, body, time_units, note_type, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(agentId, ticket_id || null, ticket_tn || null, ticket_title || null, subject || '', body, parseFloat(time_units) || 0, note_type || 'note-internal', now, now);
const inserted = db.prepare('SELECT * FROM ticket_draft_notes WHERE id = ?').get(info.lastInsertRowid);
return res.status(201).json(inserted);
}
} catch (err) {
console.error('[Notes API] POST / failed:', err);
res.status(500).json({ error: 'Errore nel salvataggio della nota temporanea', message: err.message });
}
});
// DELETE /api/notes/:id - Delete a draft note permanently
router.delete('/:id', (req, res) => {
try {
const info = db.prepare('DELETE FROM ticket_draft_notes WHERE id = ?').run(req.params.id);
if (info.changes === 0) {
return res.status(404).json({ error: 'Nota temporanea non trovata' });
}
res.json({ success: true, id: req.params.id });
} catch (err) {
res.status(500).json({ error: 'Errore nell\'eliminazione della nota', message: err.message });
}
});
// DELETE /api/notes/by-ticket/:identifier - Delete notes tied to ticket ID or TN
router.delete('/by-ticket/:identifier', async (req, res) => {
try {
const identifier = req.params.identifier;
const resolved = await resolveTicketInfo(parseInt(identifier, 10) || null, identifier);
if (resolved) {
db.prepare('DELETE FROM ticket_draft_notes WHERE ticket_id = ? OR ticket_tn = ?').run(resolved.id, resolved.tn);
} else {
db.prepare('DELETE FROM ticket_draft_notes WHERE ticket_id = ? OR ticket_tn = ?').run(identifier, identifier);
}
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: 'Errore nel ripristino/pulizia della nota', message: err.message });
}
});
// POST /api/notes/:id/submit - Submit note directly to OTRS and remove from SQLite upon success
router.post('/:id/submit', async (req, res) => {
try {
const agentId = parseInt(req.headers['x-agent-id'] || '1', 10);
const note = db.prepare('SELECT * FROM ticket_draft_notes WHERE id = ? AND agent_id = ?').get(req.params.id, agentId);
if (!note) {
return res.status(404).json({ error: 'Nota temporanea non trovata' });
}
if (!note.ticket_id && !note.ticket_tn) {
return res.status(400).json({ error: 'Impossibile inviare a OTRS: nessun ticket (TN/ID) associato a questa nota' });
}
const resolved = await resolveTicketInfo(note.ticket_id, note.ticket_tn);
if (!resolved) {
return res.status(400).json({ error: `Ticket non trovato nel sistema OTRS (TN: ${note.ticket_tn || ''}, ID: ${note.ticket_id || ''})` });
}
// Get agent login user
let userLogin = process.env.OTRS_API_USER || 'root@localhost';
try {
const prefRes = await pool.query(
`SELECT preferences_value FROM user_preferences WHERE user_id = $1 AND preferences_key = 'UserEmail'`,
[agentId]
);
if (prefRes.rows.length > 0 && prefRes.rows[0].preferences_value) {
userLogin = prefRes.rows[0].preferences_value;
}
} catch (_) {}
// Prepare Article for OTRS REST API
const isInternal = note.note_type === 'note-internal';
const isEmail = note.note_type === 'email-external';
let articleType = isInternal ? 'note-internal' : (isEmail ? 'email-external' : 'note-external');
let senderType = isInternal ? 'agent' : (isEmail ? 'agent' : 'agent');
const articlePayload = {
ArticleType: articleType,
SenderType: senderType,
Subject: note.subject || `Nota per ticket ${resolved.tn}`,
Body: note.body,
ContentType: 'text/html; charset=UTF-8',
TimeUnit: parseFloat(note.time_units) || 0
};
const otrsUrl = process.env.OTRS_API_URL || 'http://localhost/otrs/nph-genericinterface.pl/Webservice/GenericTicketConnectorREST';
const otrsPassword = process.env.OTRS_API_PASSWORD || '';
const payload = {
UserLogin: userLogin,
Password: otrsPassword,
TicketID: resolved.id,
Article: articlePayload
};
const otrsRes = await fetch(`${otrsUrl}/TicketUpdate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const otrsData = await otrsRes.json();
if (otrsData && otrsData.Error) {
throw new Error(otrsData.Error.ErrorMessage || 'Errore durante l\'invio a OTRS');
}
// Log action
logAttivita(
agentId,
userLogin,
`Aggiunta nota a Ticket #${resolved.tn}`,
{ ticket_id: resolved.id, ticket_tn: resolved.tn, subject: note.subject, time_units: note.time_units },
'successo'
);
// Delete note from local SQLite DB upon successful OTRS upload
db.prepare('DELETE FROM ticket_draft_notes WHERE id = ?').run(note.id);
res.json({
success: true,
message: `Nota inviata con successo a OTRS per il ticket #${resolved.tn} e rimossa dal database locale.`,
ticket_id: resolved.id,
ticket_tn: resolved.tn
});
} catch (err) {
console.error('[Notes API] POST /:id/submit failed:', err);
res.status(500).json({ error: 'Errore durante l\'invio della nota a OTRS', message: err.message });
}
});
module.exports = router;