Aggiunte stored procedure più utili le altre pfff

This commit is contained in:
2026-07-05 18:48:29 +02:00
parent 457c3eacf6
commit 4f41b50d37
10 changed files with 1578 additions and 156 deletions
+718
View File
@@ -0,0 +1,718 @@
/**
* Ticket Bulk Creation View
* Tabular layout for creating multiple tickets.
*/
const TicketBulkView = {
rowCount: 0,
activeRequests: false,
async render() {
const container = document.getElementById('view-container');
container.innerHTML = '<div class="loading-screen"><div class="spinner"></div><p>Caricamento dati...</p></div>';
try {
await App.ensureLookups();
// Check if we have incoming query parameters (e.g. from Teams integration)
const hashParts = window.location.hash.split('?');
const searchStr = hashParts.length > 1 ? hashParts[1] : window.location.search;
const urlParams = new URLSearchParams(searchStr);
const incomingBody = urlParams.get('body');
const incomingCustomer = urlParams.get('customer');
const incomingSubject = urlParams.get('subject');
if (incomingBody || incomingCustomer || incomingSubject) {
let foundCustomer = null;
if (incomingCustomer) {
try {
const results = await App.api(`/api/customer-users/search?q=${encodeURIComponent(incomingCustomer)}`);
if (results && results.length > 0) {
foundCustomer = results[0];
}
} catch (e) {
console.warn('Failed to search customer for Teams integration:', e);
}
}
const initialRowData = {
state_id: '1',
title: 'Segnalazione da Teams',
queue_name: App.lookups.queues && App.lookups.queues.length > 0 ? App.lookups.queues[0].name : '',
queue_id: App.lookups.queues && App.lookups.queues.length > 0 ? App.lookups.queues[0].id : '',
type_id: '',
owner_name: '',
owner_id: '',
responsible_name: '',
responsible_id: '',
customer_search: foundCustomer ? `${foundCustomer.first_name} ${foundCustomer.last_name}` : (incomingCustomer || ''),
customer_id: foundCustomer ? foundCustomer.customer_id : '',
customer_user_id: foundCustomer ? foundCustomer.login : (incomingCustomer || ''),
subject: incomingSubject || 'Messaggio da Microsoft Teams',
body: incomingBody || '',
priority_id: '3',
company_display: foundCustomer ? foundCustomer.customer_id : '',
isExpanded: true,
statusDotClass: '',
statusDotTitle: ''
};
if (!this.savedRows) this.savedRows = [];
this.savedRows.unshift(initialRowData);
// Clean query parameters from URL to prevent duplicate inserts on reload
window.history.replaceState({}, document.title, window.location.pathname + window.location.hash.split('?')[0]);
}
this.rowCount = 0;
container.innerHTML = `
<div class="card" style="padding: var(--space-md); overflow: visible;">
<div class="card-title" style="display:flex; justify-content:space-between; align-items:center; margin-bottom:var(--space-md);">
<span>Apertura Massiva Ticket</span>
<span style="font-size:0.75rem; color:var(--text-tertiary); font-weight:normal; text-transform:none;">
Usa il tasto <kbd style="background:rgba(255,255,255,0.1); padding:2px 6px; border-radius:3px;">Tab</kbd> per spostarti e creare nuove righe in automatico.
</span>
</div>
<div class="bulk-grid-wrapper">
<table class="bulk-grid-table">
<thead>
<tr>
<th style="width:40px; text-align:center;">#</th>
<th style="width:120px;">Stato *</th>
<th style="width:200px;">Titolo *</th>
<th style="width:540px;">Coda *</th>
<th style="width:110px;">Tipo</th>
<th style="width:260px;">Owner</th>
<th style="width:260px;">Responsabile</th>
<th style="width:300px;">Cliente *</th>
<th style="width:200px;">Oggetto</th>
<th style="width:200px;">Messaggio</th>
<th style="width:90px; text-align:center;">Azioni</th>
</tr>
</thead>
<tbody id="bulk-rows-container">
<!-- Rows will be added dynamically here -->
</tbody>
</table>
</div>
<div class="bulk-actions-container">
<div>
<button class="btn btn-ghost btn-sm" id="bulk-add-row-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;">
<path d="M12 5v14M5 12h14"/>
</svg>
Aggiungi riga
</button>
</div>
<div style="display:flex; gap:var(--space-md); align-items:center;">
<button class="btn btn-ghost btn-sm" onclick="window.location.hash='#/tickets'">Annulla</button>
<button class="btn btn-primary" id="bulk-submit-btn">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:18px;height:18px;">
<path d="M5 12l5 5L20 7"/>
</svg>
Conferma e Invia
</button>
</div>
</div>
</div>
`;
// Add event listeners
document.getElementById('bulk-add-row-btn').addEventListener('click', () => this.addRow());
document.getElementById('bulk-submit-btn').addEventListener('click', () => this.submitBulk());
// Add first row or restore saved rows
if (this.savedRows && this.savedRows.length > 0) {
this.savedRows.forEach(rowData => this.addRow(rowData));
} else {
this.addRow();
}
} catch (err) {
console.error('Error rendering bulk view:', err);
container.innerHTML = `<div class="error-screen"><p>Errore caricamento: ${err.message}</p></div>`;
}
},
addRow(savedData = null) {
this.rowCount++;
const id = this.rowCount;
const container = document.getElementById('bulk-rows-container');
const tr = document.createElement('tr');
tr.id = `bulk-row-${id}`;
tr.className = 'bulk-row';
tr.innerHTML = `
<td style="text-align:center; font-weight:600; color:var(--text-tertiary);">
<span class="row-status-dot" id="status-dot-${id}"></span><span class="row-num">${id}</span>
</td>
<td>
<select class="bulk-select" id="bulk-state-${id}">
${(App.lookups.states || []).map(s => {
const sel = s.type_name === 'new' ? 'selected' : '';
return `<option value="${s.id}" ${sel}>${s.name}</option>`;
}).join('')}
</select>
</td>
<td>
<input type="text" class="bulk-input" id="bulk-title-${id}" placeholder="Titolo ticket" autocomplete="new-password" required />
</td>
<td>
<input type="text" class="bulk-input" id="bulk-queue-search-${id}" placeholder="Coda..." autocomplete="new-password" />
<input type="hidden" id="bulk-queue-${id}" />
<div id="bulk-queue-suggestions-${id}" class="bulk-suggestions" style="display:none;"></div>
</td>
<td>
<select class="bulk-select" id="bulk-type-${id}">
<option value="">—</option>
${(App.lookups.types || []).map(t => `<option value="${t.id}">${t.name}</option>`).join('')}
</select>
</td>
<td>
<input type="text" class="bulk-input" id="bulk-owner-search-${id}" placeholder="Cerca owner..." autocomplete="new-password" />
<input type="hidden" id="bulk-owner-${id}" />
<div id="bulk-owner-suggestions-${id}" class="bulk-suggestions" style="display:none;"></div>
</td>
<td>
<input type="text" class="bulk-input" id="bulk-responsible-search-${id}" placeholder="Cerca responsabile..." autocomplete="new-password" />
<input type="hidden" id="bulk-responsible-${id}" />
<div id="bulk-responsible-suggestions-${id}" class="bulk-suggestions" style="display:none;"></div>
</td>
<td>
<input type="text" class="bulk-input" id="bulk-customer-search-${id}" placeholder="Nome, email, login..." autocomplete="new-password" required />
<input type="hidden" id="bulk-customer-${id}" />
<input type="hidden" id="bulk-customer-user-id-${id}" />
<div id="bulk-customer-suggestions-${id}" class="bulk-suggestions" style="display:none;"></div>
</td>
<td>
<input type="text" class="bulk-input" id="bulk-subject-${id}" placeholder="Oggetto nota" autocomplete="new-password" />
</td>
<td>
<div class="bulk-textarea-container">
<textarea class="bulk-textarea" id="bulk-body-${id}" placeholder="Scrivi nota..."></textarea>
</div>
</td>
<td style="text-align:center;">
<div class="row-actions">
<button class="btn btn-ghost btn-xs" id="bulk-expand-btn-${id}" title="Opzioni avanzate">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;">
<path d="M12 5v14M5 12h14"/>
</svg>
</button>
<button class="btn btn-danger btn-xs" id="bulk-delete-btn-${id}" title="Rimuovi riga">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;">
<path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg>
</button>
</div>
</td>
`;
// Collapsible advanced row
const trExp = document.createElement('tr');
trExp.id = `bulk-row-expanded-${id}`;
trExp.className = 'bulk-row-expanded';
trExp.style.display = 'none';
trExp.innerHTML = `
<td colspan="11">
<div class="bulk-row-expanded-content">
<div class="form-group" style="margin-bottom:0;">
<label class="form-label" style="font-size:0.75rem;">Priorità</label>
<select class="form-select" id="bulk-priority-${id}" style="height:32px; font-size:0.8rem; padding: 4px 8px; margin:0;">
${(App.lookups.priorities || []).map(p => {
const sel = p.id === 3 ? 'selected' : '';
return `<option value="${p.id}" ${sel}>${p.name}</option>`;
}).join('')}
</select>
</div>
<div class="form-group" style="margin-bottom:0;">
<label class="form-label" style="font-size:0.75rem;">Azienda Cliente (Società)</label>
<input type="text" class="form-input" id="bulk-company-display-${id}" readonly disabled placeholder="Rilevata dal cliente" style="height:32px; font-size:0.8rem; background:rgba(255,255,255,0.05); margin:0;" />
</div>
</div>
</td>
`;
container.appendChild(tr);
container.appendChild(trExp);
// Populate with savedData if available, otherwise set default values
if (savedData) {
tr.querySelector(`#bulk-state-${id}`).value = savedData.state_id;
tr.querySelector(`#bulk-title-${id}`).value = savedData.title;
tr.querySelector(`#bulk-queue-search-${id}`).value = savedData.queue_name;
tr.querySelector(`#bulk-queue-${id}`).value = savedData.queue_id;
tr.querySelector(`#bulk-type-${id}`).value = savedData.type_id;
tr.querySelector(`#bulk-owner-search-${id}`).value = savedData.owner_name;
tr.querySelector(`#bulk-owner-${id}`).value = savedData.owner_id;
tr.querySelector(`#bulk-responsible-search-${id}`).value = savedData.responsible_name;
tr.querySelector(`#bulk-responsible-${id}`).value = savedData.responsible_id;
tr.querySelector(`#bulk-customer-search-${id}`).value = savedData.customer_search;
tr.querySelector(`#bulk-customer-${id}`).value = savedData.customer_id;
tr.querySelector(`#bulk-customer-user-id-${id}`).value = savedData.customer_user_id;
tr.querySelector(`#bulk-subject-${id}`).value = savedData.subject;
tr.querySelector(`#bulk-body-${id}`).value = savedData.body;
trExp.querySelector(`#bulk-priority-${id}`).value = savedData.priority_id;
trExp.querySelector(`#bulk-company-display-${id}`).value = savedData.company_display;
if (savedData.isExpanded) {
trExp.style.display = 'table-row';
const btn = tr.querySelector(`#bulk-expand-btn-${id}`);
btn.innerHTML = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><path d="M5 12h14"/></svg>`;
}
const dot = tr.querySelector(`#status-dot-${id}`);
if (savedData.statusDotClass) dot.className = savedData.statusDotClass;
if (savedData.statusDotTitle) dot.title = savedData.statusDotTitle;
} else {
// Default queue pre-population
const defaultQueueInput = tr.querySelector(`#bulk-queue-search-${id}`);
const defaultQueueHidden = tr.querySelector(`#bulk-queue-${id}`);
if (App.lookups.queues && App.lookups.queues.length > 0) {
defaultQueueInput.value = App.lookups.queues[0].name;
defaultQueueHidden.value = App.lookups.queues[0].id;
}
// Default owner and responsible (Active Agent)
const activeAgentId = localStorage.getItem('activeAgentId') || '1';
const activeAgent = (App.lookups.users || []).find(u => String(u.id) === String(activeAgentId));
if (activeAgent) {
const agentName = `${activeAgent.first_name} ${activeAgent.last_name}`;
tr.querySelector(`#bulk-owner-search-${id}`).value = agentName;
tr.querySelector(`#bulk-owner-${id}`).value = activeAgent.id;
tr.querySelector(`#bulk-responsible-search-${id}`).value = agentName;
tr.querySelector(`#bulk-responsible-${id}`).value = activeAgent.id;
}
}
// Toggle expand button
tr.querySelector(`#bulk-expand-btn-${id}`).addEventListener('click', (e) => {
e.preventDefault();
const isHidden = trExp.style.display === 'none';
trExp.style.display = isHidden ? 'table-row' : 'none';
const btn = tr.querySelector(`#bulk-expand-btn-${id}`);
btn.innerHTML = isHidden
? `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><path d="M5 12h14"/></svg>`
: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><path d="M12 5v14M5 12h14"/></svg>`;
this.saveState();
});
// Delete button
tr.querySelector(`#bulk-delete-btn-${id}`).addEventListener('click', (e) => {
e.preventDefault();
tr.remove();
trExp.remove();
this.reorderRows();
this.saveState();
});
// Setup autocompletes
this.setupAutocomplete(id, 'queue', `bulk-queue-search-${id}`, `bulk-queue-${id}`, `bulk-queue-suggestions-${id}`);
this.setupAutocomplete(id, 'owner', `bulk-owner-search-${id}`, `bulk-owner-${id}`, `bulk-owner-suggestions-${id}`);
this.setupAutocomplete(id, 'responsible', `bulk-responsible-search-${id}`, `bulk-responsible-${id}`, `bulk-responsible-suggestions-${id}`);
this.setupAutocomplete(id, 'customer', `bulk-customer-search-${id}`, `bulk-customer-${id}`, `bulk-customer-suggestions-${id}`);
// Setup Tab key navigation on the last field (body textarea)
const textarea = tr.querySelector(`#bulk-body-${id}`);
textarea.addEventListener('keydown', (e) => {
if (e.key === 'Tab' && !e.shiftKey) {
// Check if this is the last visible bulk-row textarea
const activeRows = container.querySelectorAll('.bulk-row');
const lastRow = activeRows[activeRows.length - 1];
if (tr === lastRow) {
e.preventDefault();
this.addRow();
// Focus the next row's first input (Title)
setTimeout(() => {
const nextId = this.rowCount;
document.getElementById(`bulk-title-${nextId}`)?.focus();
}, 50);
}
}
});
// Scroll table to make inputs and textareas fully visible when focused
tr.querySelectorAll('input, select, textarea').forEach(input => {
input.addEventListener('focus', () => {
const wrapper = document.querySelector('.bulk-grid-wrapper');
if (wrapper) {
setTimeout(() => {
const rect = input.getBoundingClientRect();
const wrapperRect = wrapper.getBoundingClientRect();
if (rect.right > wrapperRect.right) {
wrapper.scrollBy({ left: rect.right - wrapperRect.right + 40, behavior: 'smooth' });
} else if (rect.left < wrapperRect.left) {
wrapper.scrollBy({ left: rect.left - wrapperRect.left - 40, behavior: 'smooth' });
}
if (rect.bottom > wrapperRect.bottom) {
wrapper.scrollBy({ top: rect.bottom - wrapperRect.bottom + 40, behavior: 'smooth' });
} else if (rect.top < wrapperRect.top) {
wrapper.scrollBy({ top: rect.top - wrapperRect.top - 40, behavior: 'smooth' });
}
}, 150);
}
});
});
// Save state on any input change
tr.querySelectorAll('input, select, textarea').forEach(input => {
input.addEventListener('input', () => this.saveState());
input.addEventListener('change', () => this.saveState());
});
trExp.querySelectorAll('input, select, textarea').forEach(input => {
input.addEventListener('input', () => this.saveState());
input.addEventListener('change', () => this.saveState());
});
this.saveState();
},
reorderRows() {
const container = document.getElementById('bulk-rows-container');
const rows = container.querySelectorAll('.bulk-row');
rows.forEach((row, index) => {
const numSpan = row.querySelector('.row-num');
if (numSpan) numSpan.textContent = index + 1;
});
},
saveState() {
const container = document.getElementById('bulk-rows-container');
if (!container) return;
const rows = container.querySelectorAll('.bulk-row');
const data = [];
rows.forEach(row => {
const id = row.id.replace('bulk-row-', '');
const state_id = document.getElementById(`bulk-state-${id}`).value;
const title = document.getElementById(`bulk-title-${id}`).value;
const queue_name = document.getElementById(`bulk-queue-search-${id}`).value;
const queue_id = document.getElementById(`bulk-queue-${id}`).value;
const type_id = document.getElementById(`bulk-type-${id}`).value;
const owner_name = document.getElementById(`bulk-owner-search-${id}`).value;
const owner_id = document.getElementById(`bulk-owner-${id}`).value;
const responsible_name = document.getElementById(`bulk-responsible-search-${id}`).value;
const responsible_id = document.getElementById(`bulk-responsible-${id}`).value;
const customer_search = document.getElementById(`bulk-customer-search-${id}`).value;
const customer_id = document.getElementById(`bulk-customer-${id}`).value;
const customer_user_id = document.getElementById(`bulk-customer-user-id-${id}`).value;
const subject = document.getElementById(`bulk-subject-${id}`).value;
const body = document.getElementById(`bulk-body-${id}`).value;
const priority_id = document.getElementById(`bulk-priority-${id}`).value;
const company_display = document.getElementById(`bulk-company-display-${id}`).value;
const trExp = document.getElementById(`bulk-row-expanded-${id}`);
const isExpanded = trExp ? trExp.style.display !== 'none' : false;
const statusDot = document.getElementById(`status-dot-${id}`);
const statusDotClass = statusDot ? statusDot.className : '';
const statusDotTitle = statusDot ? statusDot.title : '';
data.push({
state_id,
title,
queue_name,
queue_id,
type_id,
owner_name,
owner_id,
responsible_name,
responsible_id,
customer_search,
customer_id,
customer_user_id,
subject,
body,
priority_id,
company_display,
isExpanded,
statusDotClass,
statusDotTitle
});
});
this.savedRows = data;
},
setupAutocomplete(rowId, type, searchInputId, hiddenInputId, suggestionsDivId) {
const input = document.getElementById(searchInputId);
const hidden = document.getElementById(hiddenInputId);
const suggestions = document.getElementById(suggestionsDivId);
if (!input || !suggestions) return;
let debounce;
const adjustSuggestionsScroll = () => {
setTimeout(() => {
const wrapper = document.querySelector('.bulk-grid-wrapper');
if (wrapper) {
const rect = suggestions.getBoundingClientRect();
const wrapperRect = wrapper.getBoundingClientRect();
if (rect.right > wrapperRect.right) {
wrapper.scrollBy({ left: rect.right - wrapperRect.right + 40, behavior: 'smooth' });
} else if (rect.left < wrapperRect.left) {
wrapper.scrollBy({ left: rect.left - wrapperRect.left - 40, behavior: 'smooth' });
}
if (rect.bottom > wrapperRect.bottom) {
wrapper.scrollBy({ top: rect.bottom - wrapperRect.bottom + 40, behavior: 'smooth' });
} else if (rect.top < wrapperRect.top) {
wrapper.scrollBy({ top: rect.top - wrapperRect.top - 40, behavior: 'smooth' });
}
}
}, 50);
};
const renderResults = (results) => {
if (results.length === 0) {
suggestions.innerHTML = '<div class="bulk-suggestions-item" style="color:var(--text-muted); cursor:default;">Nessun risultato</div>';
suggestions.style.display = 'block';
adjustSuggestionsScroll();
return;
}
suggestions.innerHTML = results.map(r => {
if (type === 'queue') {
return `<div class="bulk-suggestions-item" data-id="${r.id}" data-name="${App.escapeHtml(r.name)}">${App.escapeHtml(r.name)}</div>`;
} else if (type === 'owner' || type === 'responsible') {
return `<div class="bulk-suggestions-item" data-id="${r.id}" data-name="${App.escapeHtml(r.first_name + ' ' + r.last_name)}"><strong>${App.escapeHtml(r.first_name)} ${App.escapeHtml(r.last_name)}</strong> <span style="font-size:0.75rem; color:var(--text-muted);">(${r.login})</span></div>`;
} else if (type === 'customer') {
return `<div class="bulk-suggestions-item" data-login="${App.escapeHtml(r.login)}" data-customer-id="${App.escapeHtml(r.customer_id || '')}" data-name="${App.escapeHtml(r.first_name + ' ' + r.last_name)}"><strong>${App.escapeHtml(r.first_name)} ${App.escapeHtml(r.last_name)}</strong> <span style="font-size:0.75rem; color:var(--text-muted);">(${r.login} | ${r.customer_id || '—'})</span></div>`;
}
}).join('');
suggestions.style.display = 'block';
adjustSuggestionsScroll();
// Bind clicks
suggestions.querySelectorAll('.bulk-suggestions-item').forEach(item => {
item.addEventListener('click', () => {
if (type === 'queue' || type === 'owner' || type === 'responsible') {
input.value = item.dataset.name;
hidden.value = item.dataset.id;
} else if (type === 'customer') {
input.value = item.dataset.name;
hidden.value = item.dataset.customerId || '';
document.getElementById(`bulk-customer-user-id-${rowId}`).value = item.dataset.login;
document.getElementById(`bulk-company-display-${rowId}`).value = item.dataset.customerId || '';
}
suggestions.style.display = 'none';
});
});
};
const showAllLocalOrQuery = async () => {
if (type === 'queue') {
renderResults(App.lookups.queues || []);
} else if (type === 'owner' || type === 'responsible') {
renderResults(App.lookups.users || []);
} else if (type === 'customer') {
try {
const results = await App.api(`/api/customer-users/search?q=`);
renderResults(results);
} catch (err) {
console.error(err);
}
}
};
input.addEventListener('input', () => {
clearTimeout(debounce);
const q = input.value.trim();
// Clear hidden values if user deletes text
if (q.length === 0) {
hidden.value = '';
if (type === 'customer') {
document.getElementById(`bulk-customer-user-id-${rowId}`).value = '';
document.getElementById(`bulk-company-display-${rowId}`).value = '';
}
showAllLocalOrQuery();
return;
}
if (type !== 'queue' && type !== 'owner' && type !== 'responsible' && type !== 'customer' && q.length < 2) {
suggestions.style.display = 'none';
return;
}
// Local filtering for fast performance on loaded lookups
if (type === 'queue' || type === 'owner' || type === 'responsible') {
const lowerQ = q.toLowerCase();
let filtered = [];
if (type === 'queue') {
filtered = (App.lookups.queues || []).filter(r => r.name.toLowerCase().includes(lowerQ));
} else {
filtered = (App.lookups.users || []).filter(r =>
r.first_name.toLowerCase().includes(lowerQ) ||
r.last_name.toLowerCase().includes(lowerQ) ||
r.login.toLowerCase().includes(lowerQ)
);
}
renderResults(filtered);
return;
}
// API search for customer users
debounce = setTimeout(async () => {
try {
const results = await App.api(`/api/customer-users/search?q=${encodeURIComponent(q)}`);
renderResults(results);
} catch (err) {
console.error(err);
}
}, 250);
});
// Close suggestions clicking outside
document.addEventListener('click', (e) => {
if (e.target !== input && e.target !== suggestions) {
suggestions.style.display = 'none';
}
});
// Close suggestions on blur (tab navigation out of input)
input.addEventListener('blur', () => {
setTimeout(() => {
suggestions.style.display = 'none';
}, 200);
});
// Empty input and show list on focus/click
input.addEventListener('focus', () => {
if (type === 'queue' || type === 'owner' || type === 'responsible' || type === 'customer') {
input.value = '';
hidden.value = '';
if (type === 'customer') {
document.getElementById(`bulk-customer-user-id-${rowId}`).value = '';
document.getElementById(`bulk-company-display-${rowId}`).value = '';
}
showAllLocalOrQuery();
}
});
},
async submitBulk() {
if (this.activeRequests) return;
const container = document.getElementById('bulk-rows-container');
const rows = container.querySelectorAll('.bulk-row');
if (rows.length === 0) {
Toast.warning('Nessun ticket da creare');
return;
}
// Validation pass
const ticketsToSend = [];
let hasValidationError = false;
rows.forEach(row => {
const id = row.id.replace('bulk-row-', '');
const state_id = document.getElementById(`bulk-state-${id}`).value;
const title = document.getElementById(`bulk-title-${id}`).value.trim();
const queue_id = document.getElementById(`bulk-queue-${id}`).value;
const queue_name = document.getElementById(`bulk-queue-search-${id}`).value;
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;
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;
// Reset status indicators
const dot = document.getElementById(`status-dot-${id}`);
dot.className = 'row-status-dot';
if (!title) {
Toast.warning(`Titolo obbligatorio alla riga ${id}`);
document.getElementById(`bulk-title-${id}`).focus();
hasValidationError = true;
return;
}
if (!queue_id) {
Toast.warning(`Seleziona una coda valida alla riga ${id}`);
document.getElementById(`bulk-queue-search-${id}`).focus();
hasValidationError = true;
return;
}
if (!customerUserId && !customerId) {
Toast.warning(`Seleziona un Utente Cliente valido alla riga ${id}`);
document.getElementById(`bulk-customer-search-${id}`).focus();
hasValidationError = true;
return;
}
ticketsToSend.push({
id,
payload: {
title,
queue_id: parseInt(queue_id),
state_id: parseInt(state_id),
priority_id: parseInt(priority_id),
user_id: ownerId ? parseInt(ownerId) : undefined,
responsible_user_id: responsibleId ? parseInt(responsibleId) : undefined,
type_id: type_id ? parseInt(type_id) : undefined,
customer_id: customerId || undefined,
customer_user_id: customerUserId || undefined,
subject: subject || undefined,
body: body || undefined
}
});
});
if (hasValidationError) return;
// Send tickets one by one
this.activeRequests = true;
const submitBtn = document.getElementById('bulk-submit-btn');
const origBtnHtml = submitBtn.innerHTML;
submitBtn.disabled = true;
Toast.info(`Inizio invio di ${ticketsToSend.length} ticket...`);
let successCount = 0;
let failCount = 0;
for (const ticket of ticketsToSend) {
const dot = document.getElementById(`status-dot-${ticket.id}`);
dot.className = 'row-status-dot pending';
try {
submitBtn.innerHTML = `<div class="spinner" style="width:16px;height:16px;border-width:2px;"></div> Invio riga ${ticket.id}...`;
const result = await App.api('/api/tickets', {
method: 'POST',
body: JSON.stringify(ticket.payload)
});
dot.className = 'row-status-dot success';
dot.title = `Successo: Ticket #${result.tn}`;
successCount++;
} catch (err) {
console.error(`Error creating bulk ticket row ${ticket.id}:`, err);
dot.className = 'row-status-dot error';
dot.title = `Errore: ${err.message}`;
failCount++;
}
}
this.activeRequests = false;
submitBtn.disabled = false;
submitBtn.innerHTML = origBtnHtml;
if (failCount === 0) {
Toast.success(`Tutti i ${successCount} ticket sono stati creati con successo!`);
this.savedRows = [];
setTimeout(() => {
window.location.hash = '#/tickets';
}, 1500);
} else {
this.saveState();
Toast.warning(`Invio completato: ${successCount} creati, ${failCount} falliti. Controlla i pallini colorati per i dettagli.`);
}
}
};
+155 -45
View File
@@ -3,6 +3,32 @@
* Minimal, fast form for creating new tickets.
*/
const TicketCreateView = {
savedState: null,
saveState() {
const stateEl = document.getElementById('create-state');
if (!stateEl) return;
this.savedState = {
state_id: stateEl.value,
title: document.getElementById('create-title').value,
queue_name: document.getElementById('create-queue-search').value,
queue_id: document.getElementById('create-queue').value,
type_id: document.getElementById('create-type').value,
owner_name: document.getElementById('create-owner-search').value,
owner_id: document.getElementById('create-owner').value,
responsible_name: document.getElementById('create-responsible-search').value,
responsible_id: document.getElementById('create-responsible').value,
user_search: document.getElementById('create-user-search').value,
customer_user_id: document.getElementById('create-customer-user-id').value,
customer_id: document.getElementById('create-customer-id').value,
company_search: document.getElementById('create-company-search').value,
priority_id: document.getElementById('create-priority').value,
subject: document.getElementById('create-subject').value,
body: document.getElementById('create-body').value,
isAdvancedVisible: document.getElementById('advanced-options').style.display !== 'none'
};
},
async render() {
const container = document.getElementById('view-container');
container.innerHTML = '<div class="loading-screen"><div class="spinner"></div><p>Caricamento form...</p></div>';
@@ -10,6 +36,57 @@ const TicketCreateView = {
try {
await App.ensureLookups();
// Check if we have incoming query parameters (e.g. from Teams integration)
const hashParts = window.location.hash.split('?');
const searchStr = hashParts.length > 1 ? hashParts[1] : window.location.search;
const urlParams = new URLSearchParams(searchStr);
const incomingBody = urlParams.get('body');
const incomingCustomer = urlParams.get('customer');
const incomingSubject = urlParams.get('subject');
if (incomingBody || incomingCustomer || incomingSubject) {
let foundCustomer = null;
if (incomingCustomer) {
try {
const results = await App.api(`/api/customer-users/search?q=${encodeURIComponent(incomingCustomer)}`);
if (results && results.length > 0) {
foundCustomer = results[0];
}
} catch (e) {
console.warn('Failed to search customer for Teams integration:', e);
}
}
// Prepopulate default active agent details
const currentAgentId = localStorage.getItem('activeAgentId') || '1';
const activeAgent = (App.lookups.users || []).find(u => String(u.id) === String(currentAgentId));
const agentName = activeAgent ? `${activeAgent.first_name} ${activeAgent.last_name}` : '';
const agentId = activeAgent ? activeAgent.id : '';
this.savedState = {
state_id: '1', // default state
title: 'Segnalazione da Teams',
queue_name: App.lookups.queues && App.lookups.queues.length > 0 ? App.lookups.queues[0].name : '',
queue_id: App.lookups.queues && App.lookups.queues.length > 0 ? App.lookups.queues[0].id : '',
type_id: '',
owner_name: agentName,
owner_id: agentId,
responsible_name: agentName,
responsible_id: agentId,
user_search: foundCustomer ? `${foundCustomer.first_name} ${foundCustomer.last_name} <${foundCustomer.email}>` : (incomingCustomer || ''),
customer_user_id: foundCustomer ? foundCustomer.login : (incomingCustomer || ''),
customer_id: foundCustomer ? foundCustomer.customer_id : '',
company_search: foundCustomer ? foundCustomer.customer_id : '',
priority_id: '3',
subject: incomingSubject || 'Messaggio da Microsoft Teams',
body: incomingBody || '',
isAdvancedVisible: false
};
// Clean query parameters from URL to prevent duplicate inserts on reload
window.history.replaceState({}, document.title, window.location.pathname + window.location.hash.split('?')[0]);
}
container.innerHTML = `
<a class="back-link" onclick="history.back()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;"><path d="M19 12H5M12 19l-7-7 7-7"/></svg>
@@ -158,57 +235,83 @@ const TicketCreateView = {
const advancedOptions = document.getElementById('advanced-options');
const arrow = document.getElementById('advanced-arrow');
// Pre-populate default values asynchronously
setTimeout(async () => {
// 1. Owner & Responsible pre-population with active agent
const currentAgentId = localStorage.getItem('activeAgentId') || '1';
const activeAgent = (App.lookups.users || []).find(u => String(u.id) === String(currentAgentId));
if (activeAgent) {
if (ownerSearchInput && ownerIdInput) {
ownerSearchInput.value = `${activeAgent.first_name} ${activeAgent.last_name}`;
ownerIdInput.value = activeAgent.id;
}
if (responsibleSearchInput && responsibleIdInput) {
responsibleSearchInput.value = `${activeAgent.first_name} ${activeAgent.last_name}`;
responsibleIdInput.value = activeAgent.id;
}
// Restore saved state if exists, otherwise load defaults asynchronously
if (this.savedState) {
document.getElementById('create-state').value = this.savedState.state_id;
document.getElementById('create-title').value = this.savedState.title;
document.getElementById('create-queue-search').value = this.savedState.queue_name;
document.getElementById('create-queue').value = this.savedState.queue_id;
if (document.getElementById('create-type')) {
document.getElementById('create-type').value = this.savedState.type_id;
}
document.getElementById('create-owner-search').value = this.savedState.owner_name;
document.getElementById('create-owner').value = this.savedState.owner_id;
document.getElementById('create-responsible-search').value = this.savedState.responsible_name;
document.getElementById('create-responsible').value = this.savedState.responsible_id;
document.getElementById('create-user-search').value = this.savedState.user_search;
document.getElementById('create-customer-user-id').value = this.savedState.customer_user_id;
document.getElementById('create-customer-id').value = this.savedState.customer_id;
document.getElementById('create-company-search').value = this.savedState.company_search;
document.getElementById('create-priority').value = this.savedState.priority_id;
document.getElementById('create-subject').value = this.savedState.subject;
document.getElementById('create-body').value = this.savedState.body;
// 2. Customer User pre-population with first match
try {
const companies = await App.api('/api/customer-companies/search?q=cliente');
if (companies.length > 0 && customerIdInput && companySearchInput) {
const defaultCompany = companies[0];
customerIdInput.value = defaultCompany.customer_id;
companySearchInput.value = defaultCompany.customer_id;
// Search users for this company
const users = await App.api(`/api/customer-users/search?q=&customer_company_id=${encodeURIComponent(defaultCompany.customer_id)}`);
if (users.length > 0 && userSearchInput && customerUserIdInput) {
const defaultUser = users[0];
userSearchInput.value = `${defaultUser.first_name} ${defaultUser.last_name}`;
customerUserIdInput.value = defaultUser.login;
} else {
// Fallback: use company name as customer user ID
userSearchInput.value = defaultCompany.name;
customerUserIdInput.value = defaultCompany.customer_id;
if (this.savedState.isAdvancedVisible) {
advancedOptions.style.display = 'grid';
if (arrow) arrow.style.transform = 'rotate(90deg)';
}
} else {
setTimeout(async () => {
// 1. Owner & Responsible pre-population with active agent
const currentAgentId = localStorage.getItem('activeAgentId') || '1';
const activeAgent = (App.lookups.users || []).find(u => String(u.id) === String(currentAgentId));
if (activeAgent) {
if (ownerSearchInput && ownerIdInput) {
ownerSearchInput.value = `${activeAgent.first_name} ${activeAgent.last_name}`;
ownerIdInput.value = activeAgent.id;
}
if (responsibleSearchInput && responsibleIdInput) {
responsibleSearchInput.value = `${activeAgent.first_name} ${activeAgent.last_name}`;
responsibleIdInput.value = activeAgent.id;
}
}
} catch (err) {
console.error('Error pre-populating defaults:', err);
}
// 3. Queue pre-population
try {
const queues = await App.api('/api/queues/search?q=');
if (queues.length > 0 && queueSearchInput && queueIdInput) {
queueSearchInput.value = queues[0].name;
queueIdInput.value = queues[0].id;
// 2. Customer User pre-population with first match
try {
const companies = await App.api('/api/customer-companies/search?q=cliente');
if (companies.length > 0 && customerIdInput && companySearchInput) {
const defaultCompany = companies[0];
customerIdInput.value = defaultCompany.customer_id;
companySearchInput.value = defaultCompany.customer_id;
// Search users for this company
const users = await App.api(`/api/customer-users/search?q=&customer_company_id=${encodeURIComponent(defaultCompany.customer_id)}`);
if (users.length > 0 && userSearchInput && customerUserIdInput) {
const defaultUser = users[0];
userSearchInput.value = `${defaultUser.first_name} ${defaultUser.last_name}`;
customerUserIdInput.value = defaultUser.login;
} else {
// Fallback: use company name as customer user ID
userSearchInput.value = defaultCompany.name;
customerUserIdInput.value = defaultCompany.customer_id;
}
}
} catch (err) {
console.error('Error pre-populating defaults:', err);
}
} catch (err) {
console.error('Error pre-populating queues:', err);
}
}, 50);
// 3. Queue pre-population
try {
const queues = await App.api('/api/queues/search?q=');
if (queues.length > 0 && queueSearchInput && queueIdInput) {
queueSearchInput.value = queues[0].name;
queueIdInput.value = queues[0].id;
}
} catch (err) {
console.error('Error pre-populating queues:', err);
}
}, 50);
}
// Collapsible Options Toggle
if (toggleBtn && advancedOptions && arrow) {
@@ -491,6 +594,7 @@ const TicketCreateView = {
});
Toast.success(`Ticket #${result.tn} creato!`);
this.savedState = null; // Clear cached state on success
// Navigate to the new ticket
window.location.hash = `#/tickets/${result.id}`;
@@ -506,5 +610,11 @@ const TicketCreateView = {
`;
}
});
// Save state on any input/change in the form
document.querySelectorAll('.create-form input, .create-form select, .create-form textarea').forEach(el => {
el.addEventListener('input', () => this.saveState());
el.addEventListener('change', () => this.saveState());
});
},
};
+121 -2
View File
@@ -15,6 +15,12 @@ const TicketDetailView = {
await App.ensureLookups();
const data = await App.api(`/api/tickets/${id}`);
const { ticket, articles } = data;
const localISO = (dateStr) => {
if (!dateStr) return '';
const d = new Date(dateStr);
return new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
};
const totalTime = articles.reduce((sum, a) => {
const val = parseFloat(a.time_unit);
return sum + (isNaN(val) ? 0 : val);
@@ -131,12 +137,24 @@ const TicketDetailView = {
<div class="article-header">
<div class="article-sender">
<span class="article-sender-badge ${(a.sender_type || 'system').toLowerCase()}">${a.sender_type || 'System'}</span>
<span style="font-size: 0.72rem; color: var(--text-muted); font-family: monospace; margin-right: var(--space-xs);">ID: ${a.article_id}</span>
<span class="article-from">${App.escapeHtml(a.a_from || a.creator_first + ' ' + a.creator_last || 'Sistema')}</span>
${a.channel_name ? `<span style="font-size:0.72rem;color:var(--text-muted);">via ${a.channel_name}</span>` : ''}
</div>
<div style="display:flex; gap: var(--space-sm); align-items:center;">
${a.time_unit ? `<span class="badge" style="background:var(--info-bg);color:var(--info);font-size:0.75rem;padding:2px 8px;border-radius:4px;">⏱ ${parseFloat(a.time_unit)} min</span>` : ''}
<span class="article-time">${App.formatDateTime(a.create_time)}</span>
<div class="retrodate-container" data-article-id="${a.article_id}">
<span class="article-time">${App.formatDateTime(a.create_time)}</span>
<button class="retrodate-btn-edit btn-edit-article-date" title="Retrodata Articolo">✏️</button>
<div class="retrodate-editor" style="display:none;">
<input type="datetime-local" class="retrodate-input input-article-date" value="${localISO(a.create_time)}" />
<div class="retrodate-actions">
<button class="retrodate-btn-action save btn-save-article-date" title="Salva">✓</button>
<button class="retrodate-btn-action cancel btn-cancel-article-date" title="Annulla">✗</button>
</div>
</div>
</div>
</div>
</div>
${a.a_subject ? `<div class="article-subject">${App.escapeHtml(a.a_subject)}</div>` : ''}
@@ -162,7 +180,19 @@ const TicketDetailView = {
</div>
<div class="meta-row">
<span class="meta-label">Creato</span>
<span class="meta-value">${App.formatDateTime(ticket.create_time)}</span>
<span class="meta-value">
<div class="retrodate-container" id="ticket-retrodate-container">
<span id="ticket-date-text">${App.formatDateTime(ticket.create_time)}</span>
<button class="retrodate-btn-edit" id="btn-edit-ticket-date" title="Retrodata Creazione Ticket">✏️</button>
<div class="retrodate-editor" id="ticket-date-editor" style="display:none;">
<input type="datetime-local" class="retrodate-input" id="input-ticket-date" value="${localISO(ticket.create_time)}" />
<div class="retrodate-actions">
<button class="retrodate-btn-action save" id="btn-save-ticket-date" title="Salva">✓</button>
<button class="retrodate-btn-action cancel" id="btn-cancel-ticket-date" title="Annulla">✗</button>
</div>
</div>
</div>
</span>
</div>
<div class="meta-row">
<span class="meta-label">Modificato</span>
@@ -338,5 +368,94 @@ const TicketDetailView = {
noteSendBtn.innerHTML = 'Invia Nota';
}
});
// Retrodate Ticket Event Listeners
const btnEditTicketDate = document.getElementById('btn-edit-ticket-date');
const ticketDateEditor = document.getElementById('ticket-date-editor');
const ticketDateText = document.getElementById('ticket-date-text');
const btnSaveTicketDate = document.getElementById('btn-save-ticket-date');
const btnCancelTicketDate = document.getElementById('btn-cancel-ticket-date');
const inputTicketDate = document.getElementById('input-ticket-date');
if (btnEditTicketDate) {
btnEditTicketDate.addEventListener('click', () => {
btnEditTicketDate.style.display = 'none';
ticketDateText.style.display = 'none';
ticketDateEditor.style.display = 'flex';
});
btnCancelTicketDate.addEventListener('click', () => {
ticketDateEditor.style.display = 'none';
btnEditTicketDate.style.display = 'inline-flex';
ticketDateText.style.display = 'inline';
});
btnSaveTicketDate.addEventListener('click', async () => {
const val = inputTicketDate.value;
if (!val) {
Toast.warning('Scegli una data/ora valida.');
return;
}
try {
btnSaveTicketDate.disabled = true;
await App.api(`/api/tickets/${this.ticketId}/retrodata-ticket`, {
method: 'POST',
body: JSON.stringify({ create_time: val })
});
Toast.success('Data creazione ticket aggiornata!');
this.render(this.ticketId);
} catch (err) {
Toast.error('Errore retrodatazione ticket: ' + err.message);
btnSaveTicketDate.disabled = false;
}
});
}
// Retrodate Articles Event Listeners
document.querySelectorAll('.retrodate-container[data-article-id]').forEach(container => {
const articleId = container.dataset.articleId;
const btnEdit = container.querySelector('.btn-edit-article-date');
const editor = container.querySelector('.retrodate-editor');
const timeText = container.querySelector('.article-time');
const btnSave = container.querySelector('.btn-save-article-date');
const btnCancel = container.querySelector('.btn-cancel-article-date');
const input = container.querySelector('.input-article-date');
if (btnEdit) {
btnEdit.addEventListener('click', () => {
btnEdit.style.display = 'none';
timeText.style.display = 'none';
editor.style.display = 'flex';
});
btnCancel.addEventListener('click', () => {
editor.style.display = 'none';
btnEdit.style.display = 'inline-flex';
timeText.style.display = 'inline';
});
btnSave.addEventListener('click', async () => {
const val = input.value;
if (!val) {
Toast.warning('Scegli una data/ora valida.');
return;
}
try {
btnSave.disabled = true;
await App.api(`/api/tickets/articles/${articleId}/retrodata-article`, {
method: 'POST',
body: JSON.stringify({ create_time: val })
});
Toast.success('Data creazione articolo aggiornata!');
this.render(this.ticketId);
} catch (err) {
Toast.error('Errore retrodatazione articolo: ' + err.message);
btnSave.disabled = false;
}
});
}
});
},
};
+40 -59
View File
@@ -51,11 +51,12 @@ const TicketListView = {
const { total, page, per_page, total_pages } = data;
container.innerHTML = `
${Filters.renderBar(App.lookups)}
<!-- Batch Actions Bar -->
<div class="batch-bar" id="batch-bar">
<span class="batch-count" id="batch-count">0 selezionati</span>
<div style="display:flex; align-items:center; gap:var(--space-sm);">
<span class="batch-count" id="batch-count">0 selezionati</span>
<button class="btn btn-ghost btn-xs" id="batch-select-all">Seleziona visibili</button>
</div>
<div class="filter-group">
<span class="filter-label">Stato</span>
<select class="filter-select" id="batch-state">
@@ -81,14 +82,13 @@ const TicketListView = {
<button class="btn btn-ghost btn-xs" id="batch-cancel">Annulla</button>
</div>
${Filters.renderBar(App.lookups)}
<!-- Ticket Table -->
<div class="ticket-table-wrapper">
<table class="ticket-table" id="ticket-table">
<thead>
<tr>
<th class="checkbox-cell">
<input type="checkbox" id="select-all" title="Seleziona tutti" />
</th>
<th class="sortable ${this.sortBy === 'tn' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="tn">N°</th>
<th class="sortable ${this.sortBy === 'title' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="title">Titolo</th>
<th class="sortable ${this.sortBy === 'state' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="state">Stato</th>
@@ -96,27 +96,22 @@ const TicketListView = {
<th class="sortable ${this.sortBy === 'queue' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="queue">Coda</th>
<th>Owner</th>
<th class="sortable ${this.sortBy === 'create_time' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="create_time">Creato</th>
<th class="sortable ${this.sortBy === 'change_time' ? (this.sortDir === 'ASC' ? 'sort-asc' : 'sort-desc') : ''}" data-sort="change_time">Modificato</th>
</tr>
</thead>
<tbody>
${tickets.length > 0 ? tickets.map(t => `
<tr data-ticket-id="${t.id}" class="${this.selectedIds.has(String(t.id)) ? 'selected' : ''}">
<td class="checkbox-cell" onclick="event.stopPropagation()">
<input type="checkbox" class="ticket-checkbox" value="${t.id}" ${this.selectedIds.has(String(t.id)) ? 'checked' : ''} />
</td>
<td><span class="ticket-tn">${t.tn}</span></td>
<td class="ticket-title-cell">${App.escapeHtml(t.title || '(senza titolo)')}</td>
<tr data-ticket-id="${t.id}" class="${this.selectedIds.has(String(t.id)) ? 'selected' : ''}" style="cursor:pointer;">
<td><span class="ticket-tn"><a href="#/tickets/${t.id}" class="ticket-tn-link" onclick="event.stopPropagation()">${t.tn}</a></span></td>
<td class="ticket-title-cell"><a href="#/tickets/${t.id}" class="ticket-title-link" onclick="event.stopPropagation()">${App.escapeHtml(t.title || '(senza titolo)')}</a></td>
<td><span class="badge badge-state" data-state-type="${(t.state_type || '').toLowerCase()}">${t.state_name}</span></td>
<td><span class="badge badge-priority" data-priority="${App.priorityIndex(t.priority_name)}">${t.priority_name}</span></td>
<td><span class="badge badge-priority" data-priority="${App.priorityIndex(t.priority_name)}">${App.priorityIndex(t.priority_name)}</span></td>
<td><span class="badge badge-queue">${t.queue_name}</span></td>
<td style="color:var(--text-secondary);font-size:0.82rem;">${t.owner_first || ''} ${t.owner_last || ''}</td>
<td style="color:var(--text-tertiary);font-size:0.78rem;">${App.formatDate(t.create_time)}</td>
<td style="color:var(--text-tertiary);font-size:0.78rem;">${App.formatDate(t.change_time)}</td>
<td style="color:var(--text-tertiary);font-size:0.78rem;">${App.formatDateTime(t.create_time)}</td>
</tr>
`).join('') : `
<tr>
<td colspan="9">
<td colspan="7">
<div class="empty-state">
<div class="empty-state-icon">📭</div>
<div class="empty-state-text">Nessun ticket trovato</div>
@@ -186,42 +181,22 @@ const TicketListView = {
});
});
// Row click → detail
// Row click → Toggle selection
document.querySelectorAll('.ticket-table tbody tr[data-ticket-id]').forEach(row => {
row.addEventListener('click', (e) => {
if (e.target.type === 'checkbox' || e.target.closest('.checkbox-cell')) return;
window.location.hash = `#/tickets/${row.dataset.ticketId}`;
});
});
// Checkbox selection
const selectAll = document.getElementById('select-all');
if (selectAll) {
selectAll.addEventListener('change', (e) => {
const checkboxes = document.querySelectorAll('.ticket-checkbox');
checkboxes.forEach(cb => {
cb.checked = e.target.checked;
const id = cb.value;
if (e.target.checked) {
this.selectedIds.add(id);
} else {
this.selectedIds.delete(id);
}
cb.closest('tr').classList.toggle('selected', e.target.checked);
});
this.updateBatchBar();
});
}
document.querySelectorAll('.ticket-checkbox').forEach(cb => {
cb.addEventListener('change', (e) => {
const id = e.target.value;
if (e.target.checked) {
// If click was on a link, let the browser perform navigation
if (e.target.closest('.ticket-tn-link') || e.target.closest('.ticket-title-link')) {
return;
}
const id = String(row.dataset.ticketId);
if (this.selectedIds.has(id)) {
this.selectedIds.delete(id);
row.classList.remove('selected');
} else {
this.selectedIds.add(id);
} else {
this.selectedIds.delete(id);
row.classList.add('selected');
}
e.target.closest('tr').classList.toggle('selected', e.target.checked);
this.updateBatchBar();
});
});
@@ -232,17 +207,27 @@ const TicketListView = {
batchApply.addEventListener('click', () => this.applyBatch());
}
// Batch select all visible
const batchSelectAll = document.getElementById('batch-select-all');
if (batchSelectAll) {
batchSelectAll.addEventListener('click', () => {
document.querySelectorAll('.ticket-table tbody tr[data-ticket-id]').forEach(row => {
const id = String(row.dataset.ticketId);
this.selectedIds.add(id);
row.classList.add('selected');
});
this.updateBatchBar();
});
}
// Batch cancel
const batchCancel = document.getElementById('batch-cancel');
if (batchCancel) {
batchCancel.addEventListener('click', () => {
this.selectedIds.clear();
document.querySelectorAll('.ticket-checkbox').forEach(cb => {
cb.checked = false;
cb.closest('tr').classList.remove('selected');
document.querySelectorAll('.ticket-table tbody tr').forEach(tr => {
tr.classList.remove('selected');
});
const selectAll = document.getElementById('select-all');
if (selectAll) selectAll.checked = false;
this.updateBatchBar();
});
}
@@ -258,13 +243,9 @@ const TicketListView = {
},
updateBatchBar() {
const bar = document.getElementById('batch-bar');
const count = document.getElementById('batch-count');
if (this.selectedIds.size > 0) {
bar.classList.add('visible');
if (count) {
count.textContent = `${this.selectedIds.size} selezionat${this.selectedIds.size === 1 ? 'o' : 'i'}`;
} else {
bar.classList.remove('visible');
}
},