-
-
-

Dashboard

-
- - + +
+
+
+ + - -
- -
- -
+ +
diff --git a/public/js/app.js b/public/js/app.js index b10b2f2..999aba0 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -13,6 +13,108 @@ const App = { lookupsLoaded: false, demotivationalPhrases: [], motivationalPhrases: [], + drafts: {}, + tabs: [], + + loadTabs() { + try { + const saved = localStorage.getItem('otrs_turbo_tabs'); + if (saved) this.tabs = JSON.parse(saved); + const savedDrafts = localStorage.getItem('otrs_turbo_drafts'); + if (savedDrafts) this.drafts = JSON.parse(savedDrafts); + } catch (e) {} + this.renderTabs(); + }, + + saveTabs() { + localStorage.setItem('otrs_turbo_tabs', JSON.stringify(this.tabs)); + this.renderTabs(); + }, + + saveDraft(ticketId, draft) { + if (!this.drafts[ticketId]) this.drafts[ticketId] = {}; + this.drafts[ticketId][draft.type] = draft; + localStorage.setItem('otrs_turbo_drafts', JSON.stringify(this.drafts)); + }, + + getDraft(ticketId, type) { + return this.drafts[ticketId] ? this.drafts[ticketId][type] : null; + }, + + clearDraft(ticketId, type) { + if (this.drafts[ticketId] && this.drafts[ticketId][type]) { + delete this.drafts[ticketId][type]; + if (Object.keys(this.drafts[ticketId]).length === 0) { + delete this.drafts[ticketId]; + } + localStorage.setItem('otrs_turbo_drafts', JSON.stringify(this.drafts)); + } + }, + + openTab(id, tn, title) { + const exists = this.tabs.find(t => String(t.id) === String(id)); + if (!exists) { + this.tabs.push({ id, tn, title }); + this.saveTabs(); + } + }, + + addTabWithoutRedirect(id, tn, title) { + const exists = this.tabs.find(t => String(t.id) === String(id)); + if (!exists) { + this.tabs.push({ id, tn, title }); + this.saveTabs(); + } else { + if (title && exists.title !== title) { + exists.title = title; + this.saveTabs(); + } else { + this.renderTabs(); + } + } + }, + + closeTab(id, e) { + if (e) e.stopPropagation(); + this.tabs = this.tabs.filter(t => String(t.id) !== String(id)); + this.saveTabs(); + + // Clear drafts for closed tab + delete this.drafts[id]; + localStorage.setItem('otrs_turbo_drafts', JSON.stringify(this.drafts)); + + const hash = window.location.hash; + if (hash === `#/tickets/${id}`) { + if (this.tabs.length > 0) { + window.location.hash = `#/tickets/${this.tabs[this.tabs.length - 1].id}`; + } else { + window.location.hash = '#/tickets'; + } + } + }, + + renderTabs() { + const bar = document.getElementById('tabs-bar'); + if (!bar) return; + if (this.tabs.length === 0) { + bar.style.display = 'none'; + return; + } + bar.style.display = 'flex'; + + const currentHash = window.location.hash; + + bar.innerHTML = this.tabs.map(t => { + const isActive = currentHash === `#/tickets/${t.id}`; + const displayTitle = t.title ? (t.title.length > 25 ? t.title.substring(0, 22) + '...' : t.title) : `#${t.tn}`; + return ` +
+ ${App.escapeHtml(displayTitle)} + +
+ `; + }).join(''); + }, get currentAgentId() { return parseInt(localStorage.getItem('activeAgentId') || '1', 10); @@ -23,6 +125,7 @@ const App = { this.initTheme(); this.loadDemotivationalPhrases(); this.loadMotivationalPhrases(); + this.loadTabs(); Toast.init(); // Hash-based SPA router @@ -112,6 +215,27 @@ const App = { const hash = fullHash.split('?')[0]; const titleEl = document.getElementById('page-title'); + // Save draft for previous ticket before routing + if (typeof TicketDetailView !== 'undefined' && TicketDetailView.ticketId) { + TicketDetailView.saveDraft(); + if (typeof EmailCompose !== 'undefined') { + EmailCompose.saveDraft(); + const overlay = document.getElementById('email-compose-overlay'); + if (overlay) overlay.remove(); + } + } + + this.renderTabs(); + + const container = document.getElementById('view-container'); + if (container) { + if (hash.match(/^#\/tickets\/(\d+)$/)) { + container.classList.add('ticket-view-active'); + } else { + container.classList.remove('ticket-view-active'); + } + } + // Update active nav link document.querySelectorAll('.nav-link').forEach(link => { link.classList.remove('active'); @@ -243,11 +367,11 @@ const App = { try { // Sync LDAP customer users to local DB cache await this.api('/api/customer-users/sync', { method: 'POST' }); - + await this.ensureLookups(true); await this.initAgentSelector(); Toast.success('Dati locali (code, utenti, ecc.) aggiornati con successo!'); - + // If we are on a view that needs lookups, we can re-render it const hash = window.location.hash; if (hash === '#/tickets/bulk') { @@ -325,7 +449,7 @@ const App = { await this.ensureLookups(); // Populate select dropdown - select.innerHTML = (this.lookups.users || []).map(u => + select.innerHTML = (this.lookups.users || []).map(u => `` ).join(''); @@ -505,7 +629,7 @@ const App = { if (timerEl) { timerEl.classList.add('clickable-auto-time'); - timerEl.setAttribute('title', `Consuntivazione Automatica fine giornata (${remaining} m). Clicca per eseguire.`); + //timerEl.setAttribute('title', `Consuntivazione Automatica fine giornata (${remaining} m). Clicca per eseguire.`); timerEl.onclick = triggerAction; } } else { @@ -580,7 +704,7 @@ const App = { async updateSidebarBadges() { try { const stats = await this.api('/api/dashboard/stats'); - + const badge = document.getElementById('open-ticket-count'); if (badge) { badge.textContent = stats.total_open > 0 ? stats.total_open : ''; @@ -657,7 +781,7 @@ const App = { btnCancel.addEventListener('click', () => cleanUp(false)); btnOk.addEventListener('click', () => cleanUp(true)); - + // Close on backdrop click overlay.addEventListener('click', (e) => { if (e.target === overlay) cleanUp(false); @@ -807,7 +931,7 @@ const App = { } }); } - + overlay.addEventListener('click', (e) => { if (e.target === overlay) cleanUp(null); }); diff --git a/public/js/components/filters.js b/public/js/components/filters.js index f599a5a..17bf3d7 100644 --- a/public/js/components/filters.js +++ b/public/js/components/filters.js @@ -681,7 +681,13 @@ const Filters = { const presetId = e.target.value; if (!presetId) { this.selectedPresetId = null; - if (onFilterChange) onFilterChange(); + const resetBtn = document.getElementById('filter-reset'); + if (resetBtn) { + resetBtn.click(); + } else { + this.reset(); + if (onFilterChange) onFilterChange(); + } return; } diff --git a/public/js/views/emailCompose.js b/public/js/views/emailCompose.js index 4e2c373..4c37cb2 100644 --- a/public/js/views/emailCompose.js +++ b/public/js/views/emailCompose.js @@ -8,6 +8,9 @@ const EmailCompose = (() => { let quillEditor = null; let attachmentsList = []; let currentOptions = {}; + let toTagsCtrl = null; + let ccTagsCtrl = null; + let bccTagsCtrl = null; // ── CSS ────────────────────────────────────────────────────────────────────── function injectStyles() { @@ -70,6 +73,10 @@ const EmailCompose = (() => { border-color: var(--accent-primary); box-shadow: 0 0 0 3px rgba(var(--accent-rgb,99,102,241),0.12); } + #email-compose-modal .ec-tags-input.drag-over { + border-color: var(--accent-primary); + background: rgba(99,102,241,0.06); + } #email-compose-modal .ec-tag { display: inline-flex; align-items: center; gap: 4px; background: var(--accent-primary); color: #fff; @@ -145,7 +152,7 @@ const EmailCompose = (() => { } // ── Tag Input Helper ────────────────────────────────────────────────────────── - function makeTagInput(containerId, initialEmails = [], onFocus = null) { + function makeTagInput(containerId, initialEmails = [], onFocus = null, controllers = null) { const container = document.getElementById(containerId); const tags = [...initialEmails]; @@ -156,8 +163,16 @@ const EmailCompose = (() => { tags.forEach((email, idx) => { const tagEl = document.createElement('span'); tagEl.className = 'ec-tag'; + tagEl.draggable = true; tagEl.innerHTML = `${App.escapeHtml(email)}`; - tagEl.querySelector('button').addEventListener('click', () => { + + tagEl.addEventListener('dragstart', (e) => { + e.dataTransfer.setData('text/plain', email); + e.dataTransfer.setData('source-container-id', containerId); + }); + + tagEl.querySelector('button').addEventListener('click', (e) => { + e.stopPropagation(); tags.splice(idx, 1); render(); }); @@ -197,8 +212,48 @@ const EmailCompose = (() => { container.addEventListener('click', () => input.focus()); } + container.addEventListener('dragover', (e) => { + e.preventDefault(); + container.classList.add('drag-over'); + }); + + container.addEventListener('dragleave', () => { + container.classList.remove('drag-over'); + }); + + container.addEventListener('drop', (e) => { + e.preventDefault(); + container.classList.remove('drag-over'); + const email = e.dataTransfer.getData('text/plain'); + const sourceContainerId = e.dataTransfer.getData('source-container-id'); + if (email && sourceContainerId && sourceContainerId !== containerId && controllers) { + const sourceCtrl = controllers[sourceContainerId]; + if (sourceCtrl) { + sourceCtrl.removeTag(email); + ctrl.addTag(email); + } + } + }); + render(); - return { getTags: () => [...tags], addTag: (email) => { if (!tags.includes(email)) { tags.push(email); render(); } } }; + + const ctrl = { + getTags: () => [...tags], + addTag: (email) => { + if (!tags.includes(email)) { + tags.push(email); + render(); + } + }, + removeTag: (email) => { + const idx = tags.indexOf(email); + if (idx > -1) { + tags.splice(idx, 1); + render(); + } + } + }; + return ctrl; } // ── Build Modal HTML ────────────────────────────────────────────────────────── @@ -356,19 +411,30 @@ const EmailCompose = (() => { document.body.appendChild(overlay); // Init tag inputs with focus tracking - const initialTo = options.initialTo || (options.customerEmail ? [options.customerEmail] : []); - const initialCc = options.initialCc || []; + const draft = options.draft; + const initialTo = draft ? draft.to : (options.initialTo || (options.customerEmail ? [options.customerEmail] : [])); + const initialCc = draft ? draft.cc : (options.initialCc || []); + const initialBcc = draft ? draft.bcc : []; + let lastFocusedCtrl = null; - const toTagsCtrl = makeTagInput('ec-to-container', initialTo, () => { lastFocusedCtrl = toTagsCtrl; }); - const ccTagsCtrl = makeTagInput('ec-cc-container', initialCc, () => { lastFocusedCtrl = ccTagsCtrl; }); - const bccTagsCtrl = makeTagInput('ec-bcc-container', [], () => { lastFocusedCtrl = bccTagsCtrl; }); + const controllers = {}; + toTagsCtrl = makeTagInput('ec-to-container', initialTo, () => { lastFocusedCtrl = toTagsCtrl; }, controllers); + ccTagsCtrl = makeTagInput('ec-cc-container', initialCc, () => { lastFocusedCtrl = ccTagsCtrl; }, controllers); + bccTagsCtrl = makeTagInput('ec-bcc-container', initialBcc, () => { lastFocusedCtrl = bccTagsCtrl; }, controllers); + controllers['ec-to-container'] = toTagsCtrl; + controllers['ec-cc-container'] = ccTagsCtrl; + controllers['ec-bcc-container'] = bccTagsCtrl; lastFocusedCtrl = toTagsCtrl; // Subject const subjectEl = document.getElementById('ec-subject'); - const tn = options.ticketTn || ''; - const title = options.ticketTitle || ''; - subjectEl.value = tn ? `Re: [Ticket#${tn}] ${title}` : title; + if (draft) { + subjectEl.value = draft.subject || ''; + } else { + const tn = options.ticketTn || ''; + const title = options.ticketTitle || ''; + subjectEl.value = tn ? `Re: [Ticket#${tn}] ${title}` : title; + } // Signature and groups select const sigSelect = document.getElementById('ec-signature-select'); @@ -377,6 +443,11 @@ const EmailCompose = (() => { const defaultSigHtml = await loadSignatures(agentId, sigSelect); await loadAddressGroups(agentId, groupsSelect); + if (draft) { + sigSelect.value = draft.signature || ''; + document.getElementById('ec-helpdesk-cc-select').value = draft.helpdeskCc || '0'; + } + groupsSelect.addEventListener('change', () => { const selectedOpt = groupsSelect.options[groupsSelect.selectedIndex]; if (!selectedOpt || !selectedOpt.value) return; @@ -410,13 +481,21 @@ const EmailCompose = (() => { // Insert initial body and signature let initialHtml = ''; - if (options.initialBodyHtml) { - initialHtml += options.initialBodyHtml; + if (draft) { + if (draft.body) { + initialHtml = draft.body; + } + attachmentsList = draft.attachments || []; + renderFileList(); } else { - initialHtml += '


'; - } - if (defaultSigHtml) { - initialHtml += '' + defaultSigHtml; + if (options.initialBodyHtml) { + initialHtml += options.initialBodyHtml; + } else { + initialHtml += '


'; + } + if (defaultSigHtml) { + initialHtml += '' + defaultSigHtml; + } } quillEditor.clipboard.dangerouslyPasteHTML(initialHtml); quillEditor.setSelection(0, 0); @@ -454,7 +533,15 @@ const EmailCompose = (() => { // Close handlers document.getElementById('ec-close').addEventListener('click', close); document.getElementById('ec-cancel').addEventListener('click', close); - overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); }); + overlay.addEventListener('click', (e) => { + if (e.target === overlay) { + saveDraft(); + const ov = document.getElementById('email-compose-overlay'); + if (ov) ov.remove(); + if (quillEditor) { quillEditor = null; } + attachmentsList = []; + } + }); // Send document.getElementById('ec-send').addEventListener('click', () => sendEmail(toTagsCtrl, ccTagsCtrl, bccTagsCtrl)); @@ -505,6 +592,7 @@ const EmailCompose = (() => { }); Toast.success(`Email inviata a ${to.join(', ')}`); + App.clearDraft(currentOptions.ticketId, 'email'); close(); } catch (err) { Toast.error('Errore invio email: ' + err.message); @@ -515,12 +603,41 @@ const EmailCompose = (() => { // ── Close ───────────────────────────────────────────────────────────────────── function close() { + if (currentOptions.ticketId) { + App.clearDraft(currentOptions.ticketId, 'email'); + } const overlay = document.getElementById('email-compose-overlay'); if (overlay) overlay.remove(); if (quillEditor) { quillEditor = null; } attachmentsList = []; } - return { open, close }; + function saveDraft() { + const overlay = document.getElementById('email-compose-overlay'); + if (!overlay || !currentOptions.ticketId) return; + + const to = toTagsCtrl ? toTagsCtrl.getTags() : []; + const cc = ccTagsCtrl ? ccTagsCtrl.getTags() : []; + const bcc = bccTagsCtrl ? bccTagsCtrl.getTags() : []; + const subject = document.getElementById('ec-subject') ? document.getElementById('ec-subject').value.trim() : ''; + const body = quillEditor ? quillEditor.root.innerHTML.trim() : ''; + const signature = document.getElementById('ec-signature-select') ? document.getElementById('ec-signature-select').value : ''; + const helpdeskCc = document.getElementById('ec-helpdesk-cc-select') ? document.getElementById('ec-helpdesk-cc-select').value : '0'; + + App.saveDraft(currentOptions.ticketId, { + type: 'email', + to, + cc, + bcc, + subject, + body, + signature, + helpdeskCc, + attachments: [...attachmentsList], + options: currentOptions + }); + } + + return { open, close, saveDraft }; })(); window.EmailCompose = EmailCompose; diff --git a/public/js/views/ticketDetail.js b/public/js/views/ticketDetail.js index 07628f1..10947b2 100644 --- a/public/js/views/ticketDetail.js +++ b/public/js/views/ticketDetail.js @@ -8,6 +8,25 @@ const TicketDetailView = { originalValues: {}, noteAttachments: [], + saveDraft() { + if (!this.ticketId) return; + const body = this.noteQuill ? this.noteQuill.root.innerHTML.trim() : ''; + const subject = document.getElementById('note-subject') ? document.getElementById('note-subject').value.trim() : ''; + const time_unit = document.getElementById('note-time-units') ? document.getElementById('note-time-units').value.trim() : ''; + + if ((body !== '


' && body !== '') || subject !== '' || time_unit !== '' || this.noteAttachments.length > 0) { + App.saveDraft(this.ticketId, { + type: 'note', + body, + subject, + time_unit, + attachments: [...this.noteAttachments] + }); + } else { + App.clearDraft(this.ticketId, 'note'); + } + }, + updateNoteAttachmentList() { const listEl = document.getElementById('note-file-list'); if (!listEl) return; @@ -41,6 +60,7 @@ const TicketDetailView = { await App.ensureLookups(); const data = await App.api(`/api/tickets/${id}`); const { ticket, articles, attachments } = data; + App.addTabWithoutRedirect(ticket.id, ticket.tn, ticket.title); let groupsData = { asMaster: [], asMember: [] }; try { @@ -75,6 +95,7 @@ const TicketDetailView = { queue_id: ticket.queue_id, queue_name: ticket.queue_name, user_id: ticket.user_id, + responsible_user_id: ticket.responsible_user_id, type_id: ticket.type_id, customer_id: ticket.customer_id, customer_user_id: ticket.customer_user_id, @@ -143,6 +164,15 @@ const TicketDetailView = { + +
+ +
@@ -333,12 +363,14 @@ const TicketDetailView = { ${ticket.type_name} ` : ''} - ${ticket.responsible_first ? ` -
- Responsabile - ${ticket.responsible_first} ${ticket.responsible_last} -
- ` : ''} +
+ Owner + ${ticket.owner_first ? `${ticket.owner_first} ${ticket.owner_last}` : (ticket.owner_login || '—')} +
+
+ Responsabile + ${ticket.responsible_first ? `${ticket.responsible_first} ${ticket.responsible_last}` : '—'} +
${totalTime > 0 ? `
Tempo Totale @@ -457,6 +489,36 @@ const TicketDetailView = { this.noteQuill = null; } + // Restore Note Draft + const noteDraft = App.getDraft(id, 'note'); + if (noteDraft) { + if (this.noteQuill && noteDraft.body) { + this.noteQuill.root.innerHTML = noteDraft.body; + } + if (document.getElementById('note-subject')) { + document.getElementById('note-subject').value = noteDraft.subject || ''; + } + if (document.getElementById('note-time-units')) { + document.getElementById('note-time-units').value = noteDraft.time_unit || ''; + } + this.noteAttachments = noteDraft.attachments || []; + this.updateNoteAttachmentList(); + } + + // Restore Email Compose Draft + const emailDraft = App.getDraft(id, 'email'); + if (emailDraft) { + // Re-open Email Compose with draft options + setTimeout(() => { + if (typeof EmailCompose !== 'undefined') { + EmailCompose.open({ + ...emailDraft.options, + draft: emailDraft + }); + } + }, 100); + } + this.bindEvents(ticket, articles, container, groupsData); } catch (err) { @@ -760,6 +822,7 @@ const TicketDetailView = { this.noteAttachments = []; // Clear attachments Toast.success(res.message || 'Nota aggiunta!'); + App.clearDraft(this.ticketId, 'note'); App.updateDailyTimer(); this.render(this.ticketId); } catch (err) { @@ -1107,7 +1170,11 @@ const TicketDetailView = { btnAssociate.addEventListener('click', async () => { const select = document.getElementById('group-association-select'); const groupId = select.value; - if (!groupId) return; + if (!groupId) { + sessionStorage.setItem('otrs_create_group_with_master_tn', ticket.tn); + window.location.hash = '#/tickets/groups'; + return; + } try { btnAssociate.disabled = true; diff --git a/public/js/views/ticketGroups.js b/public/js/views/ticketGroups.js index 5891730..bef1c2a 100644 --- a/public/js/views/ticketGroups.js +++ b/public/js/views/ticketGroups.js @@ -80,7 +80,15 @@ const TicketGroupsView = { await this.loadGroups(); - if (this.selectedGroupId) { + const prefillMasterTn = sessionStorage.getItem('otrs_create_group_with_master_tn'); + if (prefillMasterTn) { + sessionStorage.removeItem('otrs_create_group_with_master_tn'); + this.openGroupModal(); + const masterInput = document.getElementById('group-master-input'); + if (masterInput) { + masterInput.value = prefillMasterTn; + } + } else if (this.selectedGroupId) { this.selectGroup(this.selectedGroupId); } }, diff --git a/public/js/views/ticketList.js b/public/js/views/ticketList.js index 4b727ee..7722e3f 100644 --- a/public/js/views/ticketList.js +++ b/public/js/views/ticketList.js @@ -109,6 +109,7 @@ const TicketListView = {
+ @@ -147,6 +148,7 @@ const TicketListView = { O ` : ''} + ${App.formatDateTime(t.create_time)} @@ -304,6 +306,39 @@ const TicketListView = { batchMerge.addEventListener('click', () => this.mergeBatch()); } + // Batch open tabs + const batchOpenTabsBtn = document.getElementById('batch-open-tabs'); + if (batchOpenTabsBtn) { + batchOpenTabsBtn.addEventListener('click', (e) => { + e.stopPropagation(); + if (this.selectedIds.size === 0) return; + + let count = 0; + this.selectedIds.forEach(id => { + const row = document.querySelector(`.ticket-table tbody tr[data-ticket-id="${id}"]`); + if (row) { + const tnLink = row.querySelector('.ticket-tn-link'); + const titleLink = row.querySelector('.ticket-title-link'); + const tn = tnLink ? tnLink.textContent.trim() : ''; + const title = titleLink ? titleLink.textContent.trim() : ''; + App.openTab(parseInt(id, 10), tn, title); + count++; + } + }); + + if (count > 0) { + Toast.success(`${count} ticket aperti in nuove schede!`); + this.selectedIds.clear(); + this.selectedOrder = []; + document.querySelectorAll('.ticket-table tbody tr').forEach(tr => { + tr.classList.remove('selected'); + tr.classList.remove('first-selected'); + }); + this.updateBatchBar(); + } + }); + } + // Batch Copy Ticket Numbers const batchCopyTns = document.getElementById('batch-copy-tns'); if (batchCopyTns) { @@ -505,6 +540,16 @@ const TicketListView = { mergeBtn.disabled = true; } } + + // Enable/disable open tabs button + const openTabsBtn = document.getElementById('batch-open-tabs'); + if (openTabsBtn) { + if (this.selectedIds.size > 0) { + openTabsBtn.disabled = false; + } else { + openTabsBtn.disabled = true; + } + } }, async applyBatch() { diff --git a/routes/tickets.js b/routes/tickets.js index 8e95f57..0e768bd 100644 --- a/routes/tickets.js +++ b/routes/tickets.js @@ -742,7 +742,7 @@ router.patch('/:id', async (req, res) => { 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 + `SELECT ticket_state_id, ticket_priority_id, queue_id, user_id, responsible_user_id, type_id, title, ticket_lock_id, customer_id, customer_user_id FROM ticket WHERE id = $1`, [id] ); @@ -763,6 +763,7 @@ router.patch('/:id', async (req, res) => { if (updates.ticket_priority_id !== undefined) ticketFields.PriorityID = updates.ticket_priority_id; if (updates.queue_id !== undefined) ticketFields.QueueID = updates.queue_id; if (updates.user_id !== undefined) ticketFields.OwnerID = updates.user_id; + if (updates.responsible_user_id !== undefined) ticketFields.ResponsibleID = updates.responsible_user_id; if (updates.type_id !== undefined) ticketFields.TypeID = updates.type_id; if (updates.title !== undefined) ticketFields.Title = updates.title; if (updates.ticket_lock_id !== undefined) ticketFields.LockID = updates.ticket_lock_id; @@ -854,7 +855,7 @@ router.patch('/:id', async (req, res) => { const setParams = []; let pIdx = 1; - const allowedFields = ['ticket_state_id', 'ticket_priority_id', 'queue_id', 'user_id', 'type_id', 'title', 'ticket_lock_id', 'customer_id', 'customer_user_id']; + const allowedFields = ['ticket_state_id', 'ticket_priority_id', 'queue_id', 'user_id', 'responsible_user_id', 'type_id', 'title', 'ticket_lock_id', 'customer_id', 'customer_user_id']; for (const field of allowedFields) { if (updates[field] !== undefined && updates[field] !== current[field]) { setClauses.push(`${field} = $${pIdx++}`); @@ -895,6 +896,7 @@ router.patch('/:id', async (req, res) => { ticket_priority_id: 'PriorityUpdate', queue_id: 'Move', user_id: 'OwnerUpdate', + responsible_user_id: 'ResponsibleUpdate', type_id: 'TypeUpdate', ticket_lock_id: 'Lock', customer_id: 'CustomerUpdate', @@ -1795,6 +1797,33 @@ router.post('/merge', async (req, res) => { )`, [articleId, mergeNoteBody, contentPath, operatorId] ); + + // Create internal system note inside source ticket B (the merged ticket) + const sourceArtResult = 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, 1, 1, 0, 1, NOW(), $2, NOW(), $2) RETURNING id`, + [sourceId, operatorId] + ); + const sourceArticleId = sourceArtResult.rows[0].id; + const sourceMergeNoteText = `Merged Ticket ${sourceTn} to ${targetTn}`; + + 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, 'Sistema OTRS Turbo', '', '', '', '', $2, $2, + '', '', '', + 'text/plain; charset=utf-8', EXTRACT(EPOCH FROM NOW())::INTEGER, $3, + NOW(), $4, NOW(), $4 + )`, + [sourceArticleId, sourceMergeNoteText, contentPath, operatorId] + ); } await client.query('COMMIT');