Files
otrs-turbo/routes/notes.js
T

296 lines
11 KiB
JavaScript

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' });
}
let resolved = null;
if (note.ticket_id || note.ticket_tn) {
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 (_) {}
// If no ticket is associated, create a new ticket from this note
if (!resolved) {
// Default queue (first queue or 1)
let queueId = 1;
const qRes = await pool.query('SELECT id FROM queue ORDER BY id ASC LIMIT 1');
if (qRes.rows.length > 0) queueId = qRes.rows[0].id;
// Default state ('new' / 1)
let stateId = 1;
const sRes = await pool.query(`SELECT ts.id FROM ticket_state ts JOIN ticket_state_type tst ON ts.type_id = tst.id WHERE tst.name = 'new' LIMIT 1`);
if (sRes.rows.length > 0) stateId = sRes.rows[0].id;
const ticketTitle = note.subject || 'Nuova Nota Temporanea';
// Generate TN
const now = new Date();
const systemId = process.env.OTRS_SYSTEM_ID || String(agentId);
const datePrefix = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${systemId}`;
const maxTicketResult = await pool.query(
`SELECT tn FROM ticket WHERE tn LIKE $1`,
[`${datePrefix}%`]
);
let maxCounter = 0;
for (const row of maxTicketResult.rows) {
const counterPart = row.tn.substring(datePrefix.length);
const parsed = parseInt(counterPart, 10);
if (!isNaN(parsed) && parsed > maxCounter) {
maxCounter = parsed;
}
}
const counterPadding = parseInt(process.env.OTRS_COUNTER_PADDING, 10) || 6;
const newTn = `${datePrefix}${String(maxCounter + 1).padStart(counterPadding, '0')}`;
// Insert new ticket into DB
const client = await pool.connect();
try {
await client.query('BEGIN');
const nowTs = new Date().toISOString().replace('T', ' ').substring(0, 19);
const newTicketRes = await client.query(
`INSERT INTO ticket (
tn, title, queue_id, ticket_lock_id, type_id,
user_id, responsible_user_id, ticket_priority_id, ticket_state_id,
create_time, create_by, change_time, change_by
) VALUES (
$1, $2, $3, 1, 1,
$4, $4, 3, $5,
$6, $4, $6, $4
) RETURNING id, tn`,
[newTn, ticketTitle, queueId, agentId, stateId, nowTs]
);
const newTicketId = newTicketRes.rows[0].id;
resolved = { id: newTicketId, tn: newTn, title: ticketTitle };
// Insert NewTicket history
const htRes = await client.query(`SELECT id FROM ticket_history_type WHERE name = 'NewTicket'`);
const htId = htRes.rows.length > 0 ? htRes.rows[0].id : 1;
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, 1, $3, $4, 3, $5, $6, $4, $6, $4)`,
[htId, newTicketId, queueId, agentId, stateId, nowTs]
);
await client.query('COMMIT');
} catch (createErr) {
await client.query('ROLLBACK');
throw createErr;
} finally {
client.release();
}
}
// 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({
agente_id: agentId,
agente_nome: userLogin,
titolo_azione: `Aggiunta nota a Ticket #${resolved.tn}`,
azione: { ticket_id: resolved.id, ticket_tn: resolved.tn, subject: note.subject, time_units: note.time_units },
esito: '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;