From ba23635c7d19e4951aa89a3acabf66b358d19f5b Mon Sep 17 00:00:00 2001 From: Gabriele Cimaschi Date: Thu, 20 Aug 2026 00:05:34 +0200 Subject: [PATCH] feat: aggiunto campo di testo per nota nell'area della modifica rapida di un ticket (nota rapida) --- public/css/style.css | 25 ++++++++++++- public/js/views/ticketDetail.js | 27 +++++++++++++- routes/tickets.js | 63 +++++++++++++++++++-------------- 3 files changed, 87 insertions(+), 28 deletions(-) diff --git a/public/css/style.css b/public/css/style.css index c952a9f..1b094f9 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -1355,7 +1355,30 @@ body { box-shadow: 0 0 0 3px var(--accent-primary-glow); } -.quick-edit-select.changed { +.quick-edit-field.full-width { + grid-column: 1 / -1; +} + +.quick-edit-textarea { + padding: 8px 12px; + background: var(--bg-tertiary); + border: 1px solid var(--border-subtle); + border-radius: var(--radius-md); + color: var(--text-primary); + font-family: inherit; + font-size: 0.85rem; + outline: none; + resize: vertical; + min-height: 60px; + transition: all var(--transition-fast); +} + +.quick-edit-textarea:focus { + border-color: var(--accent-primary); + box-shadow: 0 0 0 3px var(--accent-primary-glow); +} + +.quick-edit-textarea.changed { border-color: var(--success); box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.15); } diff --git a/public/js/views/ticketDetail.js b/public/js/views/ticketDetail.js index bfd0394..26749d5 100644 --- a/public/js/views/ticketDetail.js +++ b/public/js/views/ticketDetail.js @@ -238,7 +238,11 @@ const TicketDetailView = { -
+
+ + +
+
@@ -588,6 +592,7 @@ const TicketDetailView = { const saveBtn = document.getElementById('qe-save'); const resetBtn = document.getElementById('qe-reset'); const timeUnitInput = document.getElementById('qe-time-unit'); + const noteInput = document.getElementById('qe-note'); const checkChanges = () => { let hasChanges = false; @@ -610,6 +615,14 @@ const TicketDetailView = { hasChanges = true; } + const noteText = noteInput ? noteInput.value.trim() : ''; + if (noteInput) { + noteInput.classList.toggle('changed', noteText.length > 0); + } + if (noteText.length > 0) { + hasChanges = true; + } + saveBtn.disabled = !hasChanges; }; @@ -618,6 +631,10 @@ const TicketDetailView = { timeUnitInput.addEventListener('input', checkChanges); timeUnitInput.addEventListener('change', checkChanges); } + if (noteInput) { + noteInput.addEventListener('input', checkChanges); + noteInput.addEventListener('change', checkChanges); + } // Queue Autocomplete inside Quick Edit const queueSearchInput = document.getElementById('qe-queue-search'); @@ -763,6 +780,10 @@ const TicketDetailView = { if (timeUnitInput) { timeUnitInput.value = ''; } + if (noteInput) { + noteInput.value = ''; + noteInput.classList.remove('changed'); + } saveBtn.disabled = true; }); @@ -803,6 +824,10 @@ const TicketDetailView = { } } + if (noteInput && noteInput.value.trim()) { + updates.note = noteInput.value.trim(); + } + if (Object.keys(updates).length === 0) return; try { diff --git a/routes/tickets.js b/routes/tickets.js index c2be5d9..d0851a2 100644 --- a/routes/tickets.js +++ b/routes/tickets.js @@ -744,9 +744,11 @@ router.post('/', async (req, res) => { // ============================================================ 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 updates = req.body; // { ticket_state_id, ticket_priority_id, queue_id, user_id, type_id, title, note } const operatorId = parseInt(req.headers['x-agent-id'], 10) || 1; const timeUnit = parseFloat(updates.time_unit); + const noteText = typeof updates.note === 'string' ? updates.note.trim() : (typeof updates.note_text === 'string' ? updates.note_text.trim() : ''); + const hasNote = noteText.length > 0; // Fetch current ticket first (needed for both REST dummy fields and database fallback) let current; @@ -780,8 +782,8 @@ 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) { + // If ticketFields is empty but we have a note or timeUnit to log, supply a dummy field (StateID) so Ticket is not empty + if (Object.keys(ticketFields).length === 0 && (hasNote || (!isNaN(timeUnit) && timeUnit > 0))) { ticketFields.StateID = current.ticket_state_id; } @@ -802,20 +804,25 @@ router.patch('/:id', async (req, res) => { } } - if (Object.keys(ticketFields).length > 0 || (!isNaN(timeUnit) && timeUnit > 0)) { + if (Object.keys(ticketFields).length > 0 || hasNote || (!isNaN(timeUnit) && timeUnit > 0)) { const reqBody = { Ticket: ticketFields }; - if (!isNaN(timeUnit) && timeUnit > 0) { - reqBody.Article = { + if (hasNote || (!isNaN(timeUnit) && timeUnit > 0)) { + const noteSubject = hasNote ? noteText : 'Consuntivazione'; + const noteBody = hasNote ? noteText : 'Consuntivazione'; + const articleObj = { CommunicationChannel: 'Internal', SenderType: 'agent', - Subject: 'Consuntivazione', - Body: 'Consuntivazione', - ContentType: 'text/html; charset=utf8', - TimeUnit: timeUnit, - TimeUnits: timeUnit + Subject: noteSubject, + Body: noteBody, + ContentType: 'text/html; charset=utf8' }; + if (!isNaN(timeUnit) && timeUnit > 0) { + articleObj.TimeUnit = timeUnit; + articleObj.TimeUnits = timeUnit; + } + reqBody.Article = articleObj; } const result = await otrsRequest('PATCH', `/Ticket/${id}`, reqBody); @@ -876,7 +883,7 @@ router.patch('/:id', async (req, res) => { const timeUnitVal = parseFloat(updates.time_unit); const hasTimeUnit = !isNaN(timeUnitVal) && timeUnitVal > 0; - if (setClauses.length === 0 && !hasTimeUnit) { + if (setClauses.length === 0 && !hasTimeUnit && !hasNote) { await client.query('ROLLBACK'); return res.json({ message: 'No changes detected' }); } @@ -893,7 +900,7 @@ router.patch('/:id', async (req, res) => { setParams ); } else { - // If there are no fields modified but we have a time_unit, we still update change_time/change_by + // 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] @@ -955,8 +962,8 @@ router.patch('/:id', async (req, res) => { } } - // If time_unit is provided, insert a system note article and log the time - if (hasTimeUnit) { + // If note or time_unit is provided, insert a system note article and log the time + if (hasNote || hasTimeUnit) { // Get sender type for "agent" const senderResult = await client.query( `SELECT id FROM article_sender_type WHERE name = 'agent'` @@ -984,8 +991,10 @@ router.patch('/:id', async (req, res) => { 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 = `${noteBody}`; + const noteSubject = hasNote ? noteText : 'Consuntivazione'; + const noteBodyRaw = hasNote ? noteText : 'Consuntivazione'; + const safeBody = noteBodyRaw.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '
'); + const htmlBody = `${safeBody}`; const binaryBody = Buffer.from(htmlBody, 'utf-8'); const contentSize = Buffer.byteLength(htmlBody, 'utf-8'); const base64Body = binaryBody.toString('base64'); @@ -1005,7 +1014,7 @@ router.patch('/:id', async (req, res) => { 'text/html; charset=utf-8', EXTRACT(EPOCH FROM NOW())::INTEGER, $5, NOW(), $6, NOW(), $6 )`, - [articleId, fromHeader, 'Consuntivazione', htmlBody, contentPath, operatorId] + [articleId, fromHeader, noteSubject, htmlBody, contentPath, operatorId] ); // Create article_data_mime_attachment for OTRS CE HTML rendering @@ -1020,14 +1029,16 @@ router.patch('/:id', async (req, res) => { [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] - ); + // Log the time in time_accounting if provided + if (hasTimeUnit) { + 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');