feat: aggiunta possiblità di visualizzare un grafico temporale dei ticket, con configurazione avanzata delle serie dei dati ed esportazione dei ticket della serie.
This commit is contained in:
+339
-19
@@ -5,6 +5,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
const { db } = require('../activityDb');
|
||||
const XLSX = require('xlsx');
|
||||
|
||||
const ALGORITHM = 'aes-256-cbc';
|
||||
const SECRET_KEY = crypto.createHash('sha256').update(process.env.CRYPTO_KEY || 'default_secret_key_12345').digest();
|
||||
@@ -235,29 +236,348 @@ router.get('/settings', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/dashboard/settings — Save settings for the agent
|
||||
router.post('/settings', (req, res) => {
|
||||
const activeAgentId = parseInt(req.headers['x-agent-id'], 10) || 1;
|
||||
const { preview_limit, tickets_per_page } = req.body;
|
||||
|
||||
// GET /api/dashboard/chart-lines
|
||||
router.get('/chart-lines', (req, res) => {
|
||||
try {
|
||||
let currentSettings = { preview_limit: 10, tickets_per_page: 50 };
|
||||
const row = db.prepare("SELECT preview_limit, tickets_per_page FROM agent_settings WHERE agent_id = ?").get(activeAgentId);
|
||||
if (row) {
|
||||
currentSettings = row;
|
||||
const lines = db.prepare("SELECT * FROM dashboard_chart_lines ORDER BY is_default DESC, id ASC").all();
|
||||
const formatted = lines.map(line => ({
|
||||
...line,
|
||||
statuses: JSON.parse(line.statuses || '[]'),
|
||||
types: JSON.parse(line.types || '[]'),
|
||||
queues: JSON.parse(line.queues || '[]'),
|
||||
owners: JSON.parse(line.owners || '[]'),
|
||||
responsibles: JSON.parse(line.responsibles || '[]')
|
||||
}));
|
||||
res.json(formatted);
|
||||
} catch (err) {
|
||||
console.error('Error fetching chart lines:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/dashboard/chart-lines
|
||||
router.post('/chart-lines', (req, res) => {
|
||||
const { name, statuses, types, queues, owners, responsibles, color, is_visible } = req.body;
|
||||
if (!name) return res.status(400).json({ error: 'Name is required' });
|
||||
|
||||
try {
|
||||
const info = db.prepare(`
|
||||
INSERT INTO dashboard_chart_lines (name, statuses, types, queues, owners, responsibles, color, is_visible, is_default)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 0)
|
||||
`).run(
|
||||
name,
|
||||
JSON.stringify(statuses || []),
|
||||
JSON.stringify(types || []),
|
||||
JSON.stringify(queues || []),
|
||||
JSON.stringify(owners || []),
|
||||
JSON.stringify(responsibles || []),
|
||||
color || '#4f46e5',
|
||||
is_visible !== undefined ? parseInt(is_visible, 10) : 1
|
||||
);
|
||||
res.json({ success: true, id: info.lastInsertRowid });
|
||||
} catch (err) {
|
||||
console.error('Error saving chart line:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// PUT /api/dashboard/chart-lines/:id
|
||||
router.put('/chart-lines/:id', (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { name, statuses, types, queues, owners, responsibles, color, is_visible } = req.body;
|
||||
if (!name) return res.status(400).json({ error: 'Name is required' });
|
||||
|
||||
try {
|
||||
const info = db.prepare(`
|
||||
UPDATE dashboard_chart_lines
|
||||
SET name = ?, statuses = ?, types = ?, queues = ?, owners = ?, responsibles = ?, color = ?, is_visible = ?
|
||||
WHERE id = ?
|
||||
`).run(
|
||||
name,
|
||||
JSON.stringify(statuses || []),
|
||||
JSON.stringify(types || []),
|
||||
JSON.stringify(queues || []),
|
||||
JSON.stringify(owners || []),
|
||||
JSON.stringify(responsibles || []),
|
||||
color || '#4f46e5',
|
||||
is_visible !== undefined ? parseInt(is_visible, 10) : 1,
|
||||
id
|
||||
);
|
||||
if (info.changes === 0) return res.status(404).json({ error: 'Line not found' });
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('Error updating chart line:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// DELETE /api/dashboard/chart-lines/:id
|
||||
router.delete('/chart-lines/:id', (req, res) => {
|
||||
const { id } = req.params;
|
||||
try {
|
||||
const line = db.prepare("SELECT is_default FROM dashboard_chart_lines WHERE id = ?").get(id);
|
||||
if (!line) return res.status(404).json({ error: 'Line not found' });
|
||||
if (line.is_default === 1) {
|
||||
return res.status(400).json({ error: 'Cannot delete default line' });
|
||||
}
|
||||
|
||||
const newPreviewLimit = preview_limit !== undefined ? parseInt(preview_limit, 10) : currentSettings.preview_limit;
|
||||
const newTicketsPerPage = tickets_per_page !== undefined ? parseInt(tickets_per_page, 10) : currentSettings.tickets_per_page;
|
||||
|
||||
db.prepare(`
|
||||
INSERT OR REPLACE INTO agent_settings (agent_id, preview_limit, tickets_per_page)
|
||||
VALUES (?, ?, ?)
|
||||
`).run(activeAgentId, newPreviewLimit, newTicketsPerPage);
|
||||
|
||||
res.json({ success: true, preview_limit: newPreviewLimit, tickets_per_page: newTicketsPerPage });
|
||||
db.prepare("DELETE FROM dashboard_chart_lines WHERE id = ?").run(id);
|
||||
res.json({ success: true });
|
||||
} catch (err) {
|
||||
console.error('Error saving agent settings:', err);
|
||||
console.error('Error deleting chart line:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/dashboard/chart-data
|
||||
router.get('/chart-data', async (req, res) => {
|
||||
const { start_date, end_date } = req.query;
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({ error: 'start_date and end_date are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Only query data series that are configured as visible
|
||||
const lines = db.prepare("SELECT * FROM dashboard_chart_lines WHERE is_visible = 1 ORDER BY is_default DESC, id ASC").all();
|
||||
|
||||
const dates = [];
|
||||
let curr = new Date(start_date);
|
||||
const endLimit = new Date(end_date);
|
||||
while (curr <= endLimit) {
|
||||
const y = curr.getFullYear();
|
||||
const m = String(curr.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(curr.getDate()).padStart(2, '0');
|
||||
dates.push(`${y}-${m}-${d}`);
|
||||
curr.setDate(curr.getDate() + 1);
|
||||
}
|
||||
|
||||
const linesData = await Promise.all(lines.map(async (line) => {
|
||||
const conditions = [];
|
||||
const params = [];
|
||||
let paramIdx = 1;
|
||||
|
||||
conditions.push(`t.create_time >= $${paramIdx++}`);
|
||||
params.push(start_date + ' 00:00:00');
|
||||
conditions.push(`t.create_time <= $${paramIdx++}`);
|
||||
params.push(end_date + ' 23:59:59');
|
||||
|
||||
const statuses = JSON.parse(line.statuses || '[]');
|
||||
if (statuses.length > 0) {
|
||||
const placeholders = statuses.map(() => `$${paramIdx++}`).join(', ');
|
||||
conditions.push(`t.ticket_state_id IN (${placeholders})`);
|
||||
params.push(...statuses.map(id => parseInt(id)));
|
||||
}
|
||||
|
||||
const types = JSON.parse(line.types || '[]');
|
||||
if (types.length > 0) {
|
||||
const placeholders = types.map(() => `$${paramIdx++}`).join(', ');
|
||||
conditions.push(`t.type_id IN (${placeholders})`);
|
||||
params.push(...types.map(id => parseInt(id)));
|
||||
}
|
||||
|
||||
const queues = JSON.parse(line.queues || '[]');
|
||||
if (queues.length > 0) {
|
||||
const placeholders = queues.map(() => `$${paramIdx++}`).join(', ');
|
||||
conditions.push(`t.queue_id IN (${placeholders})`);
|
||||
params.push(...queues.map(id => parseInt(id)));
|
||||
}
|
||||
|
||||
const owners = JSON.parse(line.owners || '[]');
|
||||
if (owners.length > 0) {
|
||||
const placeholders = owners.map(() => `$${paramIdx++}`).join(', ');
|
||||
conditions.push(`t.user_id IN (${placeholders})`);
|
||||
params.push(...owners.map(id => parseInt(id)));
|
||||
}
|
||||
|
||||
const responsibles = JSON.parse(line.responsibles || '[]');
|
||||
if (responsibles.length > 0) {
|
||||
const placeholders = responsibles.map(() => `$${paramIdx++}`).join(', ');
|
||||
conditions.push(`t.responsible_user_id IN (${placeholders})`);
|
||||
params.push(...responsibles.map(id => parseInt(id)));
|
||||
}
|
||||
|
||||
const sql = `
|
||||
SELECT CAST(t.create_time AS DATE) AS date_val, COUNT(*) AS count
|
||||
FROM ticket t
|
||||
WHERE ${conditions.join(' AND ')}
|
||||
GROUP BY CAST(t.create_time AS DATE)
|
||||
`;
|
||||
|
||||
const result = await pool.query(sql, params);
|
||||
|
||||
const countsByDate = {};
|
||||
for (const row of result.rows) {
|
||||
let dateStr = row.date_val;
|
||||
if (dateStr instanceof Date) {
|
||||
const y = dateStr.getFullYear();
|
||||
const m = String(dateStr.getMonth() + 1).padStart(2, '0');
|
||||
const d = String(dateStr.getDate()).padStart(2, '0');
|
||||
dateStr = `${y}-${m}-${d}`;
|
||||
} else if (typeof dateStr === 'string') {
|
||||
dateStr = dateStr.split('T')[0];
|
||||
}
|
||||
countsByDate[dateStr] = parseInt(row.count) || 0;
|
||||
}
|
||||
|
||||
const data = dates.map(d => countsByDate[d] || 0);
|
||||
|
||||
return {
|
||||
id: line.id,
|
||||
name: line.name,
|
||||
color: line.color,
|
||||
is_default: line.is_default,
|
||||
data
|
||||
};
|
||||
}));
|
||||
|
||||
res.json({
|
||||
labels: dates,
|
||||
lines: linesData
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error generating chart data:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/dashboard/export-excel
|
||||
router.get('/export-excel', async (req, res) => {
|
||||
const { start_date, end_date, series_ids } = req.query;
|
||||
if (!start_date || !end_date) {
|
||||
return res.status(400).json({ error: 'start_date and end_date are required' });
|
||||
}
|
||||
|
||||
try {
|
||||
let lines = [];
|
||||
if (series_ids) {
|
||||
const ids = series_ids.split(',').map(id => parseInt(id, 10)).filter(id => !isNaN(id));
|
||||
if (ids.length > 0) {
|
||||
const placeholders = ids.map(() => '?').join(', ');
|
||||
lines = db.prepare(`SELECT * FROM dashboard_chart_lines WHERE is_visible = 1 AND id IN (${placeholders}) ORDER BY is_default DESC, id ASC`).all(...ids);
|
||||
}
|
||||
} else {
|
||||
lines = db.prepare("SELECT * FROM dashboard_chart_lines WHERE is_visible = 1 ORDER BY is_default DESC, id ASC").all();
|
||||
}
|
||||
|
||||
const wb = XLSX.utils.book_new();
|
||||
const allRows = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const conditions = [];
|
||||
const params = [];
|
||||
let paramIdx = 1;
|
||||
|
||||
conditions.push(`t.create_time >= $${paramIdx++}`);
|
||||
params.push(start_date + ' 00:00:00');
|
||||
conditions.push(`t.create_time <= $${paramIdx++}`);
|
||||
params.push(end_date + ' 23:59:59');
|
||||
|
||||
const statuses = JSON.parse(line.statuses || '[]');
|
||||
if (statuses.length > 0) {
|
||||
const placeholders = statuses.map(() => `$${paramIdx++}`).join(', ');
|
||||
conditions.push(`t.ticket_state_id IN (${placeholders})`);
|
||||
params.push(...statuses.map(id => parseInt(id)));
|
||||
}
|
||||
|
||||
const types = JSON.parse(line.types || '[]');
|
||||
if (types.length > 0) {
|
||||
const placeholders = types.map(() => `$${paramIdx++}`).join(', ');
|
||||
conditions.push(`t.type_id IN (${placeholders})`);
|
||||
params.push(...types.map(id => parseInt(id)));
|
||||
}
|
||||
|
||||
const queues = JSON.parse(line.queues || '[]');
|
||||
if (queues.length > 0) {
|
||||
const placeholders = queues.map(() => `$${paramIdx++}`).join(', ');
|
||||
conditions.push(`t.queue_id IN (${placeholders})`);
|
||||
params.push(...queues.map(id => parseInt(id)));
|
||||
}
|
||||
|
||||
const owners = JSON.parse(line.owners || '[]');
|
||||
if (owners.length > 0) {
|
||||
const placeholders = owners.map(() => `$${paramIdx++}`).join(', ');
|
||||
conditions.push(`t.user_id IN (${placeholders})`);
|
||||
params.push(...owners.map(id => parseInt(id)));
|
||||
}
|
||||
|
||||
const responsibles = JSON.parse(line.responsibles || '[]');
|
||||
if (responsibles.length > 0) {
|
||||
const placeholders = responsibles.map(() => `$${paramIdx++}`).join(', ');
|
||||
conditions.push(`t.responsible_user_id IN (${placeholders})`);
|
||||
params.push(...responsibles.map(id => parseInt(id)));
|
||||
}
|
||||
|
||||
const sql = `
|
||||
SELECT
|
||||
t.id AS ticket_id,
|
||||
t.tn,
|
||||
t.title,
|
||||
tt.name AS type_name,
|
||||
ts.name AS state_name,
|
||||
tst.name AS state_type_name,
|
||||
q.name AS queue_name,
|
||||
COALESCE(u.first_name || ' ' || u.last_name, u.login) AS owner_name,
|
||||
COALESCE(ru.first_name || ' ' || ru.last_name, ru.login) AS responsible_name,
|
||||
t.create_time,
|
||||
t.change_time
|
||||
FROM ticket t
|
||||
LEFT JOIN ticket_type tt ON t.type_id = tt.id
|
||||
LEFT JOIN ticket_state ts ON t.ticket_state_id = ts.id
|
||||
LEFT JOIN ticket_state_type tst ON ts.type_id = tst.id
|
||||
LEFT JOIN queue q ON t.queue_id = q.id
|
||||
LEFT JOIN users u ON t.user_id = u.id
|
||||
LEFT JOIN users ru ON t.responsible_user_id = ru.id
|
||||
WHERE ${conditions.join(' AND ')}
|
||||
ORDER BY t.create_time DESC
|
||||
`;
|
||||
|
||||
const result = await pool.query(sql, params);
|
||||
|
||||
const rows = result.rows.map(t => {
|
||||
const isClosed = t.state_type_name && (
|
||||
t.state_type_name.toLowerCase().includes('closed') ||
|
||||
t.state_name.toLowerCase().includes('closed') ||
|
||||
t.state_name.toLowerCase().includes('chiuso') ||
|
||||
t.state_name.toLowerCase().includes('chiusa')
|
||||
);
|
||||
const closureDate = isClosed ? t.change_time : '';
|
||||
return {
|
||||
'Origine della serie': line.name,
|
||||
'ID Ticket': t.ticket_id,
|
||||
'Numero (TN)': t.tn,
|
||||
'Oggetto': t.title || '',
|
||||
'Tipo': t.type_name || '',
|
||||
'Stato': t.state_name || '',
|
||||
'Coda': t.queue_name || '',
|
||||
'Owner': t.owner_name || '',
|
||||
'Responsabile': t.responsible_name || '',
|
||||
'Data di Creazione': t.create_time,
|
||||
'Data di Chiusura': closureDate
|
||||
};
|
||||
});
|
||||
|
||||
allRows.push(...rows);
|
||||
|
||||
const ws = XLSX.utils.json_to_sheet(rows);
|
||||
const cleanName = line.name.replace(/[\\\/\?\*\:\[\]]/g, '').slice(0, 30);
|
||||
XLSX.utils.book_append_sheet(wb, ws, cleanName || `Serie ${line.id}`);
|
||||
}
|
||||
|
||||
if (allRows.length > 0) {
|
||||
// Sort combined rows by Data di Creazione descending
|
||||
allRows.sort((a, b) => new Date(b['Data di Creazione']) - new Date(a['Data di Creazione']));
|
||||
const wsAll = XLSX.utils.json_to_sheet(allRows);
|
||||
wb.SheetNames.unshift('Tutte le serie');
|
||||
wb.Sheets['Tutte le serie'] = wsAll;
|
||||
}
|
||||
|
||||
const buf = XLSX.write(wb, { type: 'buffer', bookType: 'xlsx' });
|
||||
|
||||
res.setHeader('Content-Disposition', `attachment; filename="Andamento_Ticket_${start_date}_${end_date}.xlsx"`);
|
||||
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');
|
||||
res.send(buf);
|
||||
} catch (err) {
|
||||
console.error('Error exporting excel:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user