779 lines
33 KiB
JavaScript
779 lines
33 KiB
JavaScript
/**
|
|
* Ticket Bulk Creation View
|
|
* Tabular layout for creating multiple tickets.
|
|
*/
|
|
const TicketBulkView = {
|
|
rowCount: 0,
|
|
activeRequests: false,
|
|
savedRows: null,
|
|
rowAttachments: {},
|
|
|
|
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 type="button" class="btn btn-ghost btn-xs" id="bulk-attach-btn-${id}" title="Allega file" style="position:relative; display:inline-flex; align-items:center; gap:2px; height:24px; padding:0 6px;">
|
|
📎 <span class="badge-attachments-count" id="bulk-attach-badge-${id}" style="display:none; font-size:10px;">0</span>
|
|
</button>
|
|
<input type="file" id="bulk-file-input-${id}" multiple style="display:none;" />
|
|
<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);
|
|
|
|
// Initialize attachments for this row
|
|
this.rowAttachments[id] = savedData ? (savedData.attachments || []) : [];
|
|
|
|
// Bind attachment click and input change
|
|
const attachBtn = tr.querySelector(`#bulk-attach-btn-${id}`);
|
|
const fileInput = tr.querySelector(`#bulk-file-input-${id}`);
|
|
const attachBadge = tr.querySelector(`#bulk-attach-badge-${id}`);
|
|
|
|
const updateBadge = () => {
|
|
const count = (this.rowAttachments[id] || []).length;
|
|
if (attachBadge) {
|
|
attachBadge.textContent = count;
|
|
attachBadge.style.display = count > 0 ? 'inline-block' : 'none';
|
|
}
|
|
};
|
|
|
|
if (attachBtn && fileInput) {
|
|
attachBtn.addEventListener('click', () => fileInput.click());
|
|
|
|
fileInput.addEventListener('change', (e) => {
|
|
const files = Array.from(e.target.files);
|
|
for (const file of files) {
|
|
const reader = new FileReader();
|
|
reader.onload = () => {
|
|
const base64Data = reader.result.split(',')[1];
|
|
if (!this.rowAttachments[id]) {
|
|
this.rowAttachments[id] = [];
|
|
}
|
|
this.rowAttachments[id].push({
|
|
filename: file.name,
|
|
content: base64Data,
|
|
content_type: file.type
|
|
});
|
|
updateBadge();
|
|
this.saveState();
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
fileInput.value = '';
|
|
});
|
|
}
|
|
|
|
// 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());
|
|
});
|
|
|
|
updateBadge();
|
|
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,
|
|
attachments: this.rowAttachments[id] || []
|
|
});
|
|
});
|
|
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;
|
|
let customerId = document.getElementById(`bulk-customer-${id}`).value;
|
|
let customerUserId = document.getElementById(`bulk-customer-user-id-${id}`).value;
|
|
const customerSearch = document.getElementById(`bulk-customer-search-${id}`).value.trim();
|
|
|
|
if (!customerUserId && customerSearch) {
|
|
customerUserId = customerSearch;
|
|
if (!customerId) {
|
|
customerId = customerSearch;
|
|
}
|
|
}
|
|
|
|
const subject = document.getElementById(`bulk-subject-${id}`).value.trim();
|
|
const body = document.getElementById(`bulk-body-${id}`).value.trim();
|
|
const priority_id = document.getElementById(`bulk-priority-${id}`).value;
|
|
|
|
// 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,
|
|
attachments: (this.rowAttachments[id] || []).length > 0 ? this.rowAttachments[id] : 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 = [];
|
|
this.rowAttachments = {};
|
|
setTimeout(() => {
|
|
window.location.hash = '#/tickets';
|
|
}, 1500);
|
|
} else {
|
|
this.saveState();
|
|
Toast.warning(`Invio completato: ${successCount} creati, ${failCount} falliti. Controlla i pallini colorati per i dettagli.`);
|
|
}
|
|
}
|
|
};
|