feat: aggiunta possiblità di modificare il cliente
This commit is contained in:
+18
-12
@@ -1793,21 +1793,22 @@ body {
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #1f2937;
|
||||
border: 1px solid #4b5563;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-md);
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
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 {
|
||||
padding: 10px 14px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid rgba(156, 163, 175, 0.15);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
font-size: 0.85rem;
|
||||
color: #f3f4f6;
|
||||
color: var(--text-primary);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
@@ -1817,7 +1818,7 @@ body {
|
||||
}
|
||||
|
||||
.autocomplete-suggestion-item:hover span {
|
||||
color: #e5e7eb !important;
|
||||
color: #ffffff !important;
|
||||
}
|
||||
|
||||
.autocomplete-suggestion-item:last-child {
|
||||
@@ -1943,8 +1944,8 @@ body {
|
||||
}
|
||||
|
||||
.bulk-select option {
|
||||
background-color: #111827;
|
||||
/* Sfondo scuro per gli stati e i tipi */
|
||||
background-color: var(--bg-secondary);
|
||||
/* Sfondo per gli stati e i tipi */
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@@ -1955,23 +1956,24 @@ body {
|
||||
width: max-content;
|
||||
min-width: 100%;
|
||||
max-width: 400px;
|
||||
background: #1f2937;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-md);
|
||||
max-height: 380px;
|
||||
/* Incrementata l'altezza per contenere più elementi */
|
||||
overflow-y: auto;
|
||||
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 {
|
||||
padding: 6px 10px;
|
||||
/* Compatto */
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid rgba(156, 163, 175, 0.1);
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
font-size: 0.8rem;
|
||||
color: #e5e7eb;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.bulk-suggestions-item:hover {
|
||||
@@ -1979,6 +1981,10 @@ body {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.bulk-suggestions-item:hover span {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.bulk-suggestions-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
+100
-13
@@ -40,6 +40,10 @@ const TicketDetailView = {
|
||||
queue_id: ticket.queue_id,
|
||||
user_id: ticket.user_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 = `
|
||||
@@ -113,6 +117,13 @@ const TicketDetailView = {
|
||||
</select>
|
||||
</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 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>
|
||||
@@ -340,41 +351,117 @@ const TicketDetailView = {
|
||||
|
||||
bindEvents() {
|
||||
// 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 resetBtn = document.getElementById('qe-reset');
|
||||
|
||||
const checkChanges = () => {
|
||||
let hasChanges = false;
|
||||
selects.forEach(sel => {
|
||||
const field = sel.dataset.field;
|
||||
fields.forEach(el => {
|
||||
const field = el.dataset.field;
|
||||
const original = String(this.originalValues[field] || '');
|
||||
const current = sel.value;
|
||||
const current = el.value;
|
||||
const changed = current !== original;
|
||||
sel.classList.toggle('changed', changed);
|
||||
el.classList.toggle('changed', changed);
|
||||
if (changed) hasChanges = true;
|
||||
});
|
||||
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
|
||||
resetBtn.addEventListener('click', () => {
|
||||
selects.forEach(sel => {
|
||||
sel.value = this.originalValues[sel.dataset.field] || '';
|
||||
sel.classList.remove('changed');
|
||||
fields.forEach(el => {
|
||||
el.value = this.originalValues[el.dataset.field] || '';
|
||||
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;
|
||||
});
|
||||
|
||||
// Save quick-edit
|
||||
saveBtn.addEventListener('click', async () => {
|
||||
const updates = {};
|
||||
selects.forEach(sel => {
|
||||
const field = sel.dataset.field;
|
||||
const val = sel.value ? parseInt(sel.value) : null;
|
||||
if (val !== null && val !== this.originalValues[field]) {
|
||||
fields.forEach(el => {
|
||||
const field = el.dataset.field;
|
||||
let val;
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
+6
-2
@@ -483,6 +483,8 @@ router.patch('/:id', async (req, res) => {
|
||||
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;
|
||||
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) {
|
||||
@@ -525,7 +527,7 @@ router.patch('/:id', async (req, res) => {
|
||||
|
||||
// Fetch current ticket for history comparison
|
||||
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`,
|
||||
[id]
|
||||
);
|
||||
@@ -557,7 +559,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'];
|
||||
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) {
|
||||
if (updates[field] !== undefined && updates[field] !== current[field]) {
|
||||
setClauses.push(`${field} = $${pIdx++}`);
|
||||
@@ -589,6 +591,8 @@ router.patch('/:id', async (req, res) => {
|
||||
user_id: 'OwnerUpdate',
|
||||
type_id: 'TypeUpdate',
|
||||
ticket_lock_id: 'Lock',
|
||||
customer_id: 'CustomerUpdate',
|
||||
customer_user_id: 'CustomerUpdate',
|
||||
};
|
||||
|
||||
for (const field of allowedFields) {
|
||||
|
||||
Reference in New Issue
Block a user