732 lines
33 KiB
JavaScript
732 lines
33 KiB
JavaScript
/**
|
|
* Ticket Create View
|
|
* Minimal, fast form for creating new tickets.
|
|
*/
|
|
const TicketCreateView = {
|
|
savedState: null,
|
|
attachments: [],
|
|
|
|
updateAttachmentList() {
|
|
const listEl = document.getElementById('create-file-list');
|
|
if (!listEl) return;
|
|
listEl.innerHTML = this.attachments.map((att, idx) => `
|
|
<div class="upload-file-item" data-index="${idx}">
|
|
<span class="upload-file-name">📎 ${App.escapeHtml(att.filename)} (${Math.round(att.content.length * 0.75 / 1024)} KB)</span>
|
|
<button type="button" class="upload-file-remove" data-index="${idx}">×</button>
|
|
</div>
|
|
`).join('');
|
|
|
|
// Bind remove clicks
|
|
listEl.querySelectorAll('.upload-file-remove').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
const idx = parseInt(btn.dataset.index, 10);
|
|
this.attachments.splice(idx, 1);
|
|
this.updateAttachmentList();
|
|
});
|
|
});
|
|
},
|
|
|
|
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: this.quill ? this.quill.root.innerHTML : '',
|
|
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>';
|
|
|
|
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>
|
|
Torna indietro
|
|
</a>
|
|
|
|
<div class="card create-form">
|
|
<div class="card-title" style="margin-bottom:var(--space-lg); font-size:0.9rem; display:flex; justify-content:space-between; align-items:center; width:100%; gap: var(--space-md); flex-wrap: wrap;">
|
|
<span>Crea Nuovo Ticket</span>
|
|
<select class="form-select" id="create-state" style="width:200px; padding: 6px 12px; height: 32px; font-size: 0.85rem; margin: 0; line-height: 1;">
|
|
${(App.lookups.states || []).map(s => {
|
|
const sel = s.type_name === 'new' ? 'selected' : '';
|
|
return `<option value="${s.id}" ${sel}>${s.name}</option>`;
|
|
}).join('')}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-grid">
|
|
<div class="form-group full-width">
|
|
<label class="form-label">Titolo <span class="required">*</span></label>
|
|
<input type="text" class="form-input" id="create-title" placeholder="Descrizione breve del problema" autofocus />
|
|
</div>
|
|
|
|
<div class="form-group" style="position:relative;">
|
|
<label class="form-label">Coda <span class="required">*</span></label>
|
|
<input type="text" class="form-input" id="create-queue-search" placeholder="Cerca coda..." autocomplete="off" />
|
|
<input type="hidden" id="create-queue" />
|
|
<div id="queue-suggestions" class="autocomplete-suggestions" style="display:none;"></div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Tipo</label>
|
|
<select class="form-select" id="create-type">
|
|
<option value="">—</option>
|
|
${(App.lookups.types || []).map(t => `<option value="${t.id}">${t.name}</option>`).join('')}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group" style="position:relative;">
|
|
<label class="form-label">Owner (Proprietario)</label>
|
|
<input type="text" class="form-input" id="create-owner-search" placeholder="Cerca proprietario..." autocomplete="off" />
|
|
<input type="hidden" id="create-owner" />
|
|
<div id="owner-suggestions" class="autocomplete-suggestions" style="display:none;"></div>
|
|
</div>
|
|
|
|
<div class="form-group" style="position:relative;">
|
|
<label class="form-label">Responsabile</label>
|
|
<input type="text" class="form-input" id="create-responsible-search" placeholder="Cerca responsabile..." autocomplete="off" />
|
|
<input type="hidden" id="create-responsible" />
|
|
<div id="responsible-suggestions" class="autocomplete-suggestions" style="display:none;"></div>
|
|
</div>
|
|
|
|
<div class="form-group full-width" style="position:relative;">
|
|
<label class="form-label">Utente Cliente (Persona) <span class="required">*</span></label>
|
|
<input type="text" class="form-input" id="create-user-search" placeholder="Cerca utente (nome, email, login)..." autocomplete="off" />
|
|
<input type="hidden" id="create-customer-user-id" />
|
|
<div id="user-suggestions" class="autocomplete-suggestions" style="display:none;"></div>
|
|
</div>
|
|
|
|
<!-- Collapsible Advanced Options -->
|
|
<div class="full-width" id="advanced-options" style="display: none; grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); gap: var(--space-md); padding: var(--space-md); background: rgba(255,255,255,0.02); border: 1px dashed var(--border-light); border-radius: var(--radius-md); margin-top: var(--space-md); margin-bottom: var(--space-md);">
|
|
<div class="form-group" style="margin-bottom:0;">
|
|
<label class="form-label">Azienda Cliente (Società)</label>
|
|
<input type="text" class="form-input" id="create-company-search" readonly disabled placeholder="Auto-assegnata dal cliente" style="cursor: not-allowed; background: rgba(255,255,255,0.05); color: var(--text-secondary); margin-bottom:0;" />
|
|
<input type="hidden" id="create-customer-id" />
|
|
</div>
|
|
<div class="form-group" style="margin-bottom:0;">
|
|
<label class="form-label">Priorità</label>
|
|
<select class="form-select" id="create-priority" style="margin-bottom: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>
|
|
|
|
<div class="form-group full-width">
|
|
<label class="form-label">Oggetto</label>
|
|
<input type="text" class="form-input" id="create-subject" placeholder="Oggetto del primo articolo (opzionale)" />
|
|
</div>
|
|
|
|
<div class="form-group full-width">
|
|
<label class="form-label">Messaggio / Nota iniziale</label>
|
|
<div id="create-body-container" style="background: var(--bg-tertiary); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); overflow: hidden;">
|
|
<div id="create-body-editor" style="min-height: 200px; font-family: inherit; font-size: 0.95rem; border: none; color: var(--text-primary);"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Attachments List -->
|
|
<div class="upload-file-list" id="create-file-list"></div>
|
|
|
|
<div class="form-actions" style="display:flex; justify-content:space-between; align-items:center;">
|
|
<div>
|
|
<button type="button" class="btn btn-ghost btn-sm" id="toggle-advanced" style="display: flex; align-items: center; gap: var(--space-xs); padding: 6px 12px; height: auto; margin:0;">
|
|
<svg id="advanced-arrow" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px; height:14px; transition: transform var(--transition-normal);"><path d="M9 18l6-6-6-6"/></svg>
|
|
Opzioni Avanzate
|
|
</button>
|
|
</div>
|
|
<div style="display:flex; gap:var(--space-sm); align-items:center;">
|
|
<input type="file" id="create-attachments" multiple style="display:none;" />
|
|
<button type="button" class="btn btn-ghost" id="btn-add-attachments">📎 Allega file</button>
|
|
<button class="btn btn-ghost" onclick="history.back()">Annulla</button>
|
|
<button class="btn btn-primary" id="create-submit">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;">
|
|
<path d="M12 5v14M5 12h14"/>
|
|
</svg>
|
|
Crea Ticket
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Initialize Quill Editor
|
|
if (window.Quill) {
|
|
this.quill = new Quill('#create-body-editor', {
|
|
theme: 'snow',
|
|
placeholder: 'Descrivi il problema in dettaglio...',
|
|
modules: {
|
|
toolbar: [
|
|
['bold', 'italic', 'underline', 'strike'],
|
|
[{ 'header': [1, 2, 3, false] }],
|
|
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
|
['link', 'image'],
|
|
['clean']
|
|
]
|
|
}
|
|
});
|
|
|
|
// Set saved body/state if available
|
|
if (this.savedState && this.savedState.body) {
|
|
this.quill.root.innerHTML = this.savedState.body;
|
|
}
|
|
|
|
// Prevent tab navigation on toolbar items
|
|
const toolbar = container.querySelector('.ql-toolbar');
|
|
if (toolbar) {
|
|
toolbar.querySelectorAll('button, select, span[role="button"], input').forEach(el => {
|
|
el.setAttribute('tabindex', '-1');
|
|
});
|
|
}
|
|
} else {
|
|
this.quill = null;
|
|
}
|
|
|
|
this.bindEvents();
|
|
|
|
} catch (err) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<div class="empty-state-icon">⚠️</div>
|
|
<div class="empty-state-text">Errore caricamento form</div>
|
|
<div class="empty-state-sub">${App.escapeHtml(err.message)}</div>
|
|
</div>
|
|
`;
|
|
}
|
|
},
|
|
|
|
bindEvents() {
|
|
const submitBtn = document.getElementById('create-submit');
|
|
const companySearchInput = document.getElementById('create-company-search');
|
|
const customerIdInput = document.getElementById('create-customer-id');
|
|
|
|
const userSearchInput = document.getElementById('create-user-search');
|
|
const userSuggestionsDiv = document.getElementById('user-suggestions');
|
|
const customerUserIdInput = document.getElementById('create-customer-user-id');
|
|
|
|
const ownerSearchInput = document.getElementById('create-owner-search');
|
|
const ownerSuggestionsDiv = document.getElementById('owner-suggestions');
|
|
const ownerIdInput = document.getElementById('create-owner');
|
|
|
|
const responsibleSearchInput = document.getElementById('create-responsible-search');
|
|
const responsibleSuggestionsDiv = document.getElementById('responsible-suggestions');
|
|
const responsibleIdInput = document.getElementById('create-responsible');
|
|
|
|
const queueSearchInput = document.getElementById('create-queue-search');
|
|
const queueSuggestionsDiv = document.getElementById('queue-suggestions');
|
|
const queueIdInput = document.getElementById('create-queue');
|
|
|
|
const stateIdInput = document.getElementById('create-state');
|
|
|
|
const toggleBtn = document.getElementById('toggle-advanced');
|
|
const advancedOptions = document.getElementById('advanced-options');
|
|
const arrow = document.getElementById('advanced-arrow');
|
|
|
|
// 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;
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// 2. Customer User pre-population with first match
|
|
try {
|
|
const companies = await App.api('/api/customer-companies/search?q=');
|
|
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);
|
|
}
|
|
|
|
// 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) {
|
|
toggleBtn.addEventListener('click', () => {
|
|
const isHidden = advancedOptions.style.display === 'none';
|
|
advancedOptions.style.display = isHidden ? 'grid' : 'none';
|
|
arrow.style.transform = isHidden ? 'rotate(90deg)' : 'rotate(0deg)';
|
|
});
|
|
}
|
|
|
|
// User Autocomplete
|
|
let userDebounce;
|
|
if (userSearchInput) {
|
|
userSearchInput.addEventListener('input', () => {
|
|
clearTimeout(userDebounce);
|
|
const q = userSearchInput.value.trim();
|
|
|
|
userDebounce = setTimeout(async () => {
|
|
try {
|
|
const users = await App.api(`/api/customer-users/search?q=${encodeURIComponent(q)}`);
|
|
console.log('[Frontend LDAP Search] Users returned:', users);
|
|
// Prevent race conditions: discard results if the input value has changed
|
|
if (userSearchInput.value.trim() !== q) {
|
|
console.log('[Frontend LDAP Search] Discarding stale results for query:', q);
|
|
return;
|
|
}
|
|
if (users.length === 0) {
|
|
userSuggestionsDiv.innerHTML = '<div class="autocomplete-suggestion-item" style="color:var(--text-muted); cursor:default;">Nessun utente trovato</div>';
|
|
userSuggestionsDiv.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
userSuggestionsDiv.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).trim() || u.login || '')}">
|
|
<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('');
|
|
userSuggestionsDiv.style.display = 'block';
|
|
|
|
// Use mousedown instead of click to fire before blur event
|
|
userSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => {
|
|
if (item.dataset.login) {
|
|
item.addEventListener('mousedown', (e) => {
|
|
e.preventDefault(); // prevent input from losing focus before value is set
|
|
userSearchInput.value = item.dataset.name;
|
|
customerUserIdInput.value = item.dataset.login;
|
|
userSuggestionsDiv.style.display = 'none';
|
|
|
|
// Auto-fill company inside Advanced Options
|
|
if (item.dataset.customerId) {
|
|
customerIdInput.value = item.dataset.customerId;
|
|
companySearchInput.value = item.dataset.customerId;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}, 300);
|
|
});
|
|
userSearchInput.addEventListener('focus', () => {
|
|
if (!customerUserIdInput.value) {
|
|
// Only search if no user is selected yet
|
|
userSearchInput.dispatchEvent(new Event('input'));
|
|
}
|
|
});
|
|
userSearchInput.addEventListener('blur', () => {
|
|
// Small delay to allow mousedown on item to fire first
|
|
setTimeout(() => { userSuggestionsDiv.style.display = 'none'; }, 150);
|
|
});
|
|
}
|
|
|
|
|
|
// Owner Autocomplete (dynamic backend search)
|
|
let ownerDebounce;
|
|
if (ownerSearchInput) {
|
|
ownerSearchInput.addEventListener('input', () => {
|
|
clearTimeout(ownerDebounce);
|
|
const q = ownerSearchInput.value.trim();
|
|
// Do not block empty query to allow all agent results on focus
|
|
|
|
|
|
ownerDebounce = setTimeout(async () => {
|
|
try {
|
|
const agents = await App.api(`/api/agents/search?q=${encodeURIComponent(q)}`);
|
|
if (agents.length === 0) {
|
|
ownerSuggestionsDiv.innerHTML = '<div class="autocomplete-suggestion-item" style="color:var(--text-muted); cursor:default;">Nessun agente trovato</div>';
|
|
ownerSuggestionsDiv.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
ownerSuggestionsDiv.innerHTML = agents.map(u => `
|
|
<div class="autocomplete-suggestion-item" data-id="${App.escapeHtml(u.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)})</span>
|
|
</div>
|
|
`).join('');
|
|
ownerSuggestionsDiv.style.display = 'block';
|
|
|
|
// Bind click
|
|
ownerSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => {
|
|
if (item.dataset.id) {
|
|
item.addEventListener('click', () => {
|
|
ownerSearchInput.value = item.dataset.name;
|
|
ownerIdInput.value = item.dataset.id;
|
|
ownerSuggestionsDiv.style.display = 'none';
|
|
});
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}, 300);
|
|
});
|
|
ownerSearchInput.addEventListener('focus', () => {
|
|
ownerSearchInput.value = '';
|
|
ownerIdInput.value = '';
|
|
ownerSearchInput.dispatchEvent(new Event('input'));
|
|
});
|
|
}
|
|
|
|
// Responsible Autocomplete (dynamic backend search)
|
|
let responsibleDebounce;
|
|
if (responsibleSearchInput) {
|
|
responsibleSearchInput.addEventListener('input', () => {
|
|
clearTimeout(responsibleDebounce);
|
|
const q = responsibleSearchInput.value.trim();
|
|
// Do not block empty query to allow all agent results on focus
|
|
|
|
|
|
responsibleDebounce = setTimeout(async () => {
|
|
try {
|
|
const agents = await App.api(`/api/agents/search?q=${encodeURIComponent(q)}`);
|
|
if (agents.length === 0) {
|
|
responsibleSuggestionsDiv.innerHTML = '<div class="autocomplete-suggestion-item" style="color:var(--text-muted); cursor:default;">Nessun agente trovato</div>';
|
|
responsibleSuggestionsDiv.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
responsibleSuggestionsDiv.innerHTML = agents.map(u => `
|
|
<div class="autocomplete-suggestion-item" data-id="${App.escapeHtml(u.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)})</span>
|
|
</div>
|
|
`).join('');
|
|
responsibleSuggestionsDiv.style.display = 'block';
|
|
|
|
// Bind click
|
|
responsibleSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => {
|
|
if (item.dataset.id) {
|
|
item.addEventListener('click', () => {
|
|
responsibleSearchInput.value = item.dataset.name;
|
|
responsibleIdInput.value = item.dataset.id;
|
|
responsibleSuggestionsDiv.style.display = 'none';
|
|
});
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}, 300);
|
|
});
|
|
responsibleSearchInput.addEventListener('focus', () => {
|
|
responsibleSearchInput.value = '';
|
|
responsibleIdInput.value = '';
|
|
responsibleSearchInput.dispatchEvent(new Event('input'));
|
|
});
|
|
}
|
|
|
|
// Queue Autocomplete
|
|
let queueDebounce;
|
|
if (queueSearchInput) {
|
|
queueSearchInput.addEventListener('input', () => {
|
|
clearTimeout(queueDebounce);
|
|
const q = queueSearchInput.value.trim();
|
|
queueDebounce = setTimeout(async () => {
|
|
try {
|
|
const queues = await App.api(`/api/queues/search?q=${encodeURIComponent(q)}`);
|
|
if (queues.length === 0) {
|
|
queueSuggestionsDiv.innerHTML = '<div class="autocomplete-suggestion-item" style="color:var(--text-muted); cursor:default;">Nessuna coda trovata</div>';
|
|
queueSuggestionsDiv.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
queueSuggestionsDiv.innerHTML = queues.map(q => `
|
|
<div class="autocomplete-suggestion-item" data-id="${App.escapeHtml(q.id)}" data-name="${App.escapeHtml(q.name)}">
|
|
<strong>${App.escapeHtml(q.name)}</strong>
|
|
</div>
|
|
`).join('');
|
|
queueSuggestionsDiv.style.display = 'block';
|
|
|
|
// Bind click
|
|
queueSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => {
|
|
if (item.dataset.id) {
|
|
item.addEventListener('click', () => {
|
|
queueSearchInput.value = item.dataset.name;
|
|
queueIdInput.value = item.dataset.id;
|
|
queueSuggestionsDiv.style.display = 'none';
|
|
});
|
|
}
|
|
});
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
}, 150);
|
|
});
|
|
queueSearchInput.addEventListener('focus', () => {
|
|
queueSearchInput.value = '';
|
|
queueIdInput.value = '';
|
|
queueSearchInput.dispatchEvent(new Event('input'));
|
|
});
|
|
}
|
|
|
|
// Close suggestions on click outside
|
|
document.addEventListener('click', (e) => {
|
|
if (userSearchInput && e.target !== userSearchInput && e.target !== userSuggestionsDiv) {
|
|
userSuggestionsDiv.style.display = 'none';
|
|
}
|
|
if (ownerSearchInput && e.target !== ownerSearchInput && e.target !== ownerSuggestionsDiv) {
|
|
ownerSuggestionsDiv.style.display = 'none';
|
|
}
|
|
if (responsibleSearchInput && e.target !== responsibleSearchInput && e.target !== responsibleSuggestionsDiv) {
|
|
responsibleSuggestionsDiv.style.display = 'none';
|
|
}
|
|
if (queueSearchInput && e.target !== queueSearchInput && e.target !== queueSuggestionsDiv) {
|
|
queueSuggestionsDiv.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
submitBtn.addEventListener('click', async () => {
|
|
const title = document.getElementById('create-title').value.trim();
|
|
const queue_id = queueIdInput.value;
|
|
const state_id = stateIdInput.value;
|
|
const priority_id = document.getElementById('create-priority').value;
|
|
const type_id = document.getElementById('create-type')?.value;
|
|
|
|
const userSearchVal = userSearchInput.value.trim();
|
|
let customerId = customerIdInput.value;
|
|
let customerUserId = customerUserIdInput.value;
|
|
|
|
if (!customerUserId && userSearchVal) {
|
|
customerUserId = userSearchVal;
|
|
if (!customerId) {
|
|
customerId = userSearchVal;
|
|
}
|
|
}
|
|
|
|
const ownerId = ownerIdInput.value;
|
|
const responsibleId = responsibleIdInput.value;
|
|
|
|
// Validation
|
|
if (!title) {
|
|
Toast.warning('Il titolo è obbligatorio');
|
|
document.getElementById('create-title').focus();
|
|
return;
|
|
}
|
|
if (!queue_id) {
|
|
Toast.warning('Seleziona una coda');
|
|
queueSearchInput.focus();
|
|
return;
|
|
}
|
|
if (!state_id) {
|
|
Toast.warning('Seleziona uno stato');
|
|
document.getElementById('create-state').focus();
|
|
return;
|
|
}
|
|
if (!customerId && !customerUserId) {
|
|
Toast.warning('Seleziona un Utente Cliente');
|
|
userSearchInput.focus();
|
|
return;
|
|
}
|
|
|
|
// Company fallback if no individual user selected
|
|
if (customerId && !customerUserId) {
|
|
customerUserId = customerId;
|
|
}
|
|
|
|
const 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: document.getElementById('create-subject').value.trim() || undefined,
|
|
body: this.quill ? this.quill.root.innerHTML.trim() : undefined,
|
|
attachments: this.attachments.length > 0 ? this.attachments : undefined
|
|
};
|
|
|
|
try {
|
|
submitBtn.disabled = true;
|
|
submitBtn.innerHTML = '<div class="spinner" style="width:16px;height:16px;border-width:2px;"></div> Creazione...';
|
|
|
|
const result = await App.api('/api/tickets', {
|
|
method: 'POST',
|
|
body: JSON.stringify(payload),
|
|
});
|
|
|
|
Toast.success(`Ticket #${result.tn} creato!`);
|
|
this.savedState = null; // Clear cached state on success
|
|
this.attachments = []; // Clear attachments array
|
|
|
|
// Update daily timer
|
|
App.updateDailyTimer();
|
|
|
|
// Navigate to the new ticket
|
|
window.location.hash = `#/tickets/${result.id}`;
|
|
|
|
} catch (err) {
|
|
Toast.error('Errore creazione: ' + err.message);
|
|
submitBtn.disabled = false;
|
|
submitBtn.innerHTML = `
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:16px;height:16px;">
|
|
<path d="M12 5v14M5 12h14"/>
|
|
</svg>
|
|
Crea Ticket
|
|
`;
|
|
}
|
|
});
|
|
|
|
// Attachments Upload Handlers
|
|
const attachBtn = document.getElementById('btn-add-attachments');
|
|
const fileInput = document.getElementById('create-attachments');
|
|
|
|
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];
|
|
this.attachments.push({
|
|
filename: file.name,
|
|
content: base64Data,
|
|
content_type: file.type
|
|
});
|
|
this.updateAttachmentList();
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
fileInput.value = ''; // Reset input to allow re-selecting the same file
|
|
});
|
|
}
|
|
|
|
// 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());
|
|
});
|
|
},
|
|
};
|