feat: gestione gruppi di ticket. feat: miglioramento filtri ticket a mio carico e ticket. feat: possiblità di copiare il numero del tocket con pulsante copia

This commit is contained in:
2026-07-09 08:36:03 +02:00
parent 5b914eb198
commit a89605b888
14 changed files with 1674 additions and 182 deletions
+241
View File
@@ -0,0 +1,241 @@
const express = require('express');
const router = express.Router();
const pool = require('../db');
const { db } = require('../activityDb');
// Helper to fetch details of a list of tickets from OTRS DB
async function fetchTicketsDetails(ticketIds) {
if (!ticketIds || ticketIds.length === 0) return [];
try {
const placeholders = ticketIds.map((_, i) => `$${i + 1}`).join(', ');
const query = `
SELECT
t.id, t.tn, t.title,
t.queue_id, q.name AS queue_name,
t.ticket_state_id, ts.name AS state_name, tst.name AS state_type,
t.ticket_priority_id, tp.name AS priority_name,
t.user_id, u.first_name AS owner_first, u.last_name AS owner_last, u.login AS owner_login
FROM ticket t
JOIN queue q ON t.queue_id = q.id
JOIN ticket_state ts ON t.ticket_state_id = ts.id
JOIN ticket_state_type tst ON ts.type_id = tst.id
JOIN ticket_priority tp ON t.ticket_priority_id = tp.id
LEFT JOIN users u ON t.user_id = u.id
WHERE t.id IN (${placeholders})
`;
const res = await pool.query(query, ticketIds);
return res.rows;
} catch (err) {
console.error('Error fetching ticket details from OTRS DB:', err);
return [];
}
}
// GET /api/groups - List all groups with member counts
router.get('/', (req, res) => {
try {
const groups = db.prepare(`
SELECT g.*,
(SELECT COUNT(*) FROM ticket_group_members WHERE group_id = g.id) AS member_count
FROM ticket_groups g
ORDER BY g.nome ASC
`).all();
res.json(groups);
} catch (err) {
res.status(500).json({ error: 'Errore nel recupero dei gruppi', message: err.message });
}
});
// GET /api/groups/by-ticket/:ticket_id - Get groups associated to a specific ticket
router.get('/by-ticket/:ticket_id', (req, res) => {
try {
const { ticket_id } = req.params;
const ticketIdNum = parseInt(ticket_id, 10);
if (isNaN(ticketIdNum)) {
return res.status(400).json({ error: 'ID ticket non valido' });
}
const asMaster = db.prepare('SELECT id, nome, descrizione FROM ticket_groups WHERE master_ticket_id = ?').all(ticketIdNum);
const asMember = db.prepare(`
SELECT g.id, g.nome, g.descrizione
FROM ticket_groups g
JOIN ticket_group_members m ON g.id = m.group_id
WHERE m.ticket_id = ?
`).all(ticketIdNum);
res.json({ asMaster, asMember });
} catch (err) {
res.status(500).json({ error: 'Errore nel recupero dei gruppi del ticket', message: err.message });
}
});
// POST /api/groups - Create a new group
router.post('/', (req, res) => {
try {
const { nome, descrizione, master_ticket_id } = req.body;
if (!nome) {
return res.status(400).json({ error: 'Il nome del gruppo è obbligatorio' });
}
const masterId = master_ticket_id ? parseInt(master_ticket_id, 10) : null;
const info = db.prepare(`
INSERT INTO ticket_groups (nome, descrizione, master_ticket_id)
VALUES (?, ?, ?)
`).run(nome, descrizione || '', masterId);
res.json({ id: info.lastInsertRowid, nome, descrizione, master_ticket_id: masterId });
} catch (err) {
res.status(500).json({ error: 'Errore nella creazione del gruppo', message: err.message });
}
});
// GET /api/groups/:id - Detail of a single group
router.get('/:id', async (req, res) => {
try {
const { id } = req.params;
const group = db.prepare('SELECT * FROM ticket_groups WHERE id = ?').get(id);
if (!group) {
return res.status(404).json({ error: 'Gruppo non trovato' });
}
// Get members list
const memberRows = db.prepare('SELECT ticket_id FROM ticket_group_members WHERE group_id = ?').all(id);
const memberIds = memberRows.map(r => r.ticket_id);
// Fetch details of master and member tickets from OTRS
let masterTicket = null;
if (group.master_ticket_id) {
const details = await fetchTicketsDetails([group.master_ticket_id]);
if (details.length > 0) {
masterTicket = details[0];
}
}
let memberTickets = [];
if (memberIds.length > 0) {
memberTickets = await fetchTicketsDetails(memberIds);
}
res.json({
group,
masterTicket,
memberTickets
});
} catch (err) {
res.status(500).json({ error: 'Errore nel recupero dei dettagli del gruppo', message: err.message });
}
});
// PUT /api/groups/:id - Update group info
router.put('/:id', (req, res) => {
try {
const { id } = req.params;
const { nome, descrizione, master_ticket_id } = req.body;
if (!nome) {
return res.status(400).json({ error: 'Il nome del gruppo è obbligatorio' });
}
const masterId = master_ticket_id ? parseInt(master_ticket_id, 10) : null;
const info = db.prepare(`
UPDATE ticket_groups
SET nome = ?, descrizione = ?, master_ticket_id = ?
WHERE id = ?
`).run(nome, descrizione || '', masterId, id);
if (info.changes === 0) {
return res.status(404).json({ error: 'Gruppo non trovato o nessuna modifica' });
}
res.json({ id: parseInt(id, 10), nome, descrizione, master_ticket_id: masterId });
} catch (err) {
res.status(500).json({ error: 'Errore nell\'aggiornamento del gruppo', message: err.message });
}
});
// DELETE /api/groups/:id - Delete group
router.delete('/:id', (req, res) => {
try {
const { id } = req.params;
const info = db.prepare('DELETE FROM ticket_groups WHERE id = ?').run(id);
if (info.changes === 0) {
return res.status(404).json({ error: 'Gruppo non trovato' });
}
res.json({ success: true, message: 'Gruppo eliminato con successo' });
} catch (err) {
res.status(500).json({ error: 'Errore nella cancellazione del gruppo', message: err.message });
}
});
// POST /api/groups/:id/tickets - Add ticket(s) to group
router.post('/:id/tickets', async (req, res) => {
try {
const { id } = req.params;
const { ticket_identifier } = req.body; // Can be ticket ID or ticket number (tn)
if (!ticket_identifier) {
return res.status(400).json({ error: 'Identificativo ticket obbligatorio' });
}
const cleanIdentifier = String(ticket_identifier).trim();
let queryTicketSql;
let queryParams;
if (/^\d+$/.test(cleanIdentifier)) {
// It's a number - check if it matches id or tn
queryTicketSql = 'SELECT id, tn, title FROM ticket WHERE id = $1 OR tn = $2';
queryParams = [parseInt(cleanIdentifier, 10), cleanIdentifier];
} else {
// Check tn
queryTicketSql = 'SELECT id, tn, title FROM ticket WHERE tn = $1';
queryParams = [cleanIdentifier];
}
const otrsRes = await pool.query(queryTicketSql, queryParams);
if (otrsRes.rows.length === 0) {
return res.status(404).json({ error: `Ticket con identificativo '${cleanIdentifier}' non trovato` });
}
const ticket = otrsRes.rows[0];
// Check if group exists
const group = db.prepare('SELECT id FROM ticket_groups WHERE id = ?').get(id);
if (!group) {
return res.status(404).json({ error: 'Gruppo non trovato' });
}
// Insert to membership
try {
db.prepare(`
INSERT INTO ticket_group_members (group_id, ticket_id)
VALUES (?, ?)
`).run(id, ticket.id);
} catch (dbErr) {
if (dbErr.code === 'SQLITE_CONSTRAINT_PRIMARYKEY') {
return res.status(409).json({ error: 'Il ticket appartiene già a questo gruppo' });
}
throw dbErr;
}
res.json({ success: true, ticket });
} catch (err) {
res.status(500).json({ error: 'Errore nell\'associazione del ticket', message: err.message });
}
});
// DELETE /api/groups/:id/tickets/:ticket_id - Remove ticket from group
router.delete('/:id/tickets/:ticket_id', (req, res) => {
try {
const { id, ticket_id } = req.params;
const info = db.prepare('DELETE FROM ticket_group_members WHERE group_id = ? AND ticket_id = ?').run(id, ticket_id);
if (info.changes === 0) {
return res.status(404).json({ error: 'Associazione non trovata' });
}
res.json({ success: true, message: 'Ticket rimosso dal gruppo' });
} catch (err) {
res.status(500).json({ error: 'Errore nella rimozione del ticket', message: err.message });
}
});
module.exports = router;
+72
View File
@@ -0,0 +1,72 @@
const express = require('express');
const router = express.Router();
const { db } = require('../activityDb');
// GET /api/presets - Get all presets for the active agent and page mode
router.get('/', (req, res) => {
try {
const agentId = parseInt(req.headers['x-agent-id'] || '1', 10);
const { page_mode } = req.query; // 'general' or 'my'
if (!page_mode) {
return res.status(400).json({ error: 'Il parametro page_mode è obbligatorio' });
}
const presets = db.prepare(`
SELECT * FROM filter_presets
WHERE agent_id = ? AND page_mode = ?
ORDER BY name ASC
`).all(agentId, page_mode);
res.json(presets);
} catch (err) {
res.status(500).json({ error: 'Errore nel caricamento dei preset', message: err.message });
}
});
// POST /api/presets - Save a new filter preset
router.post('/', (req, res) => {
try {
const agentId = parseInt(req.headers['x-agent-id'] || '1', 10);
const { name, page_mode, filters } = req.body;
if (!name || !page_mode || !filters) {
return res.status(400).json({ error: 'I campi name, page_mode e filters sono obbligatori' });
}
const filtersJson = typeof filters === 'string' ? filters : JSON.stringify(filters);
const info = db.prepare(`
INSERT INTO filter_presets (agent_id, name, page_mode, filters_json)
VALUES (?, ?, ?, ?)
`).run(agentId, name, page_mode, filtersJson);
res.json({
id: info.lastInsertRowid,
agent_id: agentId,
name,
page_mode,
filters_json: filtersJson
});
} catch (err) {
res.status(500).json({ error: 'Errore nel salvataggio del preset', message: err.message });
}
});
// DELETE /api/presets/:id - Delete a preset
router.delete('/:id', (req, res) => {
try {
const { id } = req.params;
const info = db.prepare('DELETE FROM filter_presets WHERE id = ?').run(id);
if (info.changes === 0) {
return res.status(404).json({ error: 'Preset non trovato' });
}
res.json({ success: true, message: 'Preset eliminato con successo' });
} catch (err) {
res.status(500).json({ error: 'Errore nella rimozione del preset', message: err.message });
}
});
module.exports = router;
+45 -6
View File
@@ -123,8 +123,21 @@ router.get('/', async (req, res) => {
}
if (queue_id) {
conditions.push(`t.queue_id = $${paramIdx++}`);
params.push(parseInt(queue_id));
let queueIds = [];
if (Array.isArray(queue_id)) {
queueIds = queue_id.map(id => parseInt(id)).filter(id => !isNaN(id));
} else if (typeof queue_id === 'string') {
queueIds = queue_id.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
} else {
const parsed = parseInt(queue_id);
if (!isNaN(parsed)) queueIds.push(parsed);
}
if (queueIds.length > 0) {
const placeholders = queueIds.map(() => `$${paramIdx++}`).join(', ');
conditions.push(`t.queue_id IN (${placeholders})`);
params.push(...queueIds);
}
}
if (state_id) {
let stateIds = [];
@@ -144,12 +157,38 @@ router.get('/', async (req, res) => {
}
}
if (priority_id) {
conditions.push(`t.ticket_priority_id = $${paramIdx++}`);
params.push(parseInt(priority_id));
let priorityIds = [];
if (Array.isArray(priority_id)) {
priorityIds = priority_id.map(id => parseInt(id)).filter(id => !isNaN(id));
} else if (typeof priority_id === 'string') {
priorityIds = priority_id.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
} else {
const parsed = parseInt(priority_id);
if (!isNaN(parsed)) priorityIds.push(parsed);
}
if (priorityIds.length > 0) {
const placeholders = priorityIds.map(() => `$${paramIdx++}`).join(', ');
conditions.push(`t.ticket_priority_id IN (${placeholders})`);
params.push(...priorityIds);
}
}
if (user_id) {
conditions.push(`t.user_id = $${paramIdx++}`);
params.push(parseInt(user_id));
let userIds = [];
if (Array.isArray(user_id)) {
userIds = user_id.map(id => parseInt(id)).filter(id => !isNaN(id));
} else if (typeof user_id === 'string') {
userIds = user_id.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
} else {
const parsed = parseInt(user_id);
if (!isNaN(parsed)) userIds.push(parsed);
}
if (userIds.length > 0) {
const placeholders = userIds.map(() => `$${paramIdx++}`).join(', ');
conditions.push(`t.user_id IN (${placeholders})`);
params.push(...userIds);
}
}
if (type_id) {
conditions.push(`t.type_id = $${paramIdx++}`);