1193 lines
56 KiB
JavaScript
1193 lines
56 KiB
JavaScript
/**
|
||
* Ticket Detail View
|
||
* Shows full ticket info with quick-edit dropdowns, article timeline, and add-note form.
|
||
*/
|
||
const TicketDetailView = {
|
||
ticketId: null,
|
||
htmlMode: false,
|
||
originalValues: {},
|
||
noteAttachments: [],
|
||
|
||
saveDraft() {
|
||
if (!this.ticketId) return;
|
||
const body = this.noteQuill ? this.noteQuill.root.innerHTML.trim() : '';
|
||
const subject = document.getElementById('note-subject') ? document.getElementById('note-subject').value.trim() : '';
|
||
const time_unit = document.getElementById('note-time-units') ? document.getElementById('note-time-units').value.trim() : '';
|
||
|
||
if ((body !== '<p><br></p>' && body !== '') || subject !== '' || time_unit !== '' || this.noteAttachments.length > 0) {
|
||
App.saveDraft(this.ticketId, {
|
||
type: 'note',
|
||
body,
|
||
subject,
|
||
time_unit,
|
||
attachments: [...this.noteAttachments]
|
||
});
|
||
} else {
|
||
App.clearDraft(this.ticketId, 'note');
|
||
}
|
||
},
|
||
|
||
updateNoteAttachmentList() {
|
||
const listEl = document.getElementById('note-file-list');
|
||
if (!listEl) return;
|
||
listEl.innerHTML = this.noteAttachments.map((att, idx) => `
|
||
<div class="upload-file-item">
|
||
<span>📎</span>
|
||
<strong>${App.escapeHtml(att.filename)}</strong>
|
||
<span style="font-size:0.75rem; color:var(--text-muted); margin-left:var(--space-xs);">(${Math.round(att.content.length * 0.75 / 1024)} KB)</span>
|
||
<button type="button" class="btn-remove" data-idx="${idx}">✕</button>
|
||
</div>
|
||
`).join('');
|
||
|
||
// Bind remove buttons
|
||
listEl.querySelectorAll('.btn-remove').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const idx = parseInt(btn.dataset.idx, 10);
|
||
this.noteAttachments.splice(idx, 1);
|
||
this.updateNoteAttachmentList();
|
||
});
|
||
});
|
||
},
|
||
|
||
async render(id) {
|
||
this.ticketId = id;
|
||
this.noteAttachments = [];
|
||
this.htmlMode = localStorage.getItem('otrs_turbo_html_mode') === 'true';
|
||
const container = document.getElementById('view-container');
|
||
container.innerHTML = '<div class="loading-screen"><div class="spinner"></div><p>Caricamento ticket...</p></div>';
|
||
|
||
try {
|
||
await App.ensureLookups();
|
||
const data = await App.api(`/api/tickets/${id}`);
|
||
const { ticket, articles, attachments } = data;
|
||
App.addTabWithoutRedirect(ticket.id, ticket.tn, ticket.title);
|
||
|
||
let groupsData = { asMaster: [], asMember: [] };
|
||
try {
|
||
groupsData = await App.api(`/api/groups/by-ticket/${id}`);
|
||
} catch (gErr) {
|
||
console.warn('Failed to load group associations for ticket', gErr);
|
||
}
|
||
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);
|
||
}, 0);
|
||
|
||
// Display ticket number in the topbar with OTRS link if available
|
||
const titleEl = document.getElementById('page-title');
|
||
if (titleEl) {
|
||
if (data.otrsWebUrl) {
|
||
titleEl.innerHTML = `<a href="${data.otrsWebUrl}" target="_blank" style="color:inherit; text-decoration:none; display:inline-flex; align-items:center; gap:6px;" title="Apri in OTRS">Ticket #${ticket.tn || id} <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;opacity:0.75;"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6M15 3h6v6M10 14L21 3"/></svg></a>`;
|
||
} else {
|
||
titleEl.innerHTML = `Ticket #${ticket.tn || id}`;
|
||
}
|
||
}
|
||
|
||
this.originalValues = {
|
||
ticket_state_id: ticket.ticket_state_id,
|
||
ticket_priority_id: ticket.ticket_priority_id,
|
||
queue_id: ticket.queue_id,
|
||
queue_name: ticket.queue_name,
|
||
user_id: ticket.user_id,
|
||
responsible_user_id: ticket.responsible_user_id,
|
||
type_id: ticket.type_id,
|
||
customer_id: ticket.customer_id,
|
||
customer_user_id: ticket.customer_user_id,
|
||
customer_first: ticket.customer_first,
|
||
customer_last: ticket.customer_last,
|
||
};
|
||
|
||
const hasEmailDraft = App.getDraft(id, 'email');
|
||
const emailBtnText = hasEmailDraft ? 'Continua mail' : 'Invia Email';
|
||
|
||
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 alla lista
|
||
</a>
|
||
|
||
<div class="ticket-detail">
|
||
<div class="ticket-detail-main">
|
||
<!-- Header -->
|
||
<div class="card">
|
||
<div class="ticket-header">
|
||
<div class="ticket-header-info">
|
||
<div class="ticket-number">#${ticket.tn}</div>
|
||
<h2 class="ticket-detail-title">
|
||
${data.otrsWebUrl ? `
|
||
<a href="${data.otrsWebUrl}" target="_blank" style="color:inherit; text-decoration:none; border-bottom:1px dashed transparent; transition:border-bottom 0.1s ease;" onmouseover="this.style.borderBottom='1px dashed var(--text-primary)'" onmouseout="this.style.borderBottom='transparent'" title="Apri in OTRS">
|
||
${App.escapeHtml(ticket.title || '(senza titolo)')}
|
||
</a>
|
||
` : App.escapeHtml(ticket.title || '(senza titolo)')}
|
||
</h2>
|
||
<div class="ticket-meta-badges">
|
||
<span class="badge badge-state" data-state-type="${(ticket.state_type || '').toLowerCase()}">${ticket.state_name}</span>
|
||
<span class="badge badge-priority" data-priority="${App.priorityIndex(ticket.priority_name)}">${ticket.priority_name}</span>
|
||
<span class="badge badge-queue">${ticket.queue_name}</span>
|
||
${ticket.lock_name === 'lock' ? '<span class="badge" style="background:var(--warning-bg);color:var(--warning);">🔒 Bloccato</span>' : ''}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Quick Edit -->
|
||
<div class="card">
|
||
<div class="card-title">Modifica Rapida</div>
|
||
<div class="quick-edit">
|
||
<div class="quick-edit-field">
|
||
<label class="quick-edit-label">Stato</label>
|
||
<select class="quick-edit-select" id="qe-state" data-field="ticket_state_id">
|
||
${(App.lookups.states || []).map(s =>
|
||
`<option value="${s.id}" ${s.id === ticket.ticket_state_id ? 'selected' : ''}>${s.name}</option>`
|
||
).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="quick-edit-field">
|
||
<label class="quick-edit-label">Priorità</label>
|
||
<select class="quick-edit-select" id="qe-priority" data-field="ticket_priority_id">
|
||
${(App.lookups.priorities || []).map(p =>
|
||
`<option value="${p.id}" ${p.id === ticket.ticket_priority_id ? 'selected' : ''}>${p.name}</option>`
|
||
).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="quick-edit-field" style="position:relative;">
|
||
<label class="quick-edit-label">Coda</label>
|
||
<input type="text" class="quick-edit-select" id="qe-queue-search" placeholder="Cerca coda..." autocomplete="off" value="${App.escapeHtml(ticket.queue_name || '')}" style="background-image: none; cursor: text;" />
|
||
<input type="hidden" id="qe-queue" data-field="queue_id" value="${ticket.queue_id || ''}" />
|
||
<div id="qe-queue-suggestions" class="autocomplete-suggestions" style="display:none; width: 800px; max-width: 800px; z-index: 1005;"></div>
|
||
</div>
|
||
<div class="quick-edit-field">
|
||
<label class="quick-edit-label">Owner</label>
|
||
<select class="quick-edit-select" id="qe-owner" data-field="user_id">
|
||
${(App.lookups.users || []).map(u =>
|
||
`<option value="${u.id}" ${u.id === ticket.user_id ? 'selected' : ''}>${u.first_name} ${u.last_name}</option>`
|
||
).join('')}
|
||
</select>
|
||
</div>
|
||
<div class="quick-edit-field">
|
||
<label class="quick-edit-label">Responsabile</label>
|
||
<select class="quick-edit-select" id="qe-responsible" data-field="responsible_user_id">
|
||
<option value="">—</option>
|
||
${(App.lookups.users || []).map(u =>
|
||
`<option value="${u.id}" ${u.id === ticket.responsible_user_id ? 'selected' : ''}>${u.first_name} ${u.last_name}</option>`
|
||
).join('')}
|
||
</select>
|
||
</div>
|
||
${(App.lookups.types || []).length > 0 ? `
|
||
<div class="quick-edit-field">
|
||
<label class="quick-edit-label">Tipo</label>
|
||
<select class="quick-edit-select" id="qe-type" data-field="type_id">
|
||
<option value="">—</option>
|
||
${App.lookups.types.map(t =>
|
||
`<option value="${t.id}" ${t.id === ticket.type_id ? 'selected' : ''}>${t.name}</option>`
|
||
).join('')}
|
||
</select>
|
||
</div>
|
||
` : ''}
|
||
<div class="quick-edit-field" style="position:relative;">
|
||
<label class="quick-edit-label">Utente Cliente</label>
|
||
<input type="text" class="form-input" id="qe-customer-search" placeholder="Cerca cliente..." autocomplete="off" value="${ticket.customer_first ? `${ticket.customer_first} ${ticket.customer_last}` : (ticket.customer_user_id || '')}" style="padding: 6px 12px; height: 32px; font-size: 0.85rem;" />
|
||
<input type="hidden" id="qe-customer-user-id" data-field="customer_user_id" value="${ticket.customer_user_id || ''}" />
|
||
<input type="hidden" id="qe-customer-id" data-field="customer_id" value="${ticket.customer_id || ''}" />
|
||
<div id="qe-customer-suggestions" class="autocomplete-suggestions" style="display:none; top: 100%;"></div>
|
||
</div>
|
||
<div class="quick-edit-field">
|
||
<label class="quick-edit-label">Tempo (minuti)</label>
|
||
<input type="number" step="any" min="0" class="form-input" id="qe-time-unit" placeholder="minuti" style="padding: 6px 12px; height: 32px; font-size: 0.85rem;" />
|
||
</div>
|
||
<div style="margin-top:var(--space-md);display:flex;gap:var(--space-sm);justify-content:flex-end;">
|
||
<button class="btn btn-ghost btn-sm" id="qe-reset">Reset</button>
|
||
<button class="btn btn-primary btn-sm" id="qe-save" disabled>Salva Modifiche</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Add Note Form -->
|
||
<div class="add-note-form" style="margin-bottom: var(--space-lg);">
|
||
<div class="card-title">Aggiungi Nota</div>
|
||
<div style="display:flex; gap:var(--space-md); margin-bottom:var(--space-md);">
|
||
<input type="text" class="note-subject-input" id="note-subject" placeholder="Oggetto (opzionale)" style="flex:1; margin-bottom:0;" />
|
||
</div>
|
||
<div id="note-body-container" style="background: var(--bg-tertiary); border: 1px solid var(--border-subtle); border-radius: var(--radius-md); overflow: hidden; margin-bottom: var(--space-md);">
|
||
<div id="note-body-editor" style="min-height: 180px; font-family: inherit; font-size: 0.95rem; border: none; color: var(--text-primary);"></div>
|
||
</div>
|
||
<!-- Attachments List for note -->
|
||
<div class="upload-file-list" id="note-file-list" style="margin-bottom: var(--space-md);"></div>
|
||
|
||
<div style="display:flex; gap:var(--space-md); justify-content:space-between; align-items:center; margin-top:var(--space-md);">
|
||
<div>
|
||
<input type="file" id="note-attachments" multiple style="display:none;" />
|
||
<button type="button" class="btn btn-ghost btn-sm" id="btn-note-add-attachments" style="height:32px; padding: 4px 10px; font-size: 0.85rem; display: flex; align-items: center; gap: 4px;">📎 Allega file</button>
|
||
</div>
|
||
<div style="display:flex; gap:var(--space-md); align-items:center;">
|
||
<input type="number" step="any" min="0" class="note-subject-input" id="note-time-units" placeholder="Tempo (minuti)" style="width:140px; margin-bottom:0; height:32px; padding:4px 10px; font-size:0.85rem;" />
|
||
<button class="btn btn-ghost btn-sm" id="btn-open-email-compose" style="height:32px; display:flex; align-items:center; gap:var(--space-xs); border-color:var(--accent-secondary); color:var(--accent-secondary);"
|
||
data-ticket-id="${ticket.id}" data-ticket-tn="${ticket.tn}" data-ticket-title="${App.escapeHtml(ticket.title)}" data-customer-email="${App.escapeHtml(ticket.customer_email || '')}">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
|
||
${emailBtnText}
|
||
</button>
|
||
<button class="btn btn-primary btn-sm" id="note-send" style="height:32px; display:flex; align-items:center; gap:var(--space-xs);">
|
||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:14px;height:14px;"><path d="M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z"/></svg>
|
||
Invia Nota
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Articles Timeline -->
|
||
<div>
|
||
<div class="article-header-wrapper">
|
||
<div class="card-title" style="margin-bottom:0;">Articoli & Note (${articles.length})</div>
|
||
<label class="html-toggle-container">
|
||
<input type="checkbox" class="html-toggle-input" id="html-toggle" ${this.htmlMode ? 'checked' : ''} />
|
||
<span>Visualizza HTML</span>
|
||
</label>
|
||
</div>
|
||
<div class="articles-timeline">
|
||
${articles.length > 0 ? articles.map(a => {
|
||
const hasHtml = (a.a_content_type || '').toLowerCase().includes('html') || a.a_body.includes('</') || a.a_body.includes('/>');
|
||
const displayBody = this.htmlMode && hasHtml
|
||
? `<iframe srcdoc="${a.a_body.replace(/"/g, '"')}" style="width:100%; border:none; background:var(--bg-card); border-radius:var(--radius-md); min-height:220px; font-family:inherit; color-scheme: dark;"></iframe>`
|
||
: `<div class="article-body">${App.escapeHtml(a.a_body || '')}</div>`;
|
||
|
||
const articleAttachments = (attachments || []).filter(att => att.article_id === a.article_id && att.filename !== 'file-1' && att.filename !== 'file-2');
|
||
|
||
return `
|
||
<div class="article-card sender-${(a.sender_type || 'system').toLowerCase()}">
|
||
<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>` : ''}
|
||
<button class="btn-delete-article" data-article-id="${a.article_id}" style="background:none; border:none; cursor:pointer; font-size:0.85rem; padding: 2px; margin-left: var(--space-xs); display:inline-flex; align-items:center; opacity: 0.6; transition: opacity 0.2s;" onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=0.6" title="Elimina Articolo">🗑️</button>
|
||
<button class="btn-email-article" data-article-id="${a.article_id}" style="background:none; border:none; cursor:pointer; font-size:0.85rem; padding: 2px; margin-left: var(--space-xs); display:inline-flex; align-items:center; opacity: 0.6; transition: opacity 0.2s;" onmouseover="this.style.opacity=1" onmouseout="this.style.opacity=0.6" title="Rispondi via email (Quota questo articolo)">✉️</button>
|
||
</div>
|
||
<div style="display:flex; gap: var(--space-sm); align-items:center;">
|
||
<div class="time-edit-container" data-article-id="${a.article_id}" style="display:inline-flex; align-items:center; gap:4px; position:relative;">
|
||
${a.time_unit ? `
|
||
<span class="badge" style="background:var(--info-bg);color:var(--info);font-size:0.75rem;padding:2px 8px;border-radius:4px;">⏱ <span class="time-value-display">${parseFloat(a.time_unit)}</span> min</span>
|
||
` : ''}
|
||
<button class="retrodate-btn-edit btn-edit-article-time" style="background:none; border:none; cursor:pointer; font-size:0.75rem; padding: 2px 4px; display:inline-flex; align-items:center;" title="${a.time_unit ? 'Modifica Tempo' : 'Aggiungi Tempo'}">
|
||
${a.time_unit ? '✏️' : '⏱+✏️'}
|
||
</button>
|
||
<div class="time-editor-popover" style="display:none; position:absolute; right:0; top: 100%; background:var(--bg-card); border:1px solid var(--border-subtle); padding:var(--space-xs); border-radius:var(--radius-sm); z-index:100; box-shadow:var(--shadow-md); margin-top:4px;">
|
||
<input type="number" step="any" min="0" class="form-input input-article-time" value="${a.time_unit ? parseFloat(a.time_unit) : 0}" style="width:70px; height:24px; padding:2px 6px; font-size:0.8rem; margin-bottom:4px; display:block;" />
|
||
<div style="display:flex; gap:4px; justify-content:flex-end;">
|
||
<button class="btn btn-primary btn-sm btn-save-article-time" style="height:20px; padding:0 6px; font-size:0.75rem; line-height:20px;">✓</button>
|
||
<button class="btn btn-ghost btn-sm btn-cancel-article-time" style="height:20px; padding:0 6px; font-size:0.75rem; line-height:20px;">✗</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<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>` : ''}
|
||
${displayBody}
|
||
|
||
${articleAttachments.length > 0 ? `
|
||
<div class="article-attachments">
|
||
${articleAttachments.map(att => `
|
||
<a href="/api/tickets/attachments/${att.id}" class="attachment-badge" target="_blank" download="${att.filename}">
|
||
<span>📎</span>
|
||
<strong>${App.escapeHtml(att.filename)}</strong>
|
||
<span class="attachment-size">(${Math.round(att.content_size / 1024)} KB)</span>
|
||
</a>
|
||
`).join('')}
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
`;
|
||
}).join('') : `
|
||
<div class="empty-state" style="padding:var(--space-lg);">
|
||
<div class="empty-state-icon">💬</div>
|
||
<div class="empty-state-text">Nessun articolo</div>
|
||
</div>
|
||
`}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Sidebar -->
|
||
<div class="ticket-sidebar">
|
||
<div class="sidebar-panel">
|
||
<div class="sidebar-panel-title">Dettagli</div>
|
||
<div class="meta-row">
|
||
<span class="meta-label">Numero</span>
|
||
<span class="meta-value" style="font-family:monospace;">${ticket.tn}</span>
|
||
</div>
|
||
<div class="meta-row">
|
||
<span class="meta-label">Stato</span>
|
||
<span class="meta-value"><span class="badge badge-state" data-state-type="${(ticket.state_type || '').toLowerCase()}">${ticket.state_name}</span></span>
|
||
</div>
|
||
<div class="meta-row">
|
||
<span class="meta-label">Creato</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>
|
||
<span class="meta-value">${App.formatDateTime(ticket.change_time)}</span>
|
||
</div>
|
||
<div class="meta-row">
|
||
<span class="meta-label">Lock</span>
|
||
<span class="meta-value">${ticket.lock_name || 'unlock'}</span>
|
||
</div>
|
||
${ticket.type_name ? `
|
||
<div class="meta-row">
|
||
<span class="meta-label">Tipo</span>
|
||
<span class="meta-value">${ticket.type_name}</span>
|
||
</div>
|
||
` : ''}
|
||
<div class="meta-row">
|
||
<span class="meta-label">Owner</span>
|
||
<span class="meta-value">${ticket.owner_first ? `${ticket.owner_first} ${ticket.owner_last}` : (ticket.owner_login || '—')}</span>
|
||
</div>
|
||
<div class="meta-row">
|
||
<span class="meta-label">Responsabile</span>
|
||
<span class="meta-value">${ticket.responsible_first ? `${ticket.responsible_first} ${ticket.responsible_last}` : '—'}</span>
|
||
</div>
|
||
${totalTime > 0 ? `
|
||
<div class="meta-row">
|
||
<span class="meta-label">Tempo Totale</span>
|
||
<span class="meta-value" style="font-weight:bold;color:var(--info);">⏱ ${totalTime} min</span>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
|
||
${ticket.customer_user_id || ticket.customer_id ? `
|
||
<div class="sidebar-panel">
|
||
<div class="sidebar-panel-title">Cliente</div>
|
||
${ticket.customer_first ? `
|
||
<div class="meta-row">
|
||
<span class="meta-label">Nome</span>
|
||
<span class="meta-value">${ticket.customer_first} ${ticket.customer_last}</span>
|
||
</div>
|
||
` : ''}
|
||
${ticket.customer_email ? `
|
||
<div class="meta-row">
|
||
<span class="meta-label">Email</span>
|
||
<span class="meta-value" style="font-size:0.78rem;">${ticket.customer_email}</span>
|
||
</div>
|
||
` : ''}
|
||
${ticket.customer_phone ? `
|
||
<div class="meta-row">
|
||
<span class="meta-label">Telefono</span>
|
||
<span class="meta-value">${ticket.customer_phone}</span>
|
||
</div>
|
||
` : ''}
|
||
${ticket.customer_id ? `
|
||
<div class="meta-row">
|
||
<span class="meta-label">Customer ID</span>
|
||
<span class="meta-value" style="font-size:0.78rem;">${ticket.customer_id}</span>
|
||
</div>
|
||
` : ''}
|
||
</div>
|
||
` : ''}
|
||
|
||
${ticket.escalation_time > 0 ? `
|
||
<div class="sidebar-panel" style="border-color: rgba(239,68,68,0.3);">
|
||
<div class="sidebar-panel-title" style="color:var(--error);">⚠ Escalation</div>
|
||
<div class="meta-row">
|
||
<span class="meta-label">Tempo</span>
|
||
<span class="meta-value" style="color:var(--error);">${new Date(ticket.escalation_time * 1000).toLocaleString('it-IT')}</span>
|
||
</div>
|
||
</div>
|
||
` : ''}
|
||
|
||
<!-- Panel per i Gruppi Ticket -->
|
||
<div class="sidebar-panel" id="ticket-groups-panel">
|
||
<div class="sidebar-panel-title">Gruppi Ticket</div>
|
||
<div id="ticket-groups-list" style="margin-bottom: var(--space-sm);">
|
||
${groupsData.asMaster.length === 0 && groupsData.asMember.length === 0 ? `
|
||
<div style="font-size:0.78rem; color:var(--text-muted); padding:4px 0;">Nessun gruppo associato</div>
|
||
` : ''}
|
||
${groupsData.asMaster.map(g => `
|
||
<div style="font-size:0.8rem; margin-bottom:4px;">
|
||
<span style="color:var(--accent-primary); font-weight:bold;">👑 Master in:</span>
|
||
<a href="#/tickets/groups" onclick="localStorage.setItem('otrs_selected_group_id', ${g.id})" style="color:var(--text-primary); text-decoration:none; border-bottom:1px dashed var(--text-muted);">${App.escapeHtml(g.nome)}</a>
|
||
</div>
|
||
`).join('')}
|
||
${groupsData.asMember.map(g => `
|
||
<div style="font-size:0.8rem; margin-bottom:4px;">
|
||
<span style="color:var(--text-secondary); font-weight:bold;">🔗 Membro di:</span>
|
||
<a href="#/tickets/groups" onclick="localStorage.setItem('otrs_selected_group_id', ${g.id})" style="color:var(--text-primary); text-decoration:none; border-bottom:1px dashed var(--text-muted);">${App.escapeHtml(g.nome)}</a>
|
||
</div>
|
||
`).join('')}
|
||
</div>
|
||
|
||
<div style="border-top:1px solid var(--border-subtle); padding-top:var(--space-xs); margin-top:var(--space-xs);">
|
||
<label class="quick-edit-label">Associa a Gruppo</label>
|
||
<div style="display:flex; gap:4px; margin-top:4px;">
|
||
<select class="form-select" id="group-association-select" style="padding:4px 20px 4px 8px; font-size:0.78rem; height:28px; margin:0; flex:1;">
|
||
<option value="">-- Seleziona --</option>
|
||
</select>
|
||
<button class="btn btn-primary btn-sm" id="btn-associate-to-group" style="height:28px; padding:0 8px; font-size:0.75rem;">+ Aggiungi</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
if (window.Quill) {
|
||
this.noteQuill = new Quill('#note-body-editor', {
|
||
theme: 'snow',
|
||
placeholder: 'Scrivi una nota interna...',
|
||
modules: {
|
||
toolbar: [
|
||
['bold', 'italic', 'underline', 'strike'],
|
||
[{ 'list': 'ordered'}, { 'list': 'bullet' }],
|
||
['link', 'image'],
|
||
['clean']
|
||
],
|
||
keyboard: {
|
||
bindings: {
|
||
tab: {
|
||
key: 'Tab',
|
||
handler: function() {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// 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.noteQuill = null;
|
||
}
|
||
|
||
// Restore Note Draft
|
||
const noteDraft = App.getDraft(id, 'note');
|
||
if (noteDraft) {
|
||
if (this.noteQuill && noteDraft.body) {
|
||
this.noteQuill.root.innerHTML = noteDraft.body;
|
||
}
|
||
if (document.getElementById('note-subject')) {
|
||
document.getElementById('note-subject').value = noteDraft.subject || '';
|
||
}
|
||
if (document.getElementById('note-time-units')) {
|
||
document.getElementById('note-time-units').value = noteDraft.time_unit || '';
|
||
}
|
||
this.noteAttachments = noteDraft.attachments || [];
|
||
this.updateNoteAttachmentList();
|
||
}
|
||
|
||
this.bindEvents(ticket, articles, container, groupsData);
|
||
|
||
} catch (err) {
|
||
container.innerHTML = `
|
||
<div class="empty-state">
|
||
<div class="empty-state-icon">⚠️</div>
|
||
<div class="empty-state-text">Errore caricamento ticket</div>
|
||
<div class="empty-state-sub">${App.escapeHtml(err.message)}</div>
|
||
<button class="btn btn-ghost" style="margin-top:var(--space-md);" onclick="history.back()">Torna indietro</button>
|
||
</div>
|
||
`;
|
||
}
|
||
},
|
||
|
||
bindEvents(ticket, articles, container, groupsData) {
|
||
// Quick-edit change detection
|
||
const fields = document.querySelectorAll('.quick-edit-select:not(#qe-queue-search), #qe-queue, #qe-customer-user-id, #qe-customer-id');
|
||
const saveBtn = document.getElementById('qe-save');
|
||
const resetBtn = document.getElementById('qe-reset');
|
||
const timeUnitInput = document.getElementById('qe-time-unit');
|
||
|
||
const checkChanges = () => {
|
||
let hasChanges = false;
|
||
fields.forEach(el => {
|
||
const field = el.dataset.field;
|
||
const original = String(this.originalValues[field] || '');
|
||
const current = el.value;
|
||
const changed = current !== original;
|
||
if (el.id === 'qe-queue') {
|
||
const searchInput = document.getElementById('qe-queue-search');
|
||
if (searchInput) searchInput.classList.toggle('changed', changed);
|
||
} else {
|
||
el.classList.toggle('changed', changed);
|
||
}
|
||
if (changed) hasChanges = true;
|
||
});
|
||
|
||
const timeVal = parseFloat(timeUnitInput ? timeUnitInput.value : '0') || 0;
|
||
if (timeVal > 0) {
|
||
hasChanges = true;
|
||
}
|
||
|
||
saveBtn.disabled = !hasChanges;
|
||
};
|
||
|
||
fields.forEach(el => el.addEventListener('change', checkChanges));
|
||
if (timeUnitInput) {
|
||
timeUnitInput.addEventListener('input', checkChanges);
|
||
timeUnitInput.addEventListener('change', checkChanges);
|
||
}
|
||
|
||
// Queue Autocomplete inside Quick Edit
|
||
const queueSearchInput = document.getElementById('qe-queue-search');
|
||
const queueSuggestionsDiv = document.getElementById('qe-queue-suggestions');
|
||
const queueIdInput = document.getElementById('qe-queue');
|
||
|
||
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 (queueSearchInput.value.trim() !== q) {
|
||
return;
|
||
}
|
||
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/mousedown
|
||
queueSuggestionsDiv.querySelectorAll('.autocomplete-suggestion-item').forEach(item => {
|
||
if (item.dataset.id) {
|
||
item.addEventListener('mousedown', (e) => {
|
||
e.preventDefault();
|
||
queueSearchInput.value = item.dataset.name;
|
||
queueIdInput.value = item.dataset.id;
|
||
queueSuggestionsDiv.style.display = 'none';
|
||
queueIdInput.dispatchEvent(new Event('change'));
|
||
});
|
||
}
|
||
});
|
||
} catch (err) {
|
||
console.error(err);
|
||
}
|
||
}, 150);
|
||
});
|
||
|
||
queueSearchInput.addEventListener('focus', () => {
|
||
queueSearchInput.value = '';
|
||
queueIdInput.value = '';
|
||
queueSearchInput.dispatchEvent(new Event('input'));
|
||
});
|
||
|
||
queueSearchInput.addEventListener('blur', () => {
|
||
setTimeout(() => { queueSuggestionsDiv.style.display = 'none'; }, 150);
|
||
});
|
||
}
|
||
|
||
// Customer User Autocomplete inside Quick Edit
|
||
const customerSearchInput = document.getElementById('qe-customer-search');
|
||
const customerSuggestionsDiv = document.getElementById('qe-customer-suggestions');
|
||
const customerUserIdInput = document.getElementById('qe-customer-user-id');
|
||
const customerIdInput = document.getElementById('qe-customer-id');
|
||
|
||
let customerDebounce;
|
||
if (customerSearchInput) {
|
||
customerSearchInput.addEventListener('input', () => {
|
||
clearTimeout(customerDebounce);
|
||
const q = customerSearchInput.value.trim();
|
||
|
||
customerDebounce = setTimeout(async () => {
|
||
try {
|
||
const users = await App.api(`/api/customer-users/search?q=${encodeURIComponent(q)}`);
|
||
console.log('[Frontend LDAP Search Detail] Users returned:', users);
|
||
// Prevent race conditions: discard results if the input value has changed
|
||
if (customerSearchInput.value.trim() !== q) {
|
||
console.log('[Frontend LDAP Search Detail] Discarding stale results for query:', q);
|
||
return;
|
||
}
|
||
if (users.length === 0) {
|
||
customerSuggestionsDiv.innerHTML = '<div class="autocomplete-suggestion-item" style="color:var(--text-muted); cursor:default;">Nessun utente trovato</div>';
|
||
customerSuggestionsDiv.style.display = 'block';
|
||
return;
|
||
}
|
||
|
||
customerSuggestionsDiv.innerHTML = users.map(u => `
|
||
<div class="autocomplete-suggestion-item" data-login="${App.escapeHtml(u.login || '')}" data-customer-id="${App.escapeHtml(u.customer_id || '')}" data-name="${App.escapeHtml((u.first_name + ' ' + u.last_name).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('');
|
||
customerSuggestionsDiv.style.display = 'block';
|
||
|
||
// Use mousedown instead of click to fire before blur event
|
||
customerSuggestionsDiv.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
|
||
customerSearchInput.value = item.dataset.name;
|
||
customerUserIdInput.value = item.dataset.login;
|
||
customerIdInput.value = item.dataset.customerId || '';
|
||
customerSuggestionsDiv.style.display = 'none';
|
||
customerUserIdInput.dispatchEvent(new Event('change'));
|
||
customerIdInput.dispatchEvent(new Event('change'));
|
||
});
|
||
}
|
||
});
|
||
} catch (err) {
|
||
console.error(err);
|
||
}
|
||
}, 300);
|
||
});
|
||
customerSearchInput.addEventListener('focus', () => {
|
||
if (!customerUserIdInput.value) {
|
||
customerSearchInput.dispatchEvent(new Event('input'));
|
||
}
|
||
});
|
||
customerSearchInput.addEventListener('blur', () => {
|
||
setTimeout(() => { customerSuggestionsDiv.style.display = 'none'; }, 150);
|
||
});
|
||
}
|
||
|
||
// Reset quick-edit
|
||
resetBtn.addEventListener('click', () => {
|
||
fields.forEach(el => {
|
||
el.value = this.originalValues[el.dataset.field] || '';
|
||
el.classList.remove('changed');
|
||
});
|
||
if (queueSearchInput) {
|
||
queueSearchInput.classList.remove('changed');
|
||
queueSearchInput.value = this.originalValues['queue_name'] || '';
|
||
}
|
||
if (customerSearchInput) {
|
||
const first = this.originalValues['customer_first'] || '';
|
||
const last = this.originalValues['customer_last'] || '';
|
||
customerSearchInput.value = first ? `${first} ${last}` : (this.originalValues['customer_user_id'] || '');
|
||
}
|
||
if (timeUnitInput) {
|
||
timeUnitInput.value = '';
|
||
}
|
||
saveBtn.disabled = true;
|
||
});
|
||
|
||
// Save quick-edit
|
||
saveBtn.addEventListener('click', async () => {
|
||
const queueSearch = queueSearchInput ? queueSearchInput.value.trim() : '';
|
||
if (queueSearchInput && !queueIdInput.value) {
|
||
Toast.warning('Seleziona una coda valida dall\'elenco.');
|
||
return;
|
||
}
|
||
|
||
const customerSearch = customerSearchInput ? customerSearchInput.value.trim() : '';
|
||
if (customerSearch && !customerUserIdInput.value) {
|
||
customerUserIdInput.value = customerSearch;
|
||
customerIdInput.value = customerSearch;
|
||
}
|
||
|
||
const updates = {};
|
||
fields.forEach(el => {
|
||
const field = el.dataset.field;
|
||
let val;
|
||
if (field === 'customer_id' || field === 'customer_user_id') {
|
||
val = el.value || null;
|
||
} else {
|
||
val = el.value ? parseInt(el.value) : null;
|
||
}
|
||
|
||
const origVal = this.originalValues[field] || null;
|
||
if (val !== origVal) {
|
||
updates[field] = val;
|
||
}
|
||
});
|
||
|
||
if (timeUnitInput && timeUnitInput.value.trim()) {
|
||
const timeVal = parseFloat(timeUnitInput.value.trim());
|
||
if (!isNaN(timeVal) && timeVal > 0) {
|
||
updates.time_unit = timeVal;
|
||
}
|
||
}
|
||
|
||
if (Object.keys(updates).length === 0) return;
|
||
|
||
try {
|
||
saveBtn.disabled = true;
|
||
saveBtn.textContent = 'Salvando...';
|
||
const res = await App.api(`/api/tickets/${this.ticketId}`, {
|
||
method: 'PATCH',
|
||
body: JSON.stringify(updates),
|
||
});
|
||
Toast.success(res.message || 'Ticket aggiornato!');
|
||
// Refresh the view
|
||
this.render(this.ticketId);
|
||
} catch (err) {
|
||
Toast.error('Errore: ' + err.message);
|
||
saveBtn.disabled = false;
|
||
saveBtn.textContent = 'Salva Modifiche';
|
||
}
|
||
});
|
||
|
||
// Send note
|
||
const noteSendBtn = document.getElementById('note-send');
|
||
noteSendBtn.addEventListener('click', async () => {
|
||
let body = this.noteQuill ? this.noteQuill.root.innerHTML.trim() : '';
|
||
if (body === '<p><br></p>') {
|
||
body = '';
|
||
}
|
||
let subject = document.getElementById('note-subject').value.trim();
|
||
const time_unit = document.getElementById('note-time-units').value.trim();
|
||
|
||
// If body is empty but subject is provided, fill body with subject text
|
||
if (!body && subject) {
|
||
body = `<p>${App.escapeHtml(subject)}</p>`;
|
||
if (this.noteQuill) {
|
||
this.noteQuill.root.innerHTML = body;
|
||
}
|
||
}
|
||
|
||
// If subject is empty but body is provided, fill subject with truncated plain text of body
|
||
if (body && !subject) {
|
||
const tempDiv = document.createElement('div');
|
||
tempDiv.innerHTML = body;
|
||
const plainText = tempDiv.textContent || tempDiv.innerText || '';
|
||
subject = plainText.trim().substring(0, 50) || 'Nota interna';
|
||
document.getElementById('note-subject').value = subject;
|
||
}
|
||
|
||
// If both are still empty, show warning
|
||
if (!body && !subject) {
|
||
Toast.warning('Inserisci un oggetto o il corpo della nota prima di inviare');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
noteSendBtn.disabled = true;
|
||
noteSendBtn.innerHTML = '<div class="spinner" style="width:14px;height:14px;border-width:2px;"></div> Invio...';
|
||
|
||
const res = await App.api(`/api/tickets/${this.ticketId}/articles`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({
|
||
subject,
|
||
body,
|
||
time_unit,
|
||
attachments: this.noteAttachments.length > 0 ? this.noteAttachments : undefined
|
||
}),
|
||
});
|
||
|
||
this.noteAttachments = []; // Clear attachments
|
||
Toast.success(res.message || 'Nota aggiunta!');
|
||
App.clearDraft(this.ticketId, 'note');
|
||
App.updateDailyTimer();
|
||
this.render(this.ticketId);
|
||
} catch (err) {
|
||
Toast.error('Errore: ' + err.message);
|
||
noteSendBtn.disabled = false;
|
||
noteSendBtn.innerHTML = 'Invia Nota';
|
||
}
|
||
});
|
||
|
||
// Note Attachments Upload Handlers
|
||
const noteAttachBtn = document.getElementById('btn-note-add-attachments');
|
||
const noteFileInput = document.getElementById('note-attachments');
|
||
|
||
if (noteAttachBtn && noteFileInput) {
|
||
noteAttachBtn.addEventListener('click', () => noteFileInput.click());
|
||
|
||
noteFileInput.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.noteAttachments.push({
|
||
filename: file.name,
|
||
content: base64Data,
|
||
content_type: file.type
|
||
});
|
||
this.updateNoteAttachmentList();
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
noteFileInput.value = ''; // Reset input
|
||
});
|
||
}
|
||
|
||
// Email Compose Button
|
||
const btnEmailCompose = document.getElementById('btn-open-email-compose');
|
||
if (btnEmailCompose) {
|
||
btnEmailCompose.addEventListener('click', () => {
|
||
if (window.EmailCompose) {
|
||
const tId = parseInt(btnEmailCompose.dataset.ticketId, 10);
|
||
const emailDraft = App.getDraft(tId, 'email');
|
||
if (emailDraft) {
|
||
EmailCompose.open({
|
||
...emailDraft.options,
|
||
draft: emailDraft
|
||
});
|
||
} else {
|
||
EmailCompose.open({
|
||
ticketId: tId,
|
||
ticketTn: btnEmailCompose.dataset.ticketTn,
|
||
ticketTitle: btnEmailCompose.dataset.ticketTitle,
|
||
customerEmail: btnEmailCompose.dataset.customerEmail,
|
||
});
|
||
}
|
||
} else {
|
||
Toast.error('Modulo email non disponibile');
|
||
}
|
||
});
|
||
}
|
||
|
||
// Article Email Reply Buttons
|
||
document.querySelectorAll('.btn-email-article').forEach(btn => {
|
||
btn.addEventListener('click', () => {
|
||
const articleId = parseInt(btn.dataset.articleId, 10);
|
||
const article = articles.find(art => art.article_id == articleId);
|
||
if (!article) return;
|
||
|
||
if (window.EmailCompose) {
|
||
const hasHtml = (article.a_content_type || '').toLowerCase().includes('html') || article.a_body.includes('</') || article.a_body.includes('/>');
|
||
const quotedBody = hasHtml ? article.a_body : App.escapeHtml(article.a_body || '').replace(/\n/g, '<br>');
|
||
|
||
const initialBodyHtml = `
|
||
<p><br></p>
|
||
<p>Il ${App.formatDateTime(article.create_time)}, <strong>${App.escapeHtml(article.a_from || 'Sistema')}</strong> ha scritto:</p>
|
||
<blockquote style="border-left: 2px solid var(--border-subtle, #444); padding-left: 12px; margin-left: 8px; color: var(--text-secondary);">
|
||
${quotedBody}
|
||
</blockquote>
|
||
<p><br></p>
|
||
`;
|
||
|
||
// Helper to extract email addresses from headers
|
||
const parseEmails = (str) => {
|
||
if (!str) return [];
|
||
return (str.match(/[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g) || [])
|
||
.map(email => email.toLowerCase().trim());
|
||
};
|
||
|
||
const fromEmails = parseEmails(article.a_from);
|
||
const toEmails = parseEmails(article.a_to);
|
||
const ccEmails = parseEmails(article.a_cc);
|
||
|
||
// Exclude list (helpdesk and active agent)
|
||
const config = App.lookups.config || {};
|
||
const helpdeskEmail = (config.helpdeskEmail || 'helpdesk@pharmaidea.com').toLowerCase();
|
||
const agentEmail = (config.agentEmail || '').toLowerCase();
|
||
const excludeEmails = [helpdeskEmail, agentEmail].filter(Boolean);
|
||
|
||
const initialToSet = new Set();
|
||
fromEmails.forEach(e => {
|
||
if (!excludeEmails.includes(e) && !e.includes('helpdesk')) {
|
||
initialToSet.add(e);
|
||
}
|
||
});
|
||
if (initialToSet.size === 0 && ticket.customer_email) {
|
||
initialToSet.add(ticket.customer_email.toLowerCase());
|
||
}
|
||
|
||
const initialCcSet = new Set();
|
||
[...toEmails, ...ccEmails].forEach(e => {
|
||
if (!excludeEmails.includes(e) && !e.includes('helpdesk') && !initialToSet.has(e)) {
|
||
initialCcSet.add(e);
|
||
}
|
||
});
|
||
|
||
let inReplyTo = article.a_message_id || '';
|
||
let references = '';
|
||
if (article.a_references) {
|
||
references = article.a_references + (article.a_message_id ? ' ' + article.a_message_id : '');
|
||
} else if (article.a_message_id) {
|
||
references = article.a_message_id;
|
||
}
|
||
|
||
EmailCompose.open({
|
||
ticketId: ticket.id,
|
||
ticketTn: ticket.tn,
|
||
ticketTitle: ticket.title,
|
||
initialTo: Array.from(initialToSet),
|
||
initialCc: Array.from(initialCcSet),
|
||
initialBodyHtml: initialBodyHtml,
|
||
inReplyTo: inReplyTo,
|
||
references: references,
|
||
});
|
||
} else {
|
||
Toast.error('Modulo email non disponibile');
|
||
}
|
||
});
|
||
});
|
||
|
||
// 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;
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// Time Edit Event Listeners (Issue 21)
|
||
document.querySelectorAll('.time-edit-container[data-article-id]').forEach(container => {
|
||
const articleId = container.dataset.articleId;
|
||
const btnEdit = container.querySelector('.btn-edit-article-time');
|
||
const popover = container.querySelector('.time-editor-popover');
|
||
const btnSave = container.querySelector('.btn-save-article-time');
|
||
const btnCancel = container.querySelector('.btn-cancel-article-time');
|
||
const input = container.querySelector('.input-article-time');
|
||
|
||
if (btnEdit) {
|
||
btnEdit.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
// Close all other open time edit popovers
|
||
document.querySelectorAll('.time-editor-popover').forEach(p => {
|
||
if (p !== popover) p.style.display = 'none';
|
||
});
|
||
const isOpen = popover.style.display === 'block';
|
||
popover.style.display = isOpen ? 'none' : 'block';
|
||
if (!isOpen) {
|
||
input.focus();
|
||
input.select();
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnCancel) {
|
||
btnCancel.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
popover.style.display = 'none';
|
||
});
|
||
}
|
||
|
||
if (btnSave) {
|
||
btnSave.addEventListener('click', async (e) => {
|
||
e.stopPropagation();
|
||
const val = parseFloat(input.value);
|
||
if (isNaN(val) || val < 0) {
|
||
Toast.warning('Inserisci un valore numerico valido maggiore o uguale a 0.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
btnSave.disabled = true;
|
||
await App.api(`/api/tickets/articles/${articleId}/time`, {
|
||
method: 'PUT',
|
||
body: JSON.stringify({ time_unit: val })
|
||
});
|
||
Toast.success('Tempo consuntivato aggiornato con successo!');
|
||
popover.style.display = 'none';
|
||
this.render(this.ticketId);
|
||
} catch (err) {
|
||
Toast.error('Errore durante l\'aggiornamento del tempo: ' + err.message);
|
||
btnSave.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
|
||
// Stop propagation so clicking inside doesn't close it
|
||
if (popover) {
|
||
popover.addEventListener('click', (e) => e.stopPropagation());
|
||
}
|
||
});
|
||
|
||
// Close popovers clicking outside
|
||
document.addEventListener('click', () => {
|
||
document.querySelectorAll('.time-editor-popover').forEach(p => {
|
||
p.style.display = 'none';
|
||
});
|
||
});
|
||
// Delete Article Event Listeners
|
||
document.querySelectorAll('.btn-delete-article').forEach(btn => {
|
||
btn.addEventListener('click', async (e) => {
|
||
e.stopPropagation();
|
||
const articleId = btn.dataset.articleId;
|
||
const ok = await App.confirm(
|
||
"Elimina Articolo",
|
||
"Sei sicuro di voler eliminare questa nota/articolo? Tutti i file allegati e i tempi ad esso associati verranno rimossi permanentemente.",
|
||
{ confirmText: 'Elimina', cancelText: 'Annulla' }
|
||
);
|
||
if (ok) {
|
||
try {
|
||
btn.disabled = true;
|
||
await App.api(`/api/tickets/articles/${articleId}`, {
|
||
method: 'DELETE'
|
||
});
|
||
Toast.success('Articolo/nota eliminato con successo!');
|
||
this.render(this.ticketId);
|
||
} catch (err) {
|
||
Toast.error('Errore durante l\'eliminazione: ' + err.message);
|
||
btn.disabled = false;
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// HTML Mode Toggle Listener
|
||
const htmlToggle = document.getElementById('html-toggle');
|
||
if (htmlToggle) {
|
||
htmlToggle.addEventListener('change', (e) => {
|
||
const isChecked = e.target.checked;
|
||
localStorage.setItem('otrs_turbo_html_mode', isChecked);
|
||
this.htmlMode = isChecked;
|
||
this.render(this.ticketId);
|
||
});
|
||
}
|
||
|
||
// Populate Group Association dropdown
|
||
App.api('/api/groups').then(allGroups => {
|
||
const select = document.getElementById('group-association-select');
|
||
if (select) {
|
||
allGroups.forEach(g => {
|
||
const isMember = groupsData.asMember.some(m => m.id === g.id);
|
||
const isMaster = groupsData.asMaster.some(m => m.id === g.id);
|
||
if (!isMember && !isMaster) {
|
||
const opt = document.createElement('option');
|
||
opt.value = g.id;
|
||
opt.textContent = g.nome;
|
||
select.appendChild(opt);
|
||
}
|
||
});
|
||
}
|
||
}).catch(err => console.warn('Failed to load groups for association dropdown', err));
|
||
|
||
const btnAssociate = document.getElementById('btn-associate-to-group');
|
||
if (btnAssociate) {
|
||
btnAssociate.addEventListener('click', async () => {
|
||
const select = document.getElementById('group-association-select');
|
||
const groupId = select.value;
|
||
if (!groupId) {
|
||
sessionStorage.setItem('otrs_create_group_with_master_tn', ticket.tn);
|
||
window.location.hash = '#/tickets/groups';
|
||
return;
|
||
}
|
||
|
||
try {
|
||
btnAssociate.disabled = true;
|
||
await App.api(`/api/groups/${groupId}/tickets`, {
|
||
method: 'POST',
|
||
body: JSON.stringify({ ticket_identifier: this.ticketId })
|
||
});
|
||
Toast.success('Ticket associato al gruppo!');
|
||
this.render(this.ticketId);
|
||
} catch (err) {
|
||
Toast.error('Errore associazione: ' + err.message);
|
||
btnAssociate.disabled = false;
|
||
}
|
||
});
|
||
}
|
||
},
|
||
};
|