Compare commits
2
Commits
0f012816ea
...
7b0b29e6c6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b0b29e6c6 | ||
|
|
b3570d6757 |
+18
-12
@@ -1793,21 +1793,22 @@ body {
|
|||||||
top: 100%;
|
top: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
background: #1f2937;
|
background: var(--bg-card);
|
||||||
border: 1px solid #4b5563;
|
border: 1px solid var(--border-light);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
max-height: 220px;
|
max-height: 220px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.5), 0 4px 6px -2px rgba(0, 0, 0, 0.3);
|
box-shadow: var(--shadow-lg);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.autocomplete-suggestion-item {
|
.autocomplete-suggestion-item {
|
||||||
padding: 10px 14px;
|
padding: 10px 14px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 1px solid rgba(156, 163, 175, 0.15);
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: #f3f4f6;
|
color: var(--text-primary);
|
||||||
transition: all var(--transition-fast);
|
transition: all var(--transition-fast);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1817,7 +1818,7 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.autocomplete-suggestion-item:hover span {
|
.autocomplete-suggestion-item:hover span {
|
||||||
color: #e5e7eb !important;
|
color: #ffffff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.autocomplete-suggestion-item:last-child {
|
.autocomplete-suggestion-item:last-child {
|
||||||
@@ -1943,8 +1944,8 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bulk-select option {
|
.bulk-select option {
|
||||||
background-color: #111827;
|
background-color: var(--bg-secondary);
|
||||||
/* Sfondo scuro per gli stati e i tipi */
|
/* Sfondo per gli stati e i tipi */
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1955,23 +1956,24 @@ body {
|
|||||||
width: max-content;
|
width: max-content;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
background: #1f2937;
|
background: var(--bg-card);
|
||||||
border: 1px solid var(--border-light);
|
border: 1px solid var(--border-light);
|
||||||
border-radius: var(--radius-md);
|
border-radius: var(--radius-md);
|
||||||
max-height: 380px;
|
max-height: 380px;
|
||||||
/* Incrementata l'altezza per contenere più elementi */
|
/* Incrementata l'altezza per contenere più elementi */
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.6);
|
box-shadow: var(--shadow-lg);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bulk-suggestions-item {
|
.bulk-suggestions-item {
|
||||||
padding: 6px 10px;
|
padding: 6px 10px;
|
||||||
/* Compatto */
|
/* Compatto */
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 1px solid rgba(156, 163, 175, 0.1);
|
border-bottom: 1px solid var(--border-subtle);
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
color: #e5e7eb;
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bulk-suggestions-item:hover {
|
.bulk-suggestions-item:hover {
|
||||||
@@ -1979,6 +1981,10 @@ body {
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bulk-suggestions-item:hover span {
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
.bulk-suggestions-item:last-child {
|
.bulk-suggestions-item:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
+114
-14
@@ -40,6 +40,10 @@ const TicketDetailView = {
|
|||||||
queue_id: ticket.queue_id,
|
queue_id: ticket.queue_id,
|
||||||
user_id: ticket.user_id,
|
user_id: ticket.user_id,
|
||||||
type_id: ticket.type_id,
|
type_id: ticket.type_id,
|
||||||
|
customer_id: ticket.customer_id,
|
||||||
|
customer_user_id: ticket.customer_user_id,
|
||||||
|
customer_first: ticket.customer_first,
|
||||||
|
customer_last: ticket.customer_last,
|
||||||
};
|
};
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
@@ -113,6 +117,13 @@ const TicketDetailView = {
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}
|
||||||
|
<div class="quick-edit-field" style="position:relative;">
|
||||||
|
<label class="quick-edit-label">Utente Cliente</label>
|
||||||
|
<input type="text" class="form-input" id="qe-customer-search" placeholder="Cerca cliente..." autocomplete="off" value="${ticket.customer_first ? `${ticket.customer_first} ${ticket.customer_last}` : (ticket.customer_user_id || '')}" style="padding: 6px 12px; height: 32px; font-size: 0.85rem;" />
|
||||||
|
<input type="hidden" id="qe-customer-user-id" data-field="customer_user_id" value="${ticket.customer_user_id || ''}" />
|
||||||
|
<input type="hidden" id="qe-customer-id" data-field="customer_id" value="${ticket.customer_id || ''}" />
|
||||||
|
<div id="qe-customer-suggestions" class="autocomplete-suggestions" style="display:none; top: 100%;"></div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="margin-top:var(--space-md);display:flex;gap:var(--space-sm);justify-content:flex-end;">
|
<div style="margin-top:var(--space-md);display:flex;gap:var(--space-sm);justify-content:flex-end;">
|
||||||
<button class="btn btn-ghost btn-sm" id="qe-reset">Reset</button>
|
<button class="btn btn-ghost btn-sm" id="qe-reset">Reset</button>
|
||||||
@@ -306,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',
|
||||||
@@ -320,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;
|
||||||
}
|
}
|
||||||
@@ -340,41 +358,123 @@ const TicketDetailView = {
|
|||||||
|
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
// Quick-edit change detection
|
// Quick-edit change detection
|
||||||
const selects = document.querySelectorAll('.quick-edit-select');
|
const fields = document.querySelectorAll('.quick-edit-select, #qe-customer-user-id, #qe-customer-id');
|
||||||
const saveBtn = document.getElementById('qe-save');
|
const saveBtn = document.getElementById('qe-save');
|
||||||
const resetBtn = document.getElementById('qe-reset');
|
const resetBtn = document.getElementById('qe-reset');
|
||||||
|
|
||||||
const checkChanges = () => {
|
const checkChanges = () => {
|
||||||
let hasChanges = false;
|
let hasChanges = false;
|
||||||
selects.forEach(sel => {
|
fields.forEach(el => {
|
||||||
const field = sel.dataset.field;
|
const field = el.dataset.field;
|
||||||
const original = String(this.originalValues[field] || '');
|
const original = String(this.originalValues[field] || '');
|
||||||
const current = sel.value;
|
const current = el.value;
|
||||||
const changed = current !== original;
|
const changed = current !== original;
|
||||||
sel.classList.toggle('changed', changed);
|
el.classList.toggle('changed', changed);
|
||||||
if (changed) hasChanges = true;
|
if (changed) hasChanges = true;
|
||||||
});
|
});
|
||||||
saveBtn.disabled = !hasChanges;
|
saveBtn.disabled = !hasChanges;
|
||||||
};
|
};
|
||||||
|
|
||||||
selects.forEach(sel => sel.addEventListener('change', checkChanges));
|
fields.forEach(el => el.addEventListener('change', checkChanges));
|
||||||
|
|
||||||
|
// Customer User Autocomplete inside Quick Edit
|
||||||
|
const customerSearchInput = document.getElementById('qe-customer-search');
|
||||||
|
const customerSuggestionsDiv = document.getElementById('qe-customer-suggestions');
|
||||||
|
const customerUserIdInput = document.getElementById('qe-customer-user-id');
|
||||||
|
const customerIdInput = document.getElementById('qe-customer-id');
|
||||||
|
|
||||||
|
let customerDebounce;
|
||||||
|
if (customerSearchInput) {
|
||||||
|
customerSearchInput.addEventListener('input', () => {
|
||||||
|
clearTimeout(customerDebounce);
|
||||||
|
const q = customerSearchInput.value.trim();
|
||||||
|
if (q.length < 2) {
|
||||||
|
customerSuggestionsDiv.style.display = 'none';
|
||||||
|
customerUserIdInput.value = '';
|
||||||
|
customerIdInput.value = '';
|
||||||
|
customerUserIdInput.dispatchEvent(new Event('change'));
|
||||||
|
customerIdInput.dispatchEvent(new Event('change'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
customerDebounce = setTimeout(async () => {
|
||||||
|
try {
|
||||||
|
const users = await App.api(`/api/customer-users/search?q=${encodeURIComponent(q)}`);
|
||||||
|
if (users.length === 0) {
|
||||||
|
customerSuggestionsDiv.innerHTML = '<div class="autocomplete-suggestion-item" style="color:var(--text-muted); cursor:default;">Nessun utente trovato</div>';
|
||||||
|
customerSuggestionsDiv.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
customerSuggestionsDiv.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('');
|
||||||
|
customerSuggestionsDiv.style.display = 'block';
|
||||||
|
|
||||||
|
// Bind click
|
||||||
|
customerSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => {
|
||||||
|
if (item.dataset.login) {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
customerSearchInput.value = item.dataset.name;
|
||||||
|
customerUserIdInput.value = item.dataset.login;
|
||||||
|
customerIdInput.value = item.dataset.customerId || '';
|
||||||
|
customerSuggestionsDiv.style.display = 'none';
|
||||||
|
customerUserIdInput.dispatchEvent(new Event('change'));
|
||||||
|
customerIdInput.dispatchEvent(new Event('change'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close suggestions on click outside
|
||||||
|
document.addEventListener('click', (e) => {
|
||||||
|
if (customerSearchInput && e.target !== customerSearchInput && e.target !== customerSuggestionsDiv) {
|
||||||
|
customerSuggestionsDiv.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Reset quick-edit
|
// Reset quick-edit
|
||||||
resetBtn.addEventListener('click', () => {
|
resetBtn.addEventListener('click', () => {
|
||||||
selects.forEach(sel => {
|
fields.forEach(el => {
|
||||||
sel.value = this.originalValues[sel.dataset.field] || '';
|
el.value = this.originalValues[el.dataset.field] || '';
|
||||||
sel.classList.remove('changed');
|
el.classList.remove('changed');
|
||||||
});
|
});
|
||||||
|
if (customerSearchInput) {
|
||||||
|
const first = this.originalValues['customer_first'] || '';
|
||||||
|
const last = this.originalValues['customer_last'] || '';
|
||||||
|
customerSearchInput.value = first ? `${first} ${last}` : (this.originalValues['customer_user_id'] || '');
|
||||||
|
}
|
||||||
saveBtn.disabled = true;
|
saveBtn.disabled = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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 = {};
|
||||||
selects.forEach(sel => {
|
fields.forEach(el => {
|
||||||
const field = sel.dataset.field;
|
const field = el.dataset.field;
|
||||||
const val = sel.value ? parseInt(sel.value) : null;
|
let val;
|
||||||
if (val !== null && val !== this.originalValues[field]) {
|
if (field === 'customer_id' || field === 'customer_user_id') {
|
||||||
|
val = el.value || null;
|
||||||
|
} else {
|
||||||
|
val = el.value ? parseInt(el.value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const origVal = this.originalValues[field] || null;
|
||||||
|
if (val !== origVal) {
|
||||||
updates[field] = val;
|
updates[field] = val;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
+33
-7
@@ -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;
|
||||||
@@ -483,6 +503,8 @@ router.patch('/:id', async (req, res) => {
|
|||||||
if (updates.type_id !== undefined) ticketFields.TypeID = updates.type_id;
|
if (updates.type_id !== undefined) ticketFields.TypeID = updates.type_id;
|
||||||
if (updates.title !== undefined) ticketFields.Title = updates.title;
|
if (updates.title !== undefined) ticketFields.Title = updates.title;
|
||||||
if (updates.ticket_lock_id !== undefined) ticketFields.LockID = updates.ticket_lock_id;
|
if (updates.ticket_lock_id !== undefined) ticketFields.LockID = updates.ticket_lock_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) {
|
||||||
@@ -525,7 +547,7 @@ router.patch('/:id', async (req, res) => {
|
|||||||
|
|
||||||
// Fetch current ticket for history comparison
|
// Fetch current ticket for history comparison
|
||||||
const currentResult = await client.query(
|
const currentResult = await client.query(
|
||||||
`SELECT ticket_state_id, ticket_priority_id, queue_id, user_id, type_id, title, ticket_lock_id
|
`SELECT ticket_state_id, ticket_priority_id, queue_id, user_id, type_id, title, ticket_lock_id, customer_id, customer_user_id
|
||||||
FROM ticket WHERE id = $1`,
|
FROM ticket WHERE id = $1`,
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
@@ -557,7 +579,7 @@ router.patch('/:id', async (req, res) => {
|
|||||||
const setParams = [];
|
const setParams = [];
|
||||||
let pIdx = 1;
|
let pIdx = 1;
|
||||||
|
|
||||||
const allowedFields = ['ticket_state_id', 'ticket_priority_id', 'queue_id', 'user_id', 'type_id', 'title', 'ticket_lock_id'];
|
const allowedFields = ['ticket_state_id', 'ticket_priority_id', 'queue_id', 'user_id', 'type_id', 'title', 'ticket_lock_id', 'customer_id', 'customer_user_id'];
|
||||||
for (const field of allowedFields) {
|
for (const field of allowedFields) {
|
||||||
if (updates[field] !== undefined && updates[field] !== current[field]) {
|
if (updates[field] !== undefined && updates[field] !== current[field]) {
|
||||||
setClauses.push(`${field} = $${pIdx++}`);
|
setClauses.push(`${field} = $${pIdx++}`);
|
||||||
@@ -589,6 +611,8 @@ router.patch('/:id', async (req, res) => {
|
|||||||
user_id: 'OwnerUpdate',
|
user_id: 'OwnerUpdate',
|
||||||
type_id: 'TypeUpdate',
|
type_id: 'TypeUpdate',
|
||||||
ticket_lock_id: 'Lock',
|
ticket_lock_id: 'Lock',
|
||||||
|
customer_id: 'CustomerUpdate',
|
||||||
|
customer_user_id: 'CustomerUpdate',
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const field of allowedFields) {
|
for (const field of allowedFields) {
|
||||||
@@ -881,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) {
|
||||||
@@ -922,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;
|
||||||
|
|||||||
Reference in New Issue
Block a user