fix: corretta modifica massiva scassata da claude. fix: e possibile inserire clienti non esistenti come in otrs.

This commit is contained in:
2026-07-07 08:15:29 +02:00
parent b3570d6757
commit 7b0b29e6c6
5 changed files with 161 additions and 10 deletions
+11 -3
View File
@@ -665,9 +665,17 @@ const TicketBulkView = {
const type_id = document.getElementById(`bulk-type-${id}`).value; const type_id = document.getElementById(`bulk-type-${id}`).value;
const ownerId = document.getElementById(`bulk-owner-${id}`).value; const ownerId = document.getElementById(`bulk-owner-${id}`).value;
const responsibleId = document.getElementById(`bulk-responsible-${id}`).value; const responsibleId = document.getElementById(`bulk-responsible-${id}`).value;
const customerId = document.getElementById(`bulk-customer-${id}`).value; let customerId = document.getElementById(`bulk-customer-${id}`).value;
const customerUserId = document.getElementById(`bulk-customer-user-id-${id}`).value; let customerUserId = document.getElementById(`bulk-customer-user-id-${id}`).value;
const customerSearch = document.getElementById(`bulk-customer-search-${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 subject = document.getElementById(`bulk-subject-${id}`).value.trim();
const body = document.getElementById(`bulk-body-${id}`).value.trim(); const body = document.getElementById(`bulk-body-${id}`).value.trim();
const priority_id = document.getElementById(`bulk-priority-${id}`).value; const priority_id = document.getElementById(`bulk-priority-${id}`).value;
+17 -1
View File
@@ -243,6 +243,14 @@ const TicketCreateView = {
if (this.savedState && this.savedState.body) { if (this.savedState && this.savedState.body) {
this.quill.root.innerHTML = 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 { } else {
this.quill = null; this.quill = null;
} }
@@ -589,9 +597,17 @@ const TicketCreateView = {
const priority_id = document.getElementById('create-priority').value; const priority_id = document.getElementById('create-priority').value;
const type_id = document.getElementById('create-type')?.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; let customerUserId = customerUserIdInput.value;
if (!customerUserId && userSearchVal) {
customerUserId = userSearchVal;
if (!customerId) {
customerId = userSearchVal;
}
}
const ownerId = ownerIdInput.value; const ownerId = ownerIdInput.value;
const responsibleId = responsibleIdInput.value; const responsibleId = responsibleIdInput.value;
+14 -1
View File
@@ -317,7 +317,6 @@ const TicketDetailView = {
</div> </div>
`; `;
// Initialize Quill Editor for Note
if (window.Quill) { if (window.Quill) {
this.noteQuill = new Quill('#note-body-editor', { this.noteQuill = new Quill('#note-body-editor', {
theme: 'snow', 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 { } else {
this.noteQuill = null; this.noteQuill = null;
} }
@@ -450,6 +457,12 @@ const TicketDetailView = {
// Save quick-edit // Save quick-edit
saveBtn.addEventListener('click', async () => { saveBtn.addEventListener('click', async () => {
const customerSearch = customerSearchInput ? customerSearchInput.value.trim() : '';
if (customerSearch && !customerUserIdInput.value) {
customerUserIdInput.value = customerSearch;
customerIdInput.value = customerSearch;
}
const updates = {}; const updates = {};
fields.forEach(el => { fields.forEach(el => {
const field = el.dataset.field; const field = el.dataset.field;
+92
View File
@@ -85,6 +85,13 @@ const TicketListView = {
${(App.lookups.users || []).map(u => `<option value="${u.id}">${u.first_name} ${u.last_name}</option>`).join('')} ${(App.lookups.users || []).map(u => `<option value="${u.id}">${u.first_name} ${u.last_name}</option>`).join('')}
</select> </select>
</div> </div>
<div class="filter-group" style="position:relative;">
<span class="filter-label">Cliente</span>
<input type="text" class="form-input filter-select" id="batch-customer-search" placeholder="Cerca cliente..." autocomplete="off" style="width:160px; padding: 4px 8px; font-family: inherit; font-size: 0.85rem;" />
<input type="hidden" id="batch-customer-user-id" />
<input type="hidden" id="batch-customer-id" />
<div id="batch-customer-suggestions" class="autocomplete-suggestions" style="display:none; top: 100%; left: 0; width: 280px; z-index: 1001;"></div>
</div>
<button class="btn btn-primary btn-sm" id="batch-apply">Applica</button> <button class="btn btn-primary btn-sm" id="batch-apply">Applica</button>
<button class="btn btn-primary btn-sm" id="batch-merge" disabled style="background: var(--accent-secondary); border-color: var(--accent-secondary); margin-left: 8px;">Unisci Selezionati</button> <button class="btn btn-primary btn-sm" id="batch-merge" disabled style="background: var(--accent-secondary); border-color: var(--accent-secondary); margin-left: 8px;">Unisci Selezionati</button>
<button class="btn btn-ghost btn-xs" id="batch-cancel">Annulla</button> <button class="btn btn-ghost btn-xs" id="batch-cancel">Annulla</button>
@@ -277,10 +284,83 @@ const TicketListView = {
tr.classList.remove('selected'); tr.classList.remove('selected');
tr.classList.remove('first-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(); 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 = '<div class="autocomplete-suggestion-item" style="color:var(--text-muted); cursor:default;">Nessun utente trovato</div>';
batchCustomerSuggestionsDiv.style.display = 'block';
return;
}
batchCustomerSuggestionsDiv.innerHTML = users.map(u => `
<div class="autocomplete-suggestion-item" data-login="${App.escapeHtml(u.login)}" data-customer-id="${App.escapeHtml(u.customer_id || '')}" data-name="${App.escapeHtml(u.first_name + ' ' + u.last_name)}">
<strong>${App.escapeHtml(u.first_name)} ${App.escapeHtml(u.last_name)}</strong>
<span style="font-size:0.75rem; color:var(--text-muted); margin-left:var(--space-xs); font-weight:normal;">(Login: ${App.escapeHtml(u.login)} | Azienda: ${App.escapeHtml(u.customer_id || '—')})</span>
</div>
`).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 // Pagination
document.querySelectorAll('.pagination-btn[data-page]').forEach(btn => { document.querySelectorAll('.pagination-btn[data-page]').forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
@@ -316,10 +396,22 @@ const TicketListView = {
const batchState = document.getElementById('batch-state')?.value; const batchState = document.getElementById('batch-state')?.value;
const batchQueue = document.getElementById('batch-queue')?.value; const batchQueue = document.getElementById('batch-queue')?.value;
const batchOwner = document.getElementById('batch-owner')?.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 (batchState) updates.ticket_state_id = parseInt(batchState);
if (batchQueue) updates.queue_id = parseInt(batchQueue); if (batchQueue) updates.queue_id = parseInt(batchQueue);
if (batchOwner) updates.user_id = parseInt(batchOwner); 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) { if (Object.keys(updates).length === 0) {
Toast.warning('Seleziona almeno un campo da modificare'); Toast.warning('Seleziona almeno un campo da modificare');
+27 -5
View File
@@ -278,12 +278,34 @@ router.post('/', async (req, res) => {
await client.query('BEGIN'); await client.query('BEGIN');
// Generate ticket number: get next counter value for today (daily reset) // 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( const counterTodayResult = await client.query(
`SELECT COALESCE(MAX(counter), 0) AS max_counter `SELECT COALESCE(MAX(counter), 0) AS max_counter
FROM ticket_number_counter FROM ticket_number_counter
WHERE create_time >= CURRENT_DATE` 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( const counterResult = await client.query(
`INSERT INTO ticket_number_counter (counter, counter_uid, create_time) `INSERT INTO ticket_number_counter (counter, counter_uid, create_time)
@@ -296,10 +318,8 @@ router.post('/', async (req, res) => {
[nextCounter] [nextCounter]
); );
const counter = counterResult.rows[0].counter; 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 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) // Determine lock type (1 = unlock by default)
const lockId = 1; 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.ticket_priority_id !== undefined) ticketFields.PriorityID = updates.ticket_priority_id;
if (updates.queue_id !== undefined) ticketFields.QueueID = updates.queue_id; if (updates.queue_id !== undefined) ticketFields.QueueID = updates.queue_id;
if (updates.user_id !== undefined) ticketFields.OwnerID = updates.user_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 // Auto sblocco check
if (updates.ticket_state_id) { if (updates.ticket_state_id) {
@@ -926,7 +948,7 @@ router.patch('/batch/update', async (req, res) => {
try { try {
await client.query('BEGIN'); 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 setClauses = [];
const setParams = []; const setParams = [];
let pIdx = 1; let pIdx = 1;