From 7b0b29e6c62b013f3ad2c9da289bce669a9c784d Mon Sep 17 00:00:00 2001 From: Gabriele Cimaschi Date: Tue, 7 Jul 2026 08:15:29 +0200 Subject: [PATCH] fix: corretta modifica massiva scassata da claude. fix: e possibile inserire clienti non esistenti come in otrs. --- public/js/views/ticketBulk.js | 14 +++-- public/js/views/ticketCreate.js | 18 ++++++- public/js/views/ticketDetail.js | 15 +++++- public/js/views/ticketList.js | 92 +++++++++++++++++++++++++++++++++ routes/tickets.js | 32 ++++++++++-- 5 files changed, 161 insertions(+), 10 deletions(-) diff --git a/public/js/views/ticketBulk.js b/public/js/views/ticketBulk.js index 89cedb3..7093a14 100644 --- a/public/js/views/ticketBulk.js +++ b/public/js/views/ticketBulk.js @@ -665,9 +665,17 @@ const TicketBulkView = { const type_id = document.getElementById(`bulk-type-${id}`).value; const ownerId = document.getElementById(`bulk-owner-${id}`).value; const responsibleId = document.getElementById(`bulk-responsible-${id}`).value; - const customerId = document.getElementById(`bulk-customer-${id}`).value; - const customerUserId = document.getElementById(`bulk-customer-user-id-${id}`).value; - const customerSearch = document.getElementById(`bulk-customer-search-${id}`).value; + let customerId = document.getElementById(`bulk-customer-${id}`).value; + let customerUserId = document.getElementById(`bulk-customer-user-id-${id}`).value; + const customerSearch = document.getElementById(`bulk-customer-search-${id}`).value.trim(); + + if (!customerUserId && customerSearch) { + customerUserId = customerSearch; + if (!customerId) { + customerId = customerSearch; + } + } + const subject = document.getElementById(`bulk-subject-${id}`).value.trim(); const body = document.getElementById(`bulk-body-${id}`).value.trim(); const priority_id = document.getElementById(`bulk-priority-${id}`).value; diff --git a/public/js/views/ticketCreate.js b/public/js/views/ticketCreate.js index 4203f56..4162ab2 100644 --- a/public/js/views/ticketCreate.js +++ b/public/js/views/ticketCreate.js @@ -243,6 +243,14 @@ const TicketCreateView = { if (this.savedState && this.savedState.body) { this.quill.root.innerHTML = this.savedState.body; } + + // Prevent tab navigation on toolbar items + const toolbar = container.querySelector('.ql-toolbar'); + if (toolbar) { + toolbar.querySelectorAll('button, select, span[role="button"], input').forEach(el => { + el.setAttribute('tabindex', '-1'); + }); + } } else { this.quill = null; } @@ -589,9 +597,17 @@ const TicketCreateView = { const priority_id = document.getElementById('create-priority').value; const type_id = document.getElementById('create-type')?.value; - const customerId = customerIdInput.value; + const userSearchVal = userSearchInput.value.trim(); + let customerId = customerIdInput.value; let customerUserId = customerUserIdInput.value; + if (!customerUserId && userSearchVal) { + customerUserId = userSearchVal; + if (!customerId) { + customerId = userSearchVal; + } + } + const ownerId = ownerIdInput.value; const responsibleId = responsibleIdInput.value; diff --git a/public/js/views/ticketDetail.js b/public/js/views/ticketDetail.js index 6b4aca5..07dea83 100644 --- a/public/js/views/ticketDetail.js +++ b/public/js/views/ticketDetail.js @@ -317,7 +317,6 @@ const TicketDetailView = { `; - // Initialize Quill Editor for Note if (window.Quill) { this.noteQuill = new Quill('#note-body-editor', { theme: 'snow', @@ -331,6 +330,14 @@ const TicketDetailView = { ] } }); + + // Prevent tab navigation on toolbar items + const toolbar = container.querySelector('.ql-toolbar'); + if (toolbar) { + toolbar.querySelectorAll('button, select, span[role="button"], input').forEach(el => { + el.setAttribute('tabindex', '-1'); + }); + } } else { this.noteQuill = null; } @@ -450,6 +457,12 @@ const TicketDetailView = { // Save quick-edit saveBtn.addEventListener('click', async () => { + const customerSearch = customerSearchInput ? customerSearchInput.value.trim() : ''; + if (customerSearch && !customerUserIdInput.value) { + customerUserIdInput.value = customerSearch; + customerIdInput.value = customerSearch; + } + const updates = {}; fields.forEach(el => { const field = el.dataset.field; diff --git a/public/js/views/ticketList.js b/public/js/views/ticketList.js index d56cec1..7224b91 100644 --- a/public/js/views/ticketList.js +++ b/public/js/views/ticketList.js @@ -85,6 +85,13 @@ const TicketListView = { ${(App.lookups.users || []).map(u => ``).join('')} +
+ Cliente + + + + +
@@ -277,10 +284,83 @@ const TicketListView = { tr.classList.remove('selected'); tr.classList.remove('first-selected'); }); + const batchCustomerSearch = document.getElementById('batch-customer-search'); + const batchCustomerUserId = document.getElementById('batch-customer-user-id'); + const batchCustomerId = document.getElementById('batch-customer-id'); + if (batchCustomerSearch) batchCustomerSearch.value = ''; + if (batchCustomerUserId) batchCustomerUserId.value = ''; + if (batchCustomerId) batchCustomerId.value = ''; + const batchState = document.getElementById('batch-state'); + if (batchState) batchState.value = ''; + const batchQueue = document.getElementById('batch-queue'); + if (batchQueue) batchQueue.value = ''; + const batchOwner = document.getElementById('batch-owner'); + if (batchOwner) batchOwner.value = ''; + this.updateBatchBar(); }); } + // Batch Customer User Autocomplete + const batchCustomerSearchInput = document.getElementById('batch-customer-search'); + const batchCustomerSuggestionsDiv = document.getElementById('batch-customer-suggestions'); + const batchCustomerUserIdInput = document.getElementById('batch-customer-user-id'); + const batchCustomerIdInput = document.getElementById('batch-customer-id'); + + let batchCustomerDebounce; + if (batchCustomerSearchInput) { + batchCustomerSearchInput.addEventListener('input', () => { + clearTimeout(batchCustomerDebounce); + const q = batchCustomerSearchInput.value.trim(); + if (q.length < 2) { + batchCustomerSuggestionsDiv.style.display = 'none'; + if (batchCustomerUserIdInput) batchCustomerUserIdInput.value = ''; + if (batchCustomerIdInput) batchCustomerIdInput.value = ''; + return; + } + + batchCustomerDebounce = setTimeout(async () => { + try { + const users = await App.api(`/api/customer-users/search?q=${encodeURIComponent(q)}`); + if (users.length === 0) { + batchCustomerSuggestionsDiv.innerHTML = '
Nessun utente trovato
'; + batchCustomerSuggestionsDiv.style.display = 'block'; + return; + } + + batchCustomerSuggestionsDiv.innerHTML = users.map(u => ` +
+ ${App.escapeHtml(u.first_name)} ${App.escapeHtml(u.last_name)} + (Login: ${App.escapeHtml(u.login)} | Azienda: ${App.escapeHtml(u.customer_id || '—')}) +
+ `).join(''); + batchCustomerSuggestionsDiv.style.display = 'block'; + + // Bind click + batchCustomerSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => { + if (item.dataset.login) { + item.addEventListener('click', () => { + batchCustomerSearchInput.value = item.dataset.name; + if (batchCustomerUserIdInput) batchCustomerUserIdInput.value = item.dataset.login; + if (batchCustomerIdInput) batchCustomerIdInput.value = item.dataset.customerId || ''; + batchCustomerSuggestionsDiv.style.display = 'none'; + }); + } + }); + } catch (err) { + console.error(err); + } + }, 300); + }); + } + + // Close suggestions on click outside + document.addEventListener('click', (e) => { + if (batchCustomerSearchInput && e.target !== batchCustomerSearchInput && e.target !== batchCustomerSuggestionsDiv) { + batchCustomerSuggestionsDiv.style.display = 'none'; + } + }); + // Pagination document.querySelectorAll('.pagination-btn[data-page]').forEach(btn => { btn.addEventListener('click', () => { @@ -316,10 +396,22 @@ const TicketListView = { const batchState = document.getElementById('batch-state')?.value; const batchQueue = document.getElementById('batch-queue')?.value; const batchOwner = document.getElementById('batch-owner')?.value; + const batchCustomerSearch = document.getElementById('batch-customer-search')?.value.trim(); + let batchCustomerUserId = document.getElementById('batch-customer-user-id')?.value; + let batchCustomerId = document.getElementById('batch-customer-id')?.value; + + if (!batchCustomerUserId && batchCustomerSearch) { + batchCustomerUserId = batchCustomerSearch; + if (!batchCustomerId) { + batchCustomerId = batchCustomerSearch; + } + } if (batchState) updates.ticket_state_id = parseInt(batchState); if (batchQueue) updates.queue_id = parseInt(batchQueue); if (batchOwner) updates.user_id = parseInt(batchOwner); + if (batchCustomerUserId) updates.customer_user_id = batchCustomerUserId; + if (batchCustomerId) updates.customer_id = batchCustomerId; if (Object.keys(updates).length === 0) { Toast.warning('Seleziona almeno un campo da modificare'); diff --git a/routes/tickets.js b/routes/tickets.js index d1198b8..fb7460a 100644 --- a/routes/tickets.js +++ b/routes/tickets.js @@ -278,12 +278,34 @@ router.post('/', async (req, res) => { await client.query('BEGIN'); // Generate ticket number: get next counter value for today (daily reset) + const now = new Date(); + const systemId = process.env.OTRS_SYSTEM_ID || '10'; + const datePrefix = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${systemId}`; + + // 1. Fetch current maximum counter in ticket table for today + const maxTicketResult = await client.query( + `SELECT tn FROM ticket WHERE tn LIKE $1`, + [`${datePrefix}%`] + ); + let maxCounterFromTicketTable = 0; + for (const row of maxTicketResult.rows) { + const counterPart = row.tn.substring(datePrefix.length); + const parsed = parseInt(counterPart, 10); + if (!isNaN(parsed) && parsed > maxCounterFromTicketTable) { + maxCounterFromTicketTable = parsed; + } + } + + // 2. Fetch current maximum counter in ticket_number_counter table for today const counterTodayResult = await client.query( `SELECT COALESCE(MAX(counter), 0) AS max_counter FROM ticket_number_counter WHERE create_time >= CURRENT_DATE` ); - const nextCounter = parseInt(counterTodayResult.rows[0].max_counter, 10) + 1; + const maxCounterFromCounterTable = parseInt(counterTodayResult.rows[0].max_counter, 10); + + // 3. Compute the next counter (absolute max + 1) + const nextCounter = Math.max(maxCounterFromTicketTable, maxCounterFromCounterTable) + 1; const counterResult = await client.query( `INSERT INTO ticket_number_counter (counter, counter_uid, create_time) @@ -296,10 +318,8 @@ router.post('/', async (req, res) => { [nextCounter] ); const counter = counterResult.rows[0].counter; - const now = new Date(); - const systemId = process.env.OTRS_SYSTEM_ID || '10'; const counterPadding = parseInt(process.env.OTRS_COUNTER_PADDING, 10) || 6; - const tn = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}${systemId}${String(counter).padStart(counterPadding, '0')}`; + const tn = `${datePrefix}${String(counter).padStart(counterPadding, '0')}`; // Determine lock type (1 = unlock by default) const lockId = 1; @@ -885,6 +905,8 @@ router.patch('/batch/update', 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.customer_id !== undefined) ticketFields.CustomerID = updates.customer_id; + if (updates.customer_user_id !== undefined) ticketFields.CustomerUser = updates.customer_user_id; // Auto sblocco check if (updates.ticket_state_id) { @@ -926,7 +948,7 @@ router.patch('/batch/update', async (req, res) => { try { await client.query('BEGIN'); - const allowedFields = ['ticket_state_id', 'ticket_priority_id', 'queue_id', 'user_id']; + const allowedFields = ['ticket_state_id', 'ticket_priority_id', 'queue_id', 'user_id', 'customer_id', 'customer_user_id']; const setClauses = []; const setParams = []; let pIdx = 1;