feat: aggiunta ricerca spudorata. progress: ripreso con il tentativo di invio delle mail che non creino nuovi ticket
This commit is contained in:
1 parent
3650bf5790
commit
8d24759a15
18 files changed
+1661
-98
No files matched your search
@@ -0,0 +1,147 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { db, logAttivita } = require('../activityDb');
|
||||
const pool = require('../db');
|
||||
|
||||
const baseDir = process.pkg ? path.dirname(process.execPath) : path.resolve(__dirname, '..');
|
||||
const envPath = fs.existsSync(path.resolve(baseDir, '.env'))
|
||||
? path.resolve(baseDir, '.env')
|
||||
: path.resolve(__dirname, '..', '.env');
|
||||
|
||||
// Helper to resolve agent name from DB
|
||||
async function resolveAgentName(agentId) {
|
||||
try {
|
||||
const r = await pool.query(
|
||||
`SELECT first_name, last_name, login FROM users WHERE id = $1`,
|
||||
[agentId]
|
||||
);
|
||||
if (r.rows.length > 0) {
|
||||
const u = r.rows[0];
|
||||
return (u.first_name || '') + ' ' + (u.last_name || '') || u.login || `Agent #${agentId}`;
|
||||
}
|
||||
} catch (_) { /* ignore */ }
|
||||
return `Agent #${agentId}`;
|
||||
}
|
||||
|
||||
// GET /api/config/env — Read current .env file and history
|
||||
router.get('/env', async (req, res) => {
|
||||
try {
|
||||
let content = '';
|
||||
if (fs.existsSync(envPath)) {
|
||||
content = fs.readFileSync(envPath, 'utf8');
|
||||
}
|
||||
|
||||
const history = db.prepare(`
|
||||
SELECT * FROM env_history ORDER BY id DESC LIMIT 100
|
||||
`).all();
|
||||
|
||||
res.json({ content, history });
|
||||
} catch (err) {
|
||||
console.error('Error reading env config:', err);
|
||||
res.status(500).json({ error: 'Errore durante la lettura del file .env', message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/config/env — Save updated .env content and create snapshot in SQLite
|
||||
router.post('/env', async (req, res) => {
|
||||
try {
|
||||
const { content, note } = req.body;
|
||||
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
|
||||
const agentName = await resolveAgentName(operatorId);
|
||||
|
||||
if (typeof content !== 'string') {
|
||||
return res.status(400).json({ error: 'Il contenuto del file .env è obbligatorio' });
|
||||
}
|
||||
|
||||
// If history is empty and current .env exists, create an initial baseline snapshot
|
||||
const countRow = db.prepare('SELECT COUNT(*) AS count FROM env_history').get();
|
||||
if (countRow.count === 0 && fs.existsSync(envPath)) {
|
||||
const initialContent = fs.readFileSync(envPath, 'utf8');
|
||||
db.prepare(`
|
||||
INSERT INTO env_history (agent_id, agent_name, content, note)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`).run(operatorId, agentName, initialContent, 'Baseline iniziale preesistente');
|
||||
}
|
||||
|
||||
// Write updated content to disk
|
||||
fs.writeFileSync(envPath, content, 'utf8');
|
||||
|
||||
// Save snapshot in history table
|
||||
const info = db.prepare(`
|
||||
INSERT INTO env_history (agent_id, agent_name, content, note)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`).run(operatorId, agentName, content, note || 'Modifica manuale .env');
|
||||
|
||||
logAttivita({
|
||||
agente_id: operatorId,
|
||||
agente_nome: agentName,
|
||||
titolo_azione: 'Modifica .env',
|
||||
azione: { note: note || 'Modifica manuale .env', history_id: info.lastInsertRowid },
|
||||
esito: 'successo',
|
||||
});
|
||||
|
||||
const history = db.prepare(`
|
||||
SELECT * FROM env_history ORDER BY id DESC LIMIT 100
|
||||
`).all();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'File .env aggiornato e salvato nello storico locale con successo!',
|
||||
history_id: info.lastInsertRowid,
|
||||
history,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error saving .env config:', err);
|
||||
res.status(500).json({ error: 'Errore durante il salvataggio del file .env', message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/config/env/restore/:history_id — Restore a previous .env snapshot
|
||||
router.post('/env/restore/:history_id', async (req, res) => {
|
||||
try {
|
||||
const { history_id } = req.params;
|
||||
const operatorId = parseInt(req.headers['x-agent-id'] || '1', 10);
|
||||
const agentName = await resolveAgentName(operatorId);
|
||||
|
||||
const record = db.prepare('SELECT * FROM env_history WHERE id = ?').get(history_id);
|
||||
if (!record) {
|
||||
return res.status(404).json({ error: 'Versione di storico non trovata' });
|
||||
}
|
||||
|
||||
// Write snapshot content to disk
|
||||
fs.writeFileSync(envPath, record.content, 'utf8');
|
||||
|
||||
// Insert new history record documenting the restore operation
|
||||
const restoreNote = `Ripristinato alla versione #${record.id} del ${record.created_at}`;
|
||||
const info = db.prepare(`
|
||||
INSERT INTO env_history (agent_id, agent_name, content, note)
|
||||
VALUES (?, ?, ?, ?)
|
||||
`).run(operatorId, agentName, record.content, restoreNote);
|
||||
|
||||
logAttivita({
|
||||
agente_id: operatorId,
|
||||
agente_nome: agentName,
|
||||
titolo_azione: 'Ripristino .env',
|
||||
azione: { restored_from_id: record.id, new_history_id: info.lastInsertRowid },
|
||||
esito: 'successo',
|
||||
});
|
||||
|
||||
const history = db.prepare(`
|
||||
SELECT * FROM env_history ORDER BY id DESC LIMIT 100
|
||||
`).all();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Versione #${record.id} ripristinata con successo!`,
|
||||
content: record.content,
|
||||
history,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error restoring .env version:', err);
|
||||
res.status(500).json({ error: 'Errore durante il ripristino della versione .env', message: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
+100
-19
@@ -193,6 +193,59 @@ router.delete('/address-groups/:id', (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ─── SETTINGS ──────────────────────────────────────────────────────────────────
|
||||
|
||||
// GET /api/email/settings — Fetch agent sender preference
|
||||
router.get('/settings', async (req, res) => {
|
||||
try {
|
||||
const agentId = parseInt(req.query.agent_id || '1', 10);
|
||||
const row = db.prepare(`SELECT use_agent_sender FROM agent_settings WHERE agent_id = ?`).get(agentId);
|
||||
const useAgentSender = row ? Boolean(row.use_agent_sender) : false;
|
||||
|
||||
let agentEmail = '';
|
||||
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) {
|
||||
agentEmail = prefRes.rows[0].preferences_value || '';
|
||||
}
|
||||
|
||||
const envSender = process.env.AZURE_MAIL_SENDER || process.env.SMTP_FROM || process.env.SMTP_USER || '';
|
||||
|
||||
res.json({
|
||||
agent_id: agentId,
|
||||
use_agent_sender: useAgentSender,
|
||||
agent_email: agentEmail,
|
||||
env_sender: envSender
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('[Email] Error fetching settings:', err);
|
||||
res.status(500).json({ error: 'Errore durante il recupero delle impostazioni email: ' + err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/email/settings — Update agent sender preference
|
||||
router.post('/settings', async (req, res) => {
|
||||
try {
|
||||
const { agent_id, use_agent_sender } = req.body;
|
||||
const agentId = parseInt(agent_id || '1', 10);
|
||||
const val = use_agent_sender ? 1 : 0;
|
||||
|
||||
const existing = db.prepare(`SELECT agent_id FROM agent_settings WHERE agent_id = ?`).get(agentId);
|
||||
if (existing) {
|
||||
db.prepare(`UPDATE agent_settings SET use_agent_sender = ? WHERE agent_id = ?`).run(val, agentId);
|
||||
} else {
|
||||
db.prepare(`INSERT INTO agent_settings (agent_id, use_agent_sender) VALUES (?, ?)`).run(agentId, val);
|
||||
}
|
||||
|
||||
res.json({ success: true, use_agent_sender: Boolean(val) });
|
||||
} catch (err) {
|
||||
console.error('[Email] Error updating settings:', err);
|
||||
res.status(500).json({ error: 'Errore durante il salvataggio delle impostazioni email: ' + err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ─── SEND EMAIL ───────────────────────────────────────────────────────────────
|
||||
|
||||
// POST /api/email/send — send an email for a ticket
|
||||
@@ -207,7 +260,6 @@ router.post('/send', async (req, res) => {
|
||||
attachments = [],
|
||||
inlineImages = [],
|
||||
agentId,
|
||||
agentName = 'Agente',
|
||||
keepHelpdeskCopy = true,
|
||||
inReplyTo,
|
||||
references,
|
||||
@@ -218,6 +270,39 @@ router.post('/send', async (req, res) => {
|
||||
if (!bodyHtml) return res.status(400).json({ error: 'Il corpo della email è obbligatorio' });
|
||||
|
||||
try {
|
||||
// Fetch agent email and login details
|
||||
let agentEmail = '';
|
||||
let agentName = '';
|
||||
|
||||
if (agentId) {
|
||||
const userRes = await pool.query(
|
||||
`SELECT login, first_name, last_name FROM users WHERE id = $1`,
|
||||
[agentId]
|
||||
);
|
||||
if (userRes.rows.length > 0) {
|
||||
const u = userRes.rows[0];
|
||||
agentName = (u.first_name && u.last_name) ? `${u.first_name} ${u.last_name}` : (u.login || '');
|
||||
}
|
||||
|
||||
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) {
|
||||
agentEmail = prefRes.rows[0].preferences_value || '';
|
||||
}
|
||||
}
|
||||
|
||||
// Check if agent sender override is enabled in agent_settings SQLite
|
||||
let customSender = null;
|
||||
if (agentId) {
|
||||
const settingRow = db.prepare(`SELECT use_agent_sender FROM agent_settings WHERE agent_id = ?`).get(parseInt(agentId, 10));
|
||||
const useAgentSender = settingRow ? settingRow.use_agent_sender === 1 : false;
|
||||
if (useAgentSender && agentEmail) {
|
||||
customSender = agentName ? `"${agentName}" <${agentEmail}>` : agentEmail;
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Fetch ticket number and title for subject
|
||||
const ticketResult = await pool.query(
|
||||
`SELECT tn, title FROM ticket WHERE id = $1`,
|
||||
@@ -263,29 +348,25 @@ router.post('/send', async (req, res) => {
|
||||
const messageId = `<${Date.now()}.${Math.random().toString(36).substring(2)}@pharmaidea.com>`;
|
||||
|
||||
// 3. Send via configured mailer (Graph API or SMTP)
|
||||
const mailResult = await sendMail({ to, cc, bcc: bccList, subject, bodyHtml: processedBodyHtml, attachments, inlineImages: finalInlineImages, inReplyTo, references, messageId });
|
||||
const mailResult = await sendMail({
|
||||
from: customSender,
|
||||
to,
|
||||
cc,
|
||||
bcc: bccList,
|
||||
subject,
|
||||
bodyHtml: processedBodyHtml,
|
||||
attachments,
|
||||
inlineImages: finalInlineImages,
|
||||
inReplyTo,
|
||||
references,
|
||||
messageId
|
||||
});
|
||||
const finalMessageId = (mailResult && mailResult.internetMessageId) || messageId;
|
||||
|
||||
// 4. Log article in OTRS ticket via DB as a standard Email article
|
||||
try {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const agentLoginResult = agentId
|
||||
? await pool.query(`SELECT login FROM users WHERE id = $1`, [agentId])
|
||||
: null;
|
||||
const agentLogin = agentLoginResult?.rows[0]?.login || 'system';
|
||||
|
||||
// Query email from user_preferences for agent
|
||||
let agentEmail = '';
|
||||
if (agentId) {
|
||||
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) {
|
||||
agentEmail = prefRes.rows[0].preferences_value || '';
|
||||
}
|
||||
}
|
||||
const aFrom = agentEmail ? `"${agentLogin}" <${agentEmail}>` : `"${agentLogin}" <${process.env.AZURE_MAIL_SENDER || process.env.SMTP_FROM || 'helpdesk@example.com'}>`;
|
||||
const aFrom = customSender || (agentEmail ? `"${agentName}" <${agentEmail}>` : `"${agentName || 'system'}" <${process.env.AZURE_MAIL_SENDER || process.env.SMTP_FROM || 'helpdesk@example.com'}>`);
|
||||
|
||||
const toList = to.join(', ');
|
||||
|
||||
|
||||
+10
-3
@@ -3,6 +3,12 @@ const router = express.Router();
|
||||
const pool = require('../db');
|
||||
const { db, logAttivita } = require('../activityDb');
|
||||
|
||||
function getLocalTimestamp() {
|
||||
const d = new Date();
|
||||
const pad = (n) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||
}
|
||||
|
||||
// Helper to fetch details of a list of tickets from OTRS DB
|
||||
async function fetchTicketsDetails(ticketIds) {
|
||||
if (!ticketIds || ticketIds.length === 0) return [];
|
||||
@@ -295,16 +301,17 @@ router.post('/close-end-of-day', async (req, res) => {
|
||||
stateId = fallbackRes.rows[0]?.id || 2;
|
||||
}
|
||||
|
||||
const localNow = getLocalTimestamp();
|
||||
const updatePlaceholders = openTicketIds.map((_, i) => `$${i + 1}`).join(', ');
|
||||
const updateQuery = `
|
||||
UPDATE ticket
|
||||
SET ticket_state_id = $${openTicketIds.length + 1},
|
||||
ticket_lock_id = 1,
|
||||
change_time = NOW(),
|
||||
change_by = $${openTicketIds.length + 2}
|
||||
change_time = $${openTicketIds.length + 2},
|
||||
change_by = $${openTicketIds.length + 3}
|
||||
WHERE id IN (${updatePlaceholders})
|
||||
`;
|
||||
await pool.query(updateQuery, [...openTicketIds, stateId, operatorId]);
|
||||
await pool.query(updateQuery, [...openTicketIds, stateId, localNow, operatorId]);
|
||||
|
||||
// Log activity
|
||||
logAttivita({
|
||||
|
||||
+109
-48
@@ -890,8 +890,9 @@ router.patch('/:id', async (req, res) => {
|
||||
|
||||
if (setClauses.length > 0) {
|
||||
// Always update change_time and change_by
|
||||
setClauses.push(`change_time = NOW()`);
|
||||
setClauses.push(`change_time = $${pIdx++}`);
|
||||
setClauses.push(`change_by = $${pIdx++}`);
|
||||
setParams.push(localNow);
|
||||
setParams.push(operatorId);
|
||||
setParams.push(parseInt(id));
|
||||
|
||||
@@ -902,8 +903,8 @@ router.patch('/:id', async (req, res) => {
|
||||
} else {
|
||||
// If there are no fields modified but we have a note or time_unit, we still update change_time/change_by
|
||||
await client.query(
|
||||
`UPDATE ticket SET change_time = NOW(), change_by = $1 WHERE id = $2`,
|
||||
[operatorId, id]
|
||||
`UPDATE ticket SET change_time = $1, change_by = $2 WHERE id = $3`,
|
||||
[localNow, operatorId, id]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -950,13 +951,13 @@ router.patch('/:id', async (req, res) => {
|
||||
) VALUES (
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7, $8,
|
||||
NOW(), $9, NOW(), $9
|
||||
$9, $10, $9, $10
|
||||
)`,
|
||||
[
|
||||
historyName,
|
||||
htResult.rows[0].id, parseInt(id), newTypeId, newQueueId,
|
||||
newOwnerId, newPriorityId, newStateId,
|
||||
operatorId
|
||||
localNow, operatorId
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -983,9 +984,9 @@ router.patch('/:id', async (req, res) => {
|
||||
is_visible_for_customer, search_index_needs_rebuild,
|
||||
create_time, create_by, change_time, change_by
|
||||
) VALUES (
|
||||
$1, $2, $3, 0, 1, NOW(), $4, NOW(), $4
|
||||
$1, $2, $3, 0, 1, $4, $5, $4, $5
|
||||
) RETURNING id`,
|
||||
[id, senderTypeId, channelId, operatorId]
|
||||
[id, senderTypeId, channelId, localNow, operatorId]
|
||||
);
|
||||
|
||||
const articleId = articleResult.rows[0].id;
|
||||
@@ -1012,9 +1013,9 @@ router.patch('/:id', async (req, res) => {
|
||||
$1, $2, '', '', '', '', $3, $4,
|
||||
'', '', '',
|
||||
'text/html; charset=utf-8', EXTRACT(EPOCH FROM NOW())::INTEGER, $5,
|
||||
NOW(), $6, NOW(), $6
|
||||
$6, $7, $6, $7
|
||||
)`,
|
||||
[articleId, fromHeader, noteSubject, htmlBody, contentPath, operatorId]
|
||||
[articleId, fromHeader, noteSubject, htmlBody, contentPath, localNow, operatorId]
|
||||
);
|
||||
|
||||
// Create article_data_mime_attachment for OTRS CE HTML rendering
|
||||
@@ -1024,9 +1025,9 @@ router.patch('/:id', async (req, res) => {
|
||||
create_time, create_by, change_time, change_by
|
||||
) VALUES (
|
||||
$1, 'file-1', $2, 'text/html; charset="utf-8"', '', $3,
|
||||
NOW(), $4, NOW(), $4
|
||||
$4, $5, $4, $5
|
||||
)`,
|
||||
[articleId, String(contentSize), base64Body, operatorId]
|
||||
[articleId, String(contentSize), base64Body, localNow, operatorId]
|
||||
);
|
||||
|
||||
// Log the time in time_accounting if provided
|
||||
@@ -1035,8 +1036,8 @@ router.patch('/:id', async (req, res) => {
|
||||
`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, articleId, timeUnitVal, operatorId]
|
||||
) VALUES ($1, $2, $3, $4, $5, $4, $5)`,
|
||||
[id, articleId, timeUnitVal, localNow, operatorId]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1733,44 +1734,60 @@ router.post('/merge', async (req, res) => {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
const localNow = getLocalTimestamp();
|
||||
|
||||
// Fetch target ticket details
|
||||
const targetRes = await client.query('SELECT tn, title FROM ticket WHERE id = $1', [targetId]);
|
||||
const targetRes = await client.query(
|
||||
'SELECT tn, title, queue_id, user_id, ticket_priority_id, ticket_state_id, type_id FROM ticket WHERE id = $1',
|
||||
[targetId]
|
||||
);
|
||||
if (targetRes.rows.length === 0) {
|
||||
throw new Error(`Ticket di destinazione #${targetId} non trovato`);
|
||||
}
|
||||
const targetTn = targetRes.rows[0].tn;
|
||||
const targetTitle = targetRes.rows[0].title;
|
||||
const targetRow = targetRes.rows[0];
|
||||
|
||||
// Fetch history type IDs
|
||||
const htStateRes = await client.query(`SELECT id FROM ticket_history_type WHERE name = 'StateUpdate'`);
|
||||
const stateUpdateTypeId = htStateRes.rows.length > 0 ? htStateRes.rows[0].id : 1;
|
||||
|
||||
const htNoteRes = await client.query(`SELECT id FROM ticket_history_type WHERE name = 'AddNote'`);
|
||||
const addNoteTypeId = htNoteRes.rows.length > 0 ? htNoteRes.rows[0].id : 1;
|
||||
|
||||
for (const sourceId of sourceIds) {
|
||||
// Fetch source ticket details
|
||||
const sourceRes = await client.query('SELECT tn, title FROM ticket WHERE id = $1', [sourceId]);
|
||||
const sourceRes = await client.query(
|
||||
'SELECT tn, title, queue_id, user_id, ticket_priority_id, type_id FROM ticket WHERE id = $1',
|
||||
[sourceId]
|
||||
);
|
||||
if (sourceRes.rows.length === 0) continue;
|
||||
const sourceTn = sourceRes.rows[0].tn;
|
||||
const sourceTitle = sourceRes.rows[0].title;
|
||||
const sourceRow = sourceRes.rows[0];
|
||||
|
||||
// 1. Move all articles of source to target
|
||||
await client.query(
|
||||
`UPDATE article
|
||||
SET ticket_id = $1, change_time = NOW(), change_by = $2
|
||||
WHERE ticket_id = $3`,
|
||||
[targetId, operatorId, sourceId]
|
||||
SET ticket_id = $1, change_time = $2, change_by = $3
|
||||
WHERE ticket_id = $4`,
|
||||
[targetId, localNow, operatorId, sourceId]
|
||||
);
|
||||
|
||||
// 2. Move all time accounting entries
|
||||
await client.query(
|
||||
`UPDATE time_accounting
|
||||
SET ticket_id = $1, change_time = NOW(), change_by = $2
|
||||
WHERE ticket_id = $3`,
|
||||
[targetId, operatorId, sourceId]
|
||||
SET ticket_id = $1, change_time = $2, change_by = $3
|
||||
WHERE ticket_id = $4`,
|
||||
[targetId, localNow, operatorId, sourceId]
|
||||
);
|
||||
|
||||
// 3. Mark source ticket as merged (state_id = 9)
|
||||
await client.query(
|
||||
`UPDATE ticket
|
||||
SET ticket_state_id = 9, change_time = NOW(), change_by = $1
|
||||
WHERE id = $2`,
|
||||
[operatorId, sourceId]
|
||||
SET ticket_state_id = 9, change_time = $1, change_by = $2
|
||||
WHERE id = $3`,
|
||||
[localNow, operatorId, sourceId]
|
||||
);
|
||||
|
||||
// 4. Create ParentChild relation link (source B is child/merged of target A)
|
||||
@@ -1786,8 +1803,8 @@ router.post('/merge', async (req, res) => {
|
||||
`INSERT INTO link_relation (
|
||||
source_object_id, source_key, target_object_id, target_key,
|
||||
type_id, state_id, create_time, create_by
|
||||
) VALUES (1, $1, 1, $2, 2, 1, NOW(), $3)`,
|
||||
[String(sourceId), String(targetId), operatorId]
|
||||
) VALUES (1, $1, 1, $2, 2, 1, $3, $4)`,
|
||||
[String(sourceId), String(targetId), localNow, operatorId]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1801,8 +1818,8 @@ router.post('/merge', async (req, res) => {
|
||||
ticket_id, article_sender_type_id, communication_channel_id,
|
||||
is_visible_for_customer, search_index_needs_rebuild,
|
||||
create_time, create_by, change_time, change_by
|
||||
) VALUES ($1, 1, 1, 0, 1, NOW(), $2, NOW(), $2) RETURNING id`,
|
||||
[targetId, operatorId]
|
||||
) VALUES ($1, 1, 1, 0, 1, $2, $3, $2, $3) RETURNING id`,
|
||||
[targetId, localNow, operatorId]
|
||||
);
|
||||
const articleId = artResult.rows[0].id;
|
||||
|
||||
@@ -1818,9 +1835,9 @@ router.post('/merge', async (req, res) => {
|
||||
$1, 'Sistema OTRS Turbo', '', '', '', '', 'Ticket Unito', $2,
|
||||
'', '', '',
|
||||
'text/plain; charset=utf-8', EXTRACT(EPOCH FROM NOW())::INTEGER, $3,
|
||||
NOW(), $4, NOW(), $4
|
||||
$4, $5, $4, $5
|
||||
)`,
|
||||
[articleId, mergeNoteBody, contentPath, operatorId]
|
||||
[articleId, mergeNoteBody, contentPath, localNow, operatorId]
|
||||
);
|
||||
|
||||
// Create internal system note inside source ticket B (the merged ticket)
|
||||
@@ -1829,8 +1846,8 @@ router.post('/merge', async (req, res) => {
|
||||
ticket_id, article_sender_type_id, communication_channel_id,
|
||||
is_visible_for_customer, search_index_needs_rebuild,
|
||||
create_time, create_by, change_time, change_by
|
||||
) VALUES ($1, 1, 1, 0, 1, NOW(), $2, NOW(), $2) RETURNING id`,
|
||||
[sourceId, operatorId]
|
||||
) VALUES ($1, 1, 1, 0, 1, $2, $3, $2, $3) RETURNING id`,
|
||||
[sourceId, localNow, operatorId]
|
||||
);
|
||||
const sourceArticleId = sourceArtResult.rows[0].id;
|
||||
const sourceMergeNoteText = `Merged Ticket ${sourceTn} to ${targetTn}`;
|
||||
@@ -1845,12 +1862,54 @@ router.post('/merge', async (req, res) => {
|
||||
$1, 'Sistema OTRS Turbo', '', '', '', '', $2, $2,
|
||||
'', '', '',
|
||||
'text/plain; charset=utf-8', EXTRACT(EPOCH FROM NOW())::INTEGER, $3,
|
||||
NOW(), $4, NOW(), $4
|
||||
$4, $5, $4, $5
|
||||
)`,
|
||||
[sourceArticleId, sourceMergeNoteText, contentPath, operatorId]
|
||||
[sourceArticleId, sourceMergeNoteText, contentPath, localNow, operatorId]
|
||||
);
|
||||
|
||||
// 6. Record ticket history for source ticket (state changed to merged = 9)
|
||||
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 (
|
||||
'%%merged%%', $1, $2, $3, $4,
|
||||
$5, $6, 9,
|
||||
$7, $8, $7, $8
|
||||
)`,
|
||||
[
|
||||
stateUpdateTypeId, sourceId, sourceRow.type_id || 1, sourceRow.queue_id,
|
||||
sourceRow.user_id || 1, sourceRow.ticket_priority_id || 1,
|
||||
localNow, operatorId
|
||||
]
|
||||
);
|
||||
|
||||
// 7. Record ticket history for target ticket (note added for merge)
|
||||
await client.query(
|
||||
`INSERT INTO ticket_history (
|
||||
name, history_type_id, ticket_id, article_id, type_id, queue_id,
|
||||
owner_id, priority_id, state_id,
|
||||
create_time, create_by, change_time, change_by
|
||||
) VALUES (
|
||||
'%%', $1, $2, $3, $4, $5,
|
||||
$6, $7, $8,
|
||||
$9, $10, $9, $10
|
||||
)`,
|
||||
[
|
||||
addNoteTypeId, targetId, articleId, targetRow.type_id || 1, targetRow.queue_id,
|
||||
targetRow.user_id || 1, targetRow.ticket_priority_id || 1, targetRow.ticket_state_id || 1,
|
||||
localNow, operatorId
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
// Update target ticket change_time & change_by
|
||||
await client.query(
|
||||
`UPDATE ticket SET change_time = $1, change_by = $2 WHERE id = $3`,
|
||||
[localNow, operatorId, targetId]
|
||||
);
|
||||
|
||||
await client.query('COMMIT');
|
||||
resolveAgentName(operatorId).then(agente_nome => {
|
||||
logAttivita({
|
||||
@@ -2185,6 +2244,8 @@ router.post('/auto-time', async (req, res) => {
|
||||
const tn = `${datePrefix}${String(counter).padStart(counterPadding, '0')}`;
|
||||
|
||||
// 3. Create Ticket (lock = 1 [unlock], owner & responsible = operatorId)
|
||||
const localNow = getLocalTimestamp();
|
||||
|
||||
const ticketResult = await client.query(
|
||||
`INSERT INTO ticket (
|
||||
tn, title, queue_id, ticket_lock_id, type_id,
|
||||
@@ -2205,14 +2266,14 @@ router.post('/auto-time', async (req, res) => {
|
||||
0, 0,
|
||||
0, 0,
|
||||
0,
|
||||
NOW(), $10, NOW(), $10
|
||||
$10, $11, $10, $11
|
||||
) RETURNING id, tn`,
|
||||
[
|
||||
tn, title, queueId, typeId,
|
||||
operatorId,
|
||||
priorityId, stateId,
|
||||
customerId, customerUserId,
|
||||
operatorId
|
||||
localNow, operatorId
|
||||
]
|
||||
);
|
||||
const ticketId = ticketResult.rows[0].id;
|
||||
@@ -2227,8 +2288,8 @@ router.post('/auto-time', async (req, res) => {
|
||||
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, $3, $4, $5, $6, $7, $8, NOW(), $9, NOW(), $9)`,
|
||||
['%%', historyTypeId, ticketId, typeId || 1, queueId, operatorId, priorityId, stateId, operatorId]
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $9, $10)`,
|
||||
['%%', historyTypeId, ticketId, typeId || 1, queueId, operatorId, priorityId, stateId, localNow, operatorId]
|
||||
);
|
||||
|
||||
// 4. Create Article (Internal note)
|
||||
@@ -2247,8 +2308,8 @@ router.post('/auto-time', async (req, res) => {
|
||||
ticket_id, article_sender_type_id, communication_channel_id,
|
||||
is_visible_for_customer, search_index_needs_rebuild,
|
||||
create_time, create_by, change_time, change_by
|
||||
) VALUES ($1, $2, $3, 0, 1, NOW(), $4, NOW(), $4) RETURNING id`,
|
||||
[ticketId, senderTypeId, channelId, operatorId]
|
||||
) VALUES ($1, $2, $3, 0, 1, $4, $5, $4, $5) RETURNING id`,
|
||||
[ticketId, senderTypeId, channelId, localNow, operatorId]
|
||||
);
|
||||
const articleId = articleResult.rows[0].id;
|
||||
|
||||
@@ -2269,9 +2330,9 @@ router.post('/auto-time', async (req, res) => {
|
||||
) VALUES (
|
||||
$1, $2, '', $3, $4,
|
||||
'text/html; charset=utf-8', EXTRACT(EPOCH FROM NOW())::INTEGER, $5,
|
||||
NOW(), $6, NOW(), $6
|
||||
$6, $7, $6, $7
|
||||
)`,
|
||||
[articleId, fromHeader, subject, htmlBody, contentPath, operatorId]
|
||||
[articleId, fromHeader, subject, htmlBody, contentPath, localNow, operatorId]
|
||||
);
|
||||
|
||||
// Create article attachment (file-1) for OTRS CE HTML display
|
||||
@@ -2279,8 +2340,8 @@ router.post('/auto-time', async (req, res) => {
|
||||
`INSERT INTO article_data_mime_attachment (
|
||||
article_id, filename, content_size, content_type, disposition, content,
|
||||
create_time, create_by, change_time, change_by
|
||||
) VALUES ($1, 'file-1', $2, 'text/html; charset="utf-8"', '', $3, NOW(), $4, NOW(), $4)`,
|
||||
[articleId, String(contentSize), base64Body, operatorId]
|
||||
) VALUES ($1, 'file-1', $2, 'text/html; charset="utf-8"', '', $3, $4, $5, $4, $5)`,
|
||||
[articleId, String(contentSize), base64Body, localNow, operatorId]
|
||||
);
|
||||
|
||||
// 5. Create Time Accounting Entry for remaining minutes
|
||||
@@ -2288,8 +2349,8 @@ router.post('/auto-time', async (req, res) => {
|
||||
`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)`,
|
||||
[ticketId, articleId, remaining, operatorId]
|
||||
) VALUES ($1, $2, $3, $4, $5, $4, $5)`,
|
||||
[ticketId, articleId, remaining, localNow, operatorId]
|
||||
);
|
||||
|
||||
// 6. Close tickets in default group 'CHIUDI A FINE GIORNATA'
|
||||
@@ -2303,8 +2364,8 @@ router.post('/auto-time', async (req, res) => {
|
||||
|
||||
// Update ticket state to closed in OTRS DB
|
||||
await client.query(
|
||||
`UPDATE ticket SET ticket_state_id = $${groupTicketIds.length + 1}, change_time = NOW(), change_by = $${groupTicketIds.length + 2} WHERE id IN (${idPlaceholders})`,
|
||||
[...groupTicketIds, stateId, operatorId]
|
||||
`UPDATE ticket SET ticket_state_id = $${groupTicketIds.length + 1}, change_time = $${groupTicketIds.length + 2}, change_by = $${groupTicketIds.length + 3} WHERE id IN (${idPlaceholders})`,
|
||||
[...groupTicketIds, stateId, localNow, operatorId]
|
||||
);
|
||||
|
||||
console.log(`[Auto-Time] Closed ${groupTicketIds.length} tickets from group 'CHIUDI A FINE GIORNATA'`);
|
||||
|
||||
Reference in new issue
Block a user