style: riviste parti dell'interfaccia dei ticket. feat: aggiunta possiblità di modificare il tempo inserito nelle note ed aggiunta voce per tempo nel menu azioni rapide
This commit is contained in:
+22
-14
@@ -168,22 +168,30 @@ router.get('/lock-types', async (req, res) => {
|
||||
// GET /api/customer-companies/search — Search customer companies
|
||||
router.get('/customer-companies/search', async (req, res) => {
|
||||
try {
|
||||
const { q } = req.query;
|
||||
const { q = '' } = req.query;
|
||||
let result;
|
||||
if (!q) {
|
||||
return res.json([]);
|
||||
result = await pool.query(
|
||||
`SELECT customer_id, name
|
||||
FROM customer_company
|
||||
WHERE valid_id = 1
|
||||
ORDER BY name
|
||||
LIMIT 20`
|
||||
);
|
||||
} else {
|
||||
const searchTerm = `%${q}%`;
|
||||
result = await pool.query(
|
||||
`SELECT customer_id, name
|
||||
FROM customer_company
|
||||
WHERE valid_id = 1 AND (
|
||||
customer_id ILIKE $1 OR
|
||||
name ILIKE $1
|
||||
)
|
||||
ORDER BY name
|
||||
LIMIT 20`,
|
||||
[searchTerm]
|
||||
);
|
||||
}
|
||||
const searchTerm = `%${q}%`;
|
||||
const result = await pool.query(
|
||||
`SELECT customer_id, name
|
||||
FROM customer_company
|
||||
WHERE valid_id = 1 AND (
|
||||
customer_id ILIKE $1 OR
|
||||
name ILIKE $1
|
||||
)
|
||||
ORDER BY name
|
||||
LIMIT 20`,
|
||||
[searchTerm]
|
||||
);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
console.error('Error searching customer companies:', err);
|
||||
|
||||
+323
-41
@@ -66,8 +66,21 @@ router.get('/', async (req, res) => {
|
||||
params.push(parseInt(queue_id));
|
||||
}
|
||||
if (state_id) {
|
||||
conditions.push(`t.ticket_state_id = $${paramIdx++}`);
|
||||
params.push(parseInt(state_id));
|
||||
let stateIds = [];
|
||||
if (Array.isArray(state_id)) {
|
||||
stateIds = state_id.map(id => parseInt(id)).filter(id => !isNaN(id));
|
||||
} else if (typeof state_id === 'string') {
|
||||
stateIds = state_id.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
|
||||
} else {
|
||||
const parsed = parseInt(state_id);
|
||||
if (!isNaN(parsed)) stateIds.push(parsed);
|
||||
}
|
||||
|
||||
if (stateIds.length > 0) {
|
||||
const placeholders = stateIds.map(() => `$${paramIdx++}`).join(', ');
|
||||
conditions.push(`t.ticket_state_id IN (${placeholders})`);
|
||||
params.push(...stateIds);
|
||||
}
|
||||
}
|
||||
if (priority_id) {
|
||||
conditions.push(`t.ticket_priority_id = $${paramIdx++}`);
|
||||
@@ -162,12 +175,22 @@ router.get('/', async (req, res) => {
|
||||
[...params, parseInt(per_page), offset]
|
||||
);
|
||||
|
||||
let otrsBaseUrl = null;
|
||||
const apiUrl = process.env.OTRS_API_URL;
|
||||
if (apiUrl) {
|
||||
const idx = apiUrl.indexOf('/otrs/');
|
||||
if (idx !== -1) {
|
||||
otrsBaseUrl = apiUrl.substring(0, idx + 6);
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
tickets: result.rows,
|
||||
total,
|
||||
page: parseInt(page),
|
||||
per_page: parseInt(per_page),
|
||||
total_pages: Math.ceil(total / parseInt(per_page)),
|
||||
otrsBaseUrl,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error fetching tickets:', err);
|
||||
@@ -252,10 +275,21 @@ router.get('/:id', async (req, res) => {
|
||||
[id]
|
||||
);
|
||||
|
||||
let otrsWebUrl = null;
|
||||
const apiUrl = process.env.OTRS_API_URL;
|
||||
if (apiUrl) {
|
||||
const idx = apiUrl.indexOf('/otrs/');
|
||||
if (idx !== -1) {
|
||||
const base = apiUrl.substring(0, idx + 6);
|
||||
otrsWebUrl = `${base}index.pl?Action=AgentTicketZoom;TicketID=${id}`;
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
ticket: ticketResult.rows[0],
|
||||
articles: articlesResult.rows,
|
||||
attachments: attachmentsResult.rows,
|
||||
otrsWebUrl,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error fetching ticket detail:', err);
|
||||
@@ -387,7 +421,13 @@ router.post('/', async (req, res) => {
|
||||
);
|
||||
|
||||
// Create initial article if body or attachments are provided
|
||||
if (body || (attachments && attachments.length > 0)) {
|
||||
let finalBody = body;
|
||||
const isBodyEmpty = !finalBody || finalBody.trim() === '' || finalBody.trim() === '<p><br></p>';
|
||||
if (isBodyEmpty) {
|
||||
finalBody = subject || title || 'Nuovo ticket';
|
||||
}
|
||||
|
||||
if (finalBody || (attachments && attachments.length > 0)) {
|
||||
// Determine sender type (customer vs agent) and sender name/email
|
||||
let senderTypeName = 'agent';
|
||||
let customerFrom = 'OTRS Turbo Agent';
|
||||
@@ -429,8 +469,7 @@ router.post('/', async (req, res) => {
|
||||
);
|
||||
|
||||
const articleId = articleResult.rows[0].id;
|
||||
const finalBody = body || 'File allegati in creazione';
|
||||
const isHtml = body && /<[a-z][\s\S]*>/i.test(body);
|
||||
const isHtml = finalBody && /<[a-z][\s\S]*>/i.test(finalBody);
|
||||
const contentType = isHtml ? 'text/html; charset=utf-8' : 'text/plain; charset=utf-8';
|
||||
|
||||
await client.query(
|
||||
@@ -446,6 +485,25 @@ router.post('/', async (req, res) => {
|
||||
[articleId, customerFrom, subject || title, finalBody, contentType, operatorId]
|
||||
);
|
||||
|
||||
// If HTML content, create article_data_mime_attachment for OTRS CE HTML rendering (file-1)
|
||||
if (isHtml) {
|
||||
const htmlBody = `<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/></head><body style="font-family:Geneva,Helvetica,Arial,sans-serif; font-size: 12px;">${finalBody}</body></html>`;
|
||||
const binaryBody = Buffer.from(htmlBody, 'utf-8');
|
||||
const contentSize = Buffer.byteLength(htmlBody, 'utf-8');
|
||||
const base64Body = binaryBody.toString('base64');
|
||||
|
||||
await client.query(
|
||||
`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]
|
||||
);
|
||||
}
|
||||
|
||||
// Insert attachments if any
|
||||
if (attachments && Array.isArray(attachments)) {
|
||||
for (const att of attachments) {
|
||||
@@ -460,7 +518,7 @@ router.post('/', async (req, res) => {
|
||||
att.filename,
|
||||
contentBuffer.length,
|
||||
att.content_type || 'application/octet-stream',
|
||||
contentBuffer,
|
||||
att.content, // OTRS CE expects base64 string directly
|
||||
operatorId
|
||||
]
|
||||
);
|
||||
@@ -491,6 +549,24 @@ router.patch('/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const updates = req.body; // { ticket_state_id, ticket_priority_id, queue_id, user_id, type_id, title }
|
||||
const operatorId = parseInt(req.headers['x-agent-id'], 10) || 1;
|
||||
const timeUnit = parseFloat(updates.time_unit);
|
||||
|
||||
// Fetch current ticket first (needed for both REST dummy fields and database fallback)
|
||||
let current;
|
||||
try {
|
||||
const currentResult = await pool.query(
|
||||
`SELECT ticket_state_id, ticket_priority_id, queue_id, user_id, type_id, title, ticket_lock_id, customer_id, customer_user_id
|
||||
FROM ticket WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
if (currentResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Ticket non trovato' });
|
||||
}
|
||||
current = currentResult.rows[0];
|
||||
} catch (dbErr) {
|
||||
console.error('Error fetching current ticket:', dbErr);
|
||||
return res.status(500).json({ error: dbErr.message });
|
||||
}
|
||||
|
||||
// 1. Try to update via REST API if configured
|
||||
if (process.env.OTRS_API_URL && process.env.OTRS_API_USER) {
|
||||
@@ -506,6 +582,11 @@ router.patch('/:id', async (req, res) => {
|
||||
if (updates.customer_id !== undefined) ticketFields.CustomerID = updates.customer_id;
|
||||
if (updates.customer_user_id !== undefined) ticketFields.CustomerUser = updates.customer_user_id;
|
||||
|
||||
// If ticketFields is empty but we have a timeUnit to log, we must supply a dummy field (e.g. StateID) so the Ticket parameter is not empty
|
||||
if (Object.keys(ticketFields).length === 0 && !isNaN(timeUnit) && timeUnit > 0) {
|
||||
ticketFields.StateID = current.ticket_state_id;
|
||||
}
|
||||
|
||||
// Auto sblocco check
|
||||
if (updates.ticket_state_id) {
|
||||
const stateTypeRes = await pool.query(
|
||||
@@ -523,10 +604,22 @@ router.patch('/:id', async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(ticketFields).length > 0) {
|
||||
const result = await otrsRequest('PATCH', `/Ticket/${id}`, {
|
||||
if (Object.keys(ticketFields).length > 0 || (!isNaN(timeUnit) && timeUnit > 0)) {
|
||||
const reqBody = {
|
||||
Ticket: ticketFields
|
||||
});
|
||||
};
|
||||
if (!isNaN(timeUnit) && timeUnit > 0) {
|
||||
reqBody.Article = {
|
||||
CommunicationChannel: 'Internal',
|
||||
SenderType: 'agent',
|
||||
Subject: 'Consuntivazione',
|
||||
Body: 'Consuntivazione',
|
||||
ContentType: 'text/html; charset=utf8',
|
||||
TimeUnit: timeUnit,
|
||||
TimeUnits: timeUnit
|
||||
};
|
||||
}
|
||||
const result = await otrsRequest('PATCH', `/Ticket/${id}`, reqBody);
|
||||
return res.json({ message: 'Ticket aggiornato! (via API REST)', result });
|
||||
}
|
||||
return res.json({ message: 'Nessuna modifica rilevata' });
|
||||
@@ -539,24 +632,8 @@ router.patch('/:id', async (req, res) => {
|
||||
// 2. Direct database update fallback
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updates = req.body; // { ticket_state_id, ticket_priority_id, queue_id, user_id, type_id, title }
|
||||
const operatorId = parseInt(req.headers['x-agent-id'], 10) || 1;
|
||||
|
||||
await client.query('BEGIN');
|
||||
|
||||
// Fetch current ticket for history comparison
|
||||
const currentResult = await client.query(
|
||||
`SELECT ticket_state_id, ticket_priority_id, queue_id, user_id, type_id, title, ticket_lock_id, customer_id, customer_user_id
|
||||
FROM ticket WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
if (currentResult.rows.length === 0) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(404).json({ error: 'Ticket not found' });
|
||||
}
|
||||
const current = currentResult.rows[0];
|
||||
|
||||
// If state is changing, check if the target state is a closed state type to auto-unlock
|
||||
if (updates.ticket_state_id && updates.ticket_state_id !== current.ticket_state_id) {
|
||||
const stateTypeRes = await client.query(
|
||||
@@ -587,21 +664,32 @@ router.patch('/:id', async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (setClauses.length === 0) {
|
||||
const timeUnitVal = parseFloat(updates.time_unit);
|
||||
const hasTimeUnit = !isNaN(timeUnitVal) && timeUnitVal > 0;
|
||||
|
||||
if (setClauses.length === 0 && !hasTimeUnit) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.json({ message: 'No changes detected' });
|
||||
}
|
||||
|
||||
// Always update change_time and change_by
|
||||
setClauses.push(`change_time = NOW()`);
|
||||
setClauses.push(`change_by = $${pIdx++}`);
|
||||
setParams.push(operatorId);
|
||||
setParams.push(parseInt(id));
|
||||
if (setClauses.length > 0) {
|
||||
// Always update change_time and change_by
|
||||
setClauses.push(`change_time = NOW()`);
|
||||
setClauses.push(`change_by = $${pIdx++}`);
|
||||
setParams.push(operatorId);
|
||||
setParams.push(parseInt(id));
|
||||
|
||||
await client.query(
|
||||
`UPDATE ticket SET ${setClauses.join(', ')} WHERE id = $${pIdx}`,
|
||||
setParams
|
||||
);
|
||||
await client.query(
|
||||
`UPDATE ticket SET ${setClauses.join(', ')} WHERE id = $${pIdx}`,
|
||||
setParams
|
||||
);
|
||||
} else {
|
||||
// If there are no fields modified but we have a 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]
|
||||
);
|
||||
}
|
||||
|
||||
// Record history entries for each changed field
|
||||
const historyTypeMap = {
|
||||
@@ -657,6 +745,79 @@ router.patch('/:id', async (req, res) => {
|
||||
}
|
||||
}
|
||||
|
||||
// If time_unit is provided, insert a system note article and log the time
|
||||
if (hasTimeUnit) {
|
||||
// Get sender type for "agent"
|
||||
const senderResult = await client.query(
|
||||
`SELECT id FROM article_sender_type WHERE name = 'agent'`
|
||||
);
|
||||
const senderTypeId = senderResult.rows.length > 0 ? senderResult.rows[0].id : 1;
|
||||
|
||||
// Get channel for "Internal"
|
||||
const channelResult = await client.query(
|
||||
`SELECT id FROM communication_channel WHERE name = 'Internal'`
|
||||
);
|
||||
const channelId = channelResult.rows.length > 0 ? channelResult.rows[0].id : 1;
|
||||
|
||||
// Create article
|
||||
const articleResult = await client.query(
|
||||
`INSERT INTO article (
|
||||
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`,
|
||||
[id, senderTypeId, channelId, operatorId]
|
||||
);
|
||||
|
||||
const articleId = articleResult.rows[0].id;
|
||||
const now = new Date();
|
||||
const contentPath = `${now.getFullYear()}/${String(now.getMonth() + 1).padStart(2, '0')}/${String(now.getDate()).padStart(2, '0')}`;
|
||||
const noteBody = 'Consuntivazione';
|
||||
const htmlBody = `<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/></head><body style="font-family:Geneva,Helvetica,Arial,sans-serif; font-size: 12px;">${noteBody}</body></html>`;
|
||||
const binaryBody = Buffer.from(htmlBody, 'utf-8');
|
||||
const contentSize = Buffer.byteLength(htmlBody, 'utf-8');
|
||||
const base64Body = binaryBody.toString('base64');
|
||||
|
||||
// Create article_data_mime
|
||||
await client.query(
|
||||
`INSERT INTO article_data_mime (
|
||||
article_id, a_from, a_to, a_reply_to, a_cc, a_bcc, a_subject, a_body,
|
||||
a_message_id, a_in_reply_to, a_references,
|
||||
a_content_type, incoming_time, content_path,
|
||||
create_time, create_by, change_time, change_by
|
||||
) VALUES (
|
||||
$1, $2, '', '', '', '', $3, $4,
|
||||
'', '', '',
|
||||
'text/html; charset=utf-8', EXTRACT(EPOCH FROM NOW())::INTEGER, $5,
|
||||
NOW(), $6, NOW(), $6
|
||||
)`,
|
||||
[articleId, 'OTRS Turbo Agent', 'Consuntivazione', htmlBody, contentPath, operatorId]
|
||||
);
|
||||
|
||||
// Create article_data_mime_attachment for OTRS CE HTML rendering
|
||||
await client.query(
|
||||
`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]
|
||||
);
|
||||
|
||||
// Log the time in time_accounting
|
||||
await client.query(
|
||||
`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]
|
||||
);
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
res.json({ message: 'Ticket aggiornato! (via DB)' });
|
||||
} catch (err) {
|
||||
@@ -707,7 +868,7 @@ router.get('/:id/articles', async (req, res) => {
|
||||
// ============================================================
|
||||
router.post('/:id/articles', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { subject, body, is_visible_for_customer = 0, time_unit } = req.body;
|
||||
const { subject, body, is_visible_for_customer = 0, time_unit, attachments } = req.body;
|
||||
const operatorId = parseInt(req.headers['x-agent-id'], 10) || 1;
|
||||
|
||||
const isHtml = body && /<[a-z][\s\S]*>/i.test(body);
|
||||
@@ -729,6 +890,15 @@ router.post('/:id/articles', async (req, res) => {
|
||||
|
||||
if (time_unit) {
|
||||
payload.Article.TimeUnit = parseFloat(time_unit);
|
||||
payload.Article.TimeUnits = parseFloat(time_unit);
|
||||
}
|
||||
|
||||
if (attachments && Array.isArray(attachments)) {
|
||||
payload.Article.Attachment = attachments.map(att => ({
|
||||
Content: att.content,
|
||||
ContentType: att.content_type || 'application/octet-stream',
|
||||
Filename: att.filename
|
||||
}));
|
||||
}
|
||||
|
||||
const result = await otrsRequest('PATCH', `/Ticket/${id}`, payload);
|
||||
@@ -747,7 +917,7 @@ router.post('/:id/articles', async (req, res) => {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { subject, body, is_visible_for_customer = 0, time_unit } = req.body;
|
||||
const { subject, body, is_visible_for_customer = 0, time_unit, attachments } = req.body;
|
||||
const operatorId = parseInt(req.headers['x-agent-id'], 10) || 1;
|
||||
|
||||
await client.query('BEGIN');
|
||||
@@ -805,10 +975,11 @@ router.post('/:id/articles', async (req, res) => {
|
||||
[articleId, 'OTRS Turbo Agent', subject || 'Nota interna', body, contentType, contentPath, operatorId]
|
||||
);
|
||||
|
||||
// Create article_data_mime_attachment for OTRS CE HTML rendering (using file-1 and raw Buffer)
|
||||
// Create article_data_mime_attachment for OTRS CE HTML rendering (using file-1 and base64 encoded text)
|
||||
const htmlBody = `<!DOCTYPE html><html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/></head><body style="font-family:Geneva,Helvetica,Arial,sans-serif; font-size: 12px;">${body}</body></html>`;
|
||||
const binaryBody = Buffer.from(htmlBody, 'utf-8');
|
||||
const contentSize = Buffer.byteLength(htmlBody, 'utf-8');
|
||||
const base64Body = binaryBody.toString('base64');
|
||||
|
||||
await client.query(
|
||||
`INSERT INTO article_data_mime_attachment (
|
||||
@@ -818,9 +989,30 @@ router.post('/:id/articles', async (req, res) => {
|
||||
$1, 'file-1', $2, 'text/html; charset="utf-8"', '', $3,
|
||||
NOW(), $4, NOW(), $4
|
||||
)`,
|
||||
[articleId, String(contentSize), binaryBody, operatorId]
|
||||
[articleId, String(contentSize), base64Body, operatorId]
|
||||
);
|
||||
|
||||
// Insert additional attachments if any
|
||||
if (attachments && Array.isArray(attachments)) {
|
||||
for (const att of attachments) {
|
||||
const contentBuffer = Buffer.from(att.content, 'base64');
|
||||
await client.query(
|
||||
`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, $2, $3, $4, 'attachment', $5, NOW(), $6, NOW(), $6)`,
|
||||
[
|
||||
articleId,
|
||||
att.filename,
|
||||
contentBuffer.length,
|
||||
att.content_type || 'application/octet-stream',
|
||||
att.content, // base64 string directly
|
||||
operatorId
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If time_unit is provided, insert into time_accounting
|
||||
if (time_unit !== undefined && time_unit !== null && time_unit !== '') {
|
||||
const parsedTime = parseFloat(time_unit);
|
||||
@@ -1051,9 +1243,35 @@ router.get('/attachments/:id', async (req, res) => {
|
||||
}
|
||||
const attachment = result.rows[0];
|
||||
|
||||
// Decode base64 if necessary
|
||||
let contentBuffer;
|
||||
if (attachment.content) {
|
||||
let contentStr = '';
|
||||
if (Buffer.isBuffer(attachment.content)) {
|
||||
contentStr = attachment.content.toString('utf-8');
|
||||
} else if (typeof attachment.content === 'string') {
|
||||
contentStr = attachment.content;
|
||||
}
|
||||
|
||||
const cleaned = contentStr.replace(/\s+/g, '');
|
||||
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/;
|
||||
if (cleaned.length % 4 === 0 && base64Regex.test(cleaned)) {
|
||||
try {
|
||||
contentBuffer = Buffer.from(cleaned, 'base64');
|
||||
} catch (e) {
|
||||
contentBuffer = Buffer.isBuffer(attachment.content) ? attachment.content : Buffer.from(attachment.content);
|
||||
}
|
||||
} else {
|
||||
contentBuffer = Buffer.isBuffer(attachment.content) ? attachment.content : Buffer.from(attachment.content);
|
||||
}
|
||||
} else {
|
||||
contentBuffer = Buffer.alloc(0);
|
||||
}
|
||||
|
||||
const safeFilename = attachment.filename || 'attachment';
|
||||
res.setHeader('Content-Type', attachment.content_type || 'application/octet-stream');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${attachment.filename}"`);
|
||||
res.send(attachment.content);
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(safeFilename)}"; filename*=UTF-8''${encodeURIComponent(safeFilename)}`);
|
||||
res.send(contentBuffer);
|
||||
} catch (err) {
|
||||
console.error('Errore download allegato:', err);
|
||||
res.status(500).send(err.message);
|
||||
@@ -1174,4 +1392,68 @@ router.post('/merge', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ============================================================
|
||||
// PUT /api/articles/:articleId/time — Update time accounted for an article
|
||||
// ============================================================
|
||||
router.put('/articles/:articleId/time', async (req, res) => {
|
||||
const { articleId } = req.params;
|
||||
const { time_unit } = req.body;
|
||||
const operatorId = parseInt(req.headers['x-agent-id'], 10) || 1;
|
||||
const parsedTime = parseFloat(time_unit);
|
||||
|
||||
if (isNaN(parsedTime) || parsedTime < 0) {
|
||||
return res.status(400).json({ error: 'Il tempo specificato deve essere un numero maggiore o uguale a 0' });
|
||||
}
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// 1. Check if a record already exists in time_accounting
|
||||
const existing = await client.query(
|
||||
`SELECT id FROM time_accounting WHERE article_id = $1`,
|
||||
[articleId]
|
||||
);
|
||||
|
||||
if (existing.rows.length > 0) {
|
||||
// Update
|
||||
await client.query(
|
||||
`UPDATE time_accounting
|
||||
SET time_unit = $1, change_time = NOW(), change_by = $2
|
||||
WHERE article_id = $3`,
|
||||
[parsedTime, operatorId, articleId]
|
||||
);
|
||||
} else {
|
||||
// Find ticket_id from article
|
||||
const artRes = await client.query(
|
||||
`SELECT ticket_id FROM article WHERE id = $1`,
|
||||
[articleId]
|
||||
);
|
||||
if (artRes.rows.length === 0) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(404).json({ error: 'Articolo non trovato' });
|
||||
}
|
||||
const ticketId = artRes.rows[0].ticket_id;
|
||||
|
||||
// Insert
|
||||
await client.query(
|
||||
`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, parsedTime, operatorId]
|
||||
);
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
res.json({ message: 'Tempo aggiornato con successo!' });
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('Error updating article time:', err);
|
||||
res.status(500).json({ error: err.message });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user