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('')}
+
@@ -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;