Files
otrs-turbo/public/js/views/ticketCreate.js
T

710 lines
32 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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}">&times;</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; width: 450px; max-width: 600px; z-index: 1005;"></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']
],
keyboard: {
bindings: {
tab: {
key: 'Tab',
handler: function() {
return true;
}
}
}
}
}
});
// 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 () => {
// Active agent pre-population for Owner & Responsible
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;
}
}
}, 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 => {
const displayName = q.name.replace(/::/g, ' ');
return `
<div class="autocomplete-suggestion-item" data-id="${App.escapeHtml(q.id)}" data-name="${App.escapeHtml(q.name)}" style="padding: 6px 12px; font-size: 0.78rem; line-height: 1.25;">
${App.escapeHtml(displayName)}
</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());
});
},
};