feat: funzione per l'invio della posta elettronica da otrs-turbo. fix: corretta consuntivazione automatica. feat: aggiunto link apri in otrs anche nel numero del ticket in cima
This commit is contained in:
@@ -0,0 +1,470 @@
|
||||
/**
|
||||
* emailCompose.js
|
||||
* Modal per la composizione e invio email dal contesto di un ticket.
|
||||
* Utilizza Quill.js per l'editor HTML, supporta allegati e immagini inline.
|
||||
*/
|
||||
|
||||
const EmailCompose = (() => {
|
||||
let quillEditor = null;
|
||||
let attachmentsList = [];
|
||||
let currentOptions = {};
|
||||
|
||||
// ── CSS ──────────────────────────────────────────────────────────────────────
|
||||
function injectStyles() {
|
||||
if (document.getElementById('email-compose-styles')) return;
|
||||
const style = document.createElement('style');
|
||||
style.id = 'email-compose-styles';
|
||||
style.textContent = `
|
||||
#email-compose-overlay {
|
||||
position: fixed; inset: 0; z-index: 9000;
|
||||
background: rgba(0,0,0,0.55);
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
animation: fadeIn 0.15s ease;
|
||||
}
|
||||
@keyframes fadeIn { from { opacity:0 } to { opacity:1 } }
|
||||
|
||||
#email-compose-modal {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-xl, 14px);
|
||||
box-shadow: 0 24px 80px rgba(0,0,0,0.35);
|
||||
width: min(860px, 95vw);
|
||||
max-height: 92vh;
|
||||
display: flex; flex-direction: column;
|
||||
animation: slideUp 0.18s ease;
|
||||
}
|
||||
@keyframes slideUp { from { transform: translateY(20px); opacity:0 } to { transform: translateY(0); opacity:1 } }
|
||||
|
||||
#email-compose-modal .ec-header {
|
||||
padding: 16px 20px 12px;
|
||||
border-bottom: 1px solid var(--border-subtle);
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#email-compose-modal .ec-title {
|
||||
font-size: 1rem; font-weight: 600; color: var(--text-primary);
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
#email-compose-modal .ec-body {
|
||||
padding: 16px 20px;
|
||||
overflow-y: auto;
|
||||
flex: 1;
|
||||
display: flex; flex-direction: column; gap: 12px;
|
||||
}
|
||||
#email-compose-modal .ec-field {
|
||||
display: flex; flex-direction: column; gap: 4px;
|
||||
}
|
||||
#email-compose-modal .ec-label {
|
||||
font-size: 0.75rem; font-weight: 600; color: var(--text-secondary);
|
||||
text-transform: uppercase; letter-spacing: 0.04em;
|
||||
}
|
||||
#email-compose-modal .ec-tags-input {
|
||||
display: flex; flex-wrap: wrap; gap: 4px; align-items: center;
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 6px 10px; min-height: 36px; cursor: text;
|
||||
}
|
||||
#email-compose-modal .ec-tags-input:focus-within {
|
||||
border-color: var(--accent-primary);
|
||||
box-shadow: 0 0 0 3px rgba(var(--accent-rgb,99,102,241),0.12);
|
||||
}
|
||||
#email-compose-modal .ec-tag {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
background: var(--accent-primary); color: #fff;
|
||||
border-radius: 4px; padding: 2px 6px; font-size: 0.78rem;
|
||||
}
|
||||
#email-compose-modal .ec-tag button {
|
||||
background: none; border: none; color: rgba(255,255,255,0.8);
|
||||
cursor: pointer; padding: 0; line-height: 1; font-size: 0.9rem;
|
||||
}
|
||||
#email-compose-modal .ec-tag button:hover { color: #fff; }
|
||||
#email-compose-modal .ec-tag-input {
|
||||
border: none; outline: none; background: transparent;
|
||||
font-size: 0.88rem; color: var(--text-primary);
|
||||
min-width: 160px; flex: 1;
|
||||
}
|
||||
#email-compose-modal .ec-editor-wrapper {
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
overflow: hidden;
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
#email-compose-modal .ec-editor-wrapper .ql-toolbar {
|
||||
border: none; border-bottom: 1px solid var(--border-subtle);
|
||||
background: var(--bg-secondary);
|
||||
}
|
||||
#email-compose-modal .ec-editor-wrapper .ql-container {
|
||||
border: none; min-height: 220px; font-size: 0.9rem;
|
||||
}
|
||||
#email-compose-modal .ec-attachments-zone {
|
||||
border: 2px dashed var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 12px 14px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
font-size: 0.82rem; color: var(--text-muted);
|
||||
}
|
||||
#email-compose-modal .ec-attachments-zone:hover,
|
||||
#email-compose-modal .ec-attachments-zone.drag-over {
|
||||
border-color: var(--accent-primary);
|
||||
background: rgba(var(--accent-rgb,99,102,241),0.06);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
#email-compose-modal .ec-file-list {
|
||||
display: flex; flex-direction: column; gap: 4px;
|
||||
}
|
||||
#email-compose-modal .ec-file-item {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
font-size: 0.82rem; padding: 4px 8px;
|
||||
background: var(--bg-secondary); border-radius: var(--radius-sm);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
#email-compose-modal .ec-file-item button {
|
||||
margin-left: auto; background: none; border: none; cursor: pointer;
|
||||
color: var(--text-muted); font-size: 0.85rem; padding: 0 2px;
|
||||
}
|
||||
#email-compose-modal .ec-file-item button:hover { color: var(--error); }
|
||||
#email-compose-modal .ec-footer {
|
||||
padding: 12px 20px;
|
||||
border-top: 1px solid var(--border-subtle);
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
flex-shrink: 0; gap: 10px;
|
||||
}
|
||||
#email-compose-modal select.ec-select {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 6px 12px; font-size: 0.85rem;
|
||||
color: var(--text-primary); min-width: 180px;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// ── Tag Input Helper ──────────────────────────────────────────────────────────
|
||||
function makeTagInput(containerId, initialEmails = []) {
|
||||
const container = document.getElementById(containerId);
|
||||
const tags = [...initialEmails];
|
||||
|
||||
function render() {
|
||||
const inputEl = container.querySelector('.ec-tag-input');
|
||||
const currentVal = inputEl ? inputEl.value : '';
|
||||
container.innerHTML = '';
|
||||
tags.forEach((email, idx) => {
|
||||
const tagEl = document.createElement('span');
|
||||
tagEl.className = 'ec-tag';
|
||||
tagEl.innerHTML = `${App.escapeHtml(email)}<button type="button" data-idx="${idx}">✕</button>`;
|
||||
tagEl.querySelector('button').addEventListener('click', () => {
|
||||
tags.splice(idx, 1);
|
||||
render();
|
||||
});
|
||||
container.appendChild(tagEl);
|
||||
});
|
||||
const input = document.createElement('input');
|
||||
input.className = 'ec-tag-input';
|
||||
input.type = 'text';
|
||||
input.placeholder = tags.length ? '' : 'email@esempio.com, premi Invio';
|
||||
input.value = currentVal;
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if ((e.key === 'Enter' || e.key === ',') && input.value.trim()) {
|
||||
e.preventDefault();
|
||||
const val = input.value.trim().replace(/,$/, '');
|
||||
if (val && !tags.includes(val)) tags.push(val);
|
||||
input.value = '';
|
||||
render();
|
||||
} else if (e.key === 'Backspace' && !input.value && tags.length) {
|
||||
tags.pop();
|
||||
render();
|
||||
}
|
||||
});
|
||||
input.addEventListener('blur', () => {
|
||||
if (input.value.trim()) {
|
||||
const val = input.value.trim().replace(/,$/, '');
|
||||
if (val && !tags.includes(val)) tags.push(val);
|
||||
input.value = '';
|
||||
render();
|
||||
}
|
||||
});
|
||||
container.appendChild(input);
|
||||
container.addEventListener('click', () => input.focus());
|
||||
}
|
||||
|
||||
render();
|
||||
return { getTags: () => [...tags], addTag: (email) => { if (!tags.includes(email)) { tags.push(email); render(); } } };
|
||||
}
|
||||
|
||||
// ── Build Modal HTML ──────────────────────────────────────────────────────────
|
||||
function buildModal() {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'email-compose-overlay';
|
||||
overlay.innerHTML = `
|
||||
<div id="email-compose-modal">
|
||||
<div class="ec-header">
|
||||
<div class="ec-title">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:18px;height:18px;">
|
||||
<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>
|
||||
Invia Email
|
||||
</div>
|
||||
<button id="ec-close" style="background:none;border:none;cursor:pointer;color:var(--text-muted);padding:4px;font-size:1.2rem;" title="Chiudi">✕</button>
|
||||
</div>
|
||||
<div class="ec-body">
|
||||
<div class="ec-field">
|
||||
<label class="ec-label">A (To)</label>
|
||||
<div class="ec-tags-input" id="ec-to-container"></div>
|
||||
</div>
|
||||
<div class="ec-field">
|
||||
<label class="ec-label">CC</label>
|
||||
<div class="ec-tags-input" id="ec-cc-container"></div>
|
||||
</div>
|
||||
<div class="ec-field">
|
||||
<label class="ec-label">Oggetto</label>
|
||||
<input type="text" id="ec-subject" class="form-input" style="margin-bottom:0;" placeholder="Oggetto email" />
|
||||
</div>
|
||||
<div style="display:flex; gap:16px;">
|
||||
<div class="ec-field" style="flex:1;">
|
||||
<label class="ec-label">Firma</label>
|
||||
<select id="ec-signature-select" class="ec-select" style="width:100%; min-width:unset;">
|
||||
<option value="">— Nessuna firma —</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="ec-field" style="flex:1;">
|
||||
<label class="ec-label">Tieni helpdesk in copia</label>
|
||||
<select id="ec-helpdesk-cc-select" class="ec-select" style="width:100%; min-width:unset;">
|
||||
<option value="1">Sì (BCC automatico)</option>
|
||||
<option value="0">No</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ec-field">
|
||||
<label class="ec-label">Corpo</label>
|
||||
<div class="ec-editor-wrapper">
|
||||
<div id="ec-quill-editor"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ec-field">
|
||||
<label class="ec-label">Allegati</label>
|
||||
<div class="ec-attachments-zone" id="ec-drop-zone">
|
||||
📎 Trascina file qui o clicca per selezionare
|
||||
</div>
|
||||
<input type="file" id="ec-file-input" multiple style="display:none;" />
|
||||
<div class="ec-file-list" id="ec-file-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ec-footer">
|
||||
<button class="btn btn-ghost btn-sm" id="ec-cancel">Annulla</button>
|
||||
<button class="btn btn-primary btn-sm" id="ec-send" style="display:flex;align-items:center;gap:6px;">
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
return overlay;
|
||||
}
|
||||
|
||||
// ── File List Renderer ────────────────────────────────────────────────────────
|
||||
function renderFileList() {
|
||||
const list = document.getElementById('ec-file-list');
|
||||
if (!list) return;
|
||||
list.innerHTML = attachmentsList.map((f, idx) => `
|
||||
<div class="ec-file-item">
|
||||
📎 <strong>${App.escapeHtml(f.filename)}</strong>
|
||||
<span style="color:var(--text-muted);font-size:0.75rem;">(${Math.round(f.content.length * 0.75 / 1024)} KB)</span>
|
||||
<button data-idx="${idx}" title="Rimuovi">✕</button>
|
||||
</div>
|
||||
`).join('');
|
||||
list.querySelectorAll('button[data-idx]').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
attachmentsList.splice(parseInt(btn.dataset.idx, 10), 1);
|
||||
renderFileList();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Load Signatures ───────────────────────────────────────────────────────────
|
||||
async function loadSignatures(agentId, selectEl) {
|
||||
try {
|
||||
const sigs = await App.api(`/api/email/signatures?agent_id=${agentId}`);
|
||||
selectEl.innerHTML = '<option value="">— Nessuna firma —</option>';
|
||||
sigs.forEach(sig => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = sig.id;
|
||||
opt.textContent = sig.name + (sig.is_default ? ' ★' : '');
|
||||
opt.dataset.html = sig.body_html;
|
||||
selectEl.appendChild(opt);
|
||||
});
|
||||
// Pre-select default
|
||||
const defSig = sigs.find(s => s.is_default);
|
||||
if (defSig) {
|
||||
selectEl.value = defSig.id;
|
||||
return defSig.body_html;
|
||||
}
|
||||
} catch (e) { console.warn('[EmailCompose] Signatures load error:', e); }
|
||||
return '';
|
||||
}
|
||||
|
||||
// ── Open ─────────────────────────────────────────────────────────────────────
|
||||
async function open(options = {}) {
|
||||
injectStyles();
|
||||
attachmentsList = [];
|
||||
currentOptions = options;
|
||||
|
||||
// Remove existing
|
||||
const existing = document.getElementById('email-compose-overlay');
|
||||
if (existing) existing.remove();
|
||||
|
||||
const overlay = buildModal();
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Init tag inputs
|
||||
const initialTo = options.customerEmail ? [options.customerEmail] : [];
|
||||
const toTagsCtrl = makeTagInput('ec-to-container', initialTo);
|
||||
const ccTagsCtrl = makeTagInput('ec-cc-container', []);
|
||||
|
||||
// Subject
|
||||
const subjectEl = document.getElementById('ec-subject');
|
||||
const tn = options.ticketTn || '';
|
||||
const title = options.ticketTitle || '';
|
||||
subjectEl.value = tn ? `Re: [Ticket#${tn}] ${title}` : title;
|
||||
|
||||
// Signature select
|
||||
const sigSelect = document.getElementById('ec-signature-select');
|
||||
const agentId = App.currentAgentId || 0;
|
||||
const defaultSigHtml = await loadSignatures(agentId, sigSelect);
|
||||
|
||||
// Quill editor
|
||||
quillEditor = new Quill('#ec-quill-editor', {
|
||||
theme: 'snow',
|
||||
placeholder: 'Scrivi il testo della email...',
|
||||
modules: {
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline', 'strike'],
|
||||
[{ 'header': [1, 2, 3, false] }],
|
||||
[{ 'list': 'ordered' }, { 'list': 'bullet' }],
|
||||
['link', 'image'],
|
||||
[{ 'color': [] }, { 'background': [] }],
|
||||
['clean']
|
||||
],
|
||||
}
|
||||
});
|
||||
|
||||
// Insert initial body and signature
|
||||
let initialHtml = '';
|
||||
if (options.initialBodyHtml) {
|
||||
initialHtml += options.initialBodyHtml;
|
||||
} else {
|
||||
initialHtml += '<p><br></p>';
|
||||
}
|
||||
if (defaultSigHtml) {
|
||||
initialHtml += '<!-- sig -->' + defaultSigHtml;
|
||||
}
|
||||
quillEditor.clipboard.dangerouslyPasteHTML(initialHtml);
|
||||
quillEditor.setSelection(0, 0);
|
||||
|
||||
// Signature change
|
||||
sigSelect.addEventListener('change', () => {
|
||||
const selectedOpt = sigSelect.options[sigSelect.selectedIndex];
|
||||
const sigHtml = selectedOpt ? (selectedOpt.dataset.html || '') : '';
|
||||
// Replace signature: get current body, strip old signature (after first <br>), append new
|
||||
const currentHtml = quillEditor.root.innerHTML;
|
||||
const sigMarker = '<!-- sig -->';
|
||||
const baseHtml = currentHtml.includes(sigMarker)
|
||||
? currentHtml.split(sigMarker)[0]
|
||||
: currentHtml;
|
||||
const newHtml = baseHtml + (sigHtml ? sigMarker + sigHtml : '');
|
||||
quillEditor.clipboard.dangerouslyPasteHTML(newHtml);
|
||||
});
|
||||
|
||||
// File drag & drop
|
||||
const dropZone = document.getElementById('ec-drop-zone');
|
||||
const fileInput = document.getElementById('ec-file-input');
|
||||
|
||||
dropZone.addEventListener('click', () => fileInput.click());
|
||||
dropZone.addEventListener('dragover', (e) => { e.preventDefault(); dropZone.classList.add('drag-over'); });
|
||||
dropZone.addEventListener('dragleave', () => dropZone.classList.remove('drag-over'));
|
||||
dropZone.addEventListener('drop', (e) => {
|
||||
e.preventDefault(); dropZone.classList.remove('drag-over');
|
||||
processFiles(Array.from(e.dataTransfer.files));
|
||||
});
|
||||
fileInput.addEventListener('change', (e) => {
|
||||
processFiles(Array.from(e.target.files));
|
||||
fileInput.value = '';
|
||||
});
|
||||
|
||||
// Close handlers
|
||||
document.getElementById('ec-close').addEventListener('click', close);
|
||||
document.getElementById('ec-cancel').addEventListener('click', close);
|
||||
overlay.addEventListener('click', (e) => { if (e.target === overlay) close(); });
|
||||
|
||||
// Send
|
||||
document.getElementById('ec-send').addEventListener('click', () => sendEmail(toTagsCtrl, ccTagsCtrl));
|
||||
}
|
||||
|
||||
function processFiles(files) {
|
||||
for (const file of files) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const base64 = reader.result.split(',')[1];
|
||||
attachmentsList.push({ filename: file.name, content: base64, contentType: file.type });
|
||||
renderFileList();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Send ─────────────────────────────────────────────────────────────────────
|
||||
async function sendEmail(toCtrl, ccCtrl) {
|
||||
const to = toCtrl.getTags();
|
||||
const cc = ccCtrl.getTags();
|
||||
const subject = document.getElementById('ec-subject').value.trim();
|
||||
const bodyHtml = quillEditor ? quillEditor.root.innerHTML : '';
|
||||
|
||||
if (!to.length) { Toast.warning('Inserisci almeno un destinatario (campo A)'); return; }
|
||||
if (!subject) { Toast.warning('Inserisci l\'oggetto della email'); return; }
|
||||
|
||||
const sendBtn = document.getElementById('ec-send');
|
||||
sendBtn.disabled = true;
|
||||
sendBtn.innerHTML = '<div class="spinner" style="width:14px;height:14px;border-width:2px;"></div> Invio...';
|
||||
|
||||
try {
|
||||
const agentId = App.currentAgentId || 0;
|
||||
const payload = {
|
||||
ticketId: currentOptions.ticketId,
|
||||
to, cc, subject, bodyHtml,
|
||||
attachments: attachmentsList,
|
||||
agentId,
|
||||
keepHelpdeskCopy: document.getElementById('ec-helpdesk-cc-select').value === '1',
|
||||
};
|
||||
|
||||
const res = await App.api('/api/email/send', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
Toast.success(`Email inviata a ${to.join(', ')}`);
|
||||
close();
|
||||
} catch (err) {
|
||||
Toast.error('Errore invio email: ' + err.message);
|
||||
sendBtn.disabled = false;
|
||||
sendBtn.innerHTML = '<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';
|
||||
}
|
||||
}
|
||||
|
||||
// ── Close ─────────────────────────────────────────────────────────────────────
|
||||
function close() {
|
||||
const overlay = document.getElementById('email-compose-overlay');
|
||||
if (overlay) overlay.remove();
|
||||
if (quillEditor) { quillEditor = null; }
|
||||
attachmentsList = [];
|
||||
}
|
||||
|
||||
return { open, close };
|
||||
})();
|
||||
window.EmailCompose = EmailCompose;
|
||||
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* signatures.js
|
||||
* Pagina di gestione delle firme email per agente.
|
||||
* Usa Quill.js come editor WYSIWYG.
|
||||
*/
|
||||
|
||||
const SignaturesView = {
|
||||
agentId: null,
|
||||
signatures: [],
|
||||
editingId: null,
|
||||
signatureQuill: null,
|
||||
|
||||
async render() {
|
||||
this.agentId = App.currentAgentId || 0;
|
||||
const container = document.getElementById('view-container');
|
||||
container.innerHTML = `
|
||||
<div style="max-width: 820px; margin: 0 auto; padding: var(--space-xl) var(--space-lg);">
|
||||
<div style="display:flex; align-items:center; justify-content:space-between; margin-bottom: var(--space-xl);">
|
||||
<div>
|
||||
<h2 style="margin:0; font-size:1.25rem; font-weight:700; color:var(--text-primary);">✉️ Le Mie Firme Email</h2>
|
||||
<p style="margin:4px 0 0; font-size:0.85rem; color:var(--text-muted);">Gestisci le firme da allegare automaticamente alle email inviate dal ticket.</p>
|
||||
</div>
|
||||
<button class="btn btn-primary btn-sm" id="btn-new-signature">
|
||||
+ Nuova Firma
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Lista firme -->
|
||||
<div id="signatures-list" style="display:flex; flex-direction:column; gap:var(--space-md);"></div>
|
||||
|
||||
<!-- Modal Editor -->
|
||||
<div id="signature-editor-modal" style="display:none; position:fixed; inset:0; z-index:8000; background:rgba(0,0,0,0.5); backdrop-filter:blur(4px); align-items:center; justify-content:center;">
|
||||
<div style="background:var(--bg-card); border:1px solid var(--border-light); border-radius:var(--radius-xl); box-shadow:0 24px 80px rgba(0,0,0,0.3); width:min(720px,94vw); max-height:90vh; display:flex; flex-direction:column;">
|
||||
<div style="padding:16px 20px 12px; border-bottom:1px solid var(--border-subtle); display:flex; align-items:center; justify-content:space-between; flex-shrink:0;">
|
||||
<div style="font-size:1rem; font-weight:600; color:var(--text-primary);" id="sig-modal-title">Nuova Firma</div>
|
||||
<button id="sig-modal-close" style="background:none;border:none;cursor:pointer;color:var(--text-muted);font-size:1.2rem;">✕</button>
|
||||
</div>
|
||||
<div style="padding:16px 20px; flex:1; overflow-y:auto; display:flex; flex-direction:column; gap:12px;">
|
||||
<div>
|
||||
<label style="font-size:0.75rem; font-weight:600; color:var(--text-secondary); text-transform:uppercase; letter-spacing:0.04em;">Nome Firma</label>
|
||||
<input type="text" id="sig-name" class="form-input" placeholder="es. Firma Professionale" style="margin-bottom:0; margin-top:4px;" />
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:0.75rem; font-weight:600; color:var(--text-secondary); text-transform:uppercase; letter-spacing:0.04em;">Contenuto</label>
|
||||
<div style="margin-top:4px; border:1px solid var(--border-subtle); border-radius:var(--radius-md); overflow:hidden; background:var(--bg-tertiary);">
|
||||
<div id="sig-quill-editor" style="min-height:200px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<label style="display:flex; align-items:center; gap:8px; font-size:0.88rem; color:var(--text-secondary); cursor:pointer;">
|
||||
<input type="checkbox" id="sig-is-default" />
|
||||
Imposta come firma predefinita
|
||||
</label>
|
||||
</div>
|
||||
<div style="padding:12px 20px; border-top:1px solid var(--border-subtle); display:flex; justify-content:flex-end; gap:10px; flex-shrink:0;">
|
||||
<button class="btn btn-ghost btn-sm" id="sig-cancel">Annulla</button>
|
||||
<button class="btn btn-primary btn-sm" id="sig-save">Salva Firma</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('btn-new-signature').addEventListener('click', () => this.openEditor(null));
|
||||
|
||||
await this.loadSignatures();
|
||||
},
|
||||
|
||||
async loadSignatures() {
|
||||
try {
|
||||
this.signatures = await App.api(`/api/email/signatures?agent_id=${this.agentId}`);
|
||||
this.renderList();
|
||||
} catch (err) {
|
||||
Toast.error('Errore caricamento firme: ' + err.message);
|
||||
}
|
||||
},
|
||||
|
||||
renderList() {
|
||||
const list = document.getElementById('signatures-list');
|
||||
if (!list) return;
|
||||
|
||||
if (!this.signatures.length) {
|
||||
list.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">✉️</div>
|
||||
<div class="empty-state-text">Nessuna firma configurata</div>
|
||||
<div class="empty-state-sub">Crea la tua prima firma per velocizzare l'invio delle email.</div>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
list.innerHTML = this.signatures.map(sig => `
|
||||
<div class="card" style="padding: var(--space-lg);">
|
||||
<div style="display:flex; align-items:flex-start; justify-content:space-between; gap:12px; margin-bottom:${sig.body_html ? 'var(--space-md)' : '0'};">
|
||||
<div>
|
||||
<div style="font-weight:600; font-size:0.95rem; color:var(--text-primary); display:flex; align-items:center; gap:8px;">
|
||||
${App.escapeHtml(sig.name)}
|
||||
${sig.is_default ? '<span style="font-size:0.72rem; background:var(--accent-primary); color:#fff; padding:2px 7px; border-radius:20px;">Predefinita</span>' : ''}
|
||||
</div>
|
||||
<div style="font-size:0.72rem; color:var(--text-muted); margin-top:2px;">
|
||||
Creata: ${new Date(sig.created_at).toLocaleDateString('it-IT')}
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex; gap:6px; flex-shrink:0;">
|
||||
${!sig.is_default ? `<button class="btn btn-ghost btn-sm sig-btn-default" data-id="${sig.id}" style="height:30px; font-size:0.78rem;">★ Predefinita</button>` : ''}
|
||||
<button class="btn btn-ghost btn-sm sig-btn-edit" data-id="${sig.id}" style="height:30px; font-size:0.78rem;">✏️ Modifica</button>
|
||||
<button class="btn btn-ghost btn-sm sig-btn-delete" data-id="${sig.id}" style="height:30px; font-size:0.78rem; color:var(--error);">🗑️</button>
|
||||
</div>
|
||||
</div>
|
||||
${sig.body_html ? `
|
||||
<div style="border:1px solid var(--border-subtle); border-radius:var(--radius-md); padding:10px 14px; background:var(--bg-secondary); max-height:120px; overflow:hidden; position:relative;">
|
||||
<div style="font-size:0.82rem; color:var(--text-secondary);">${sig.body_html}</div>
|
||||
<div style="position:absolute;bottom:0;left:0;right:0;height:40px;background:linear-gradient(transparent,var(--bg-secondary));"></div>
|
||||
</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Bind buttons
|
||||
list.querySelectorAll('.sig-btn-edit').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.openEditor(parseInt(btn.dataset.id, 10)));
|
||||
});
|
||||
list.querySelectorAll('.sig-btn-delete').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.deleteSig(parseInt(btn.dataset.id, 10)));
|
||||
});
|
||||
list.querySelectorAll('.sig-btn-default').forEach(btn => {
|
||||
btn.addEventListener('click', () => this.setDefault(parseInt(btn.dataset.id, 10)));
|
||||
});
|
||||
},
|
||||
|
||||
openEditor(id) {
|
||||
this.editingId = id;
|
||||
const sig = id ? this.signatures.find(s => s.id === id) : null;
|
||||
|
||||
const modal = document.getElementById('signature-editor-modal');
|
||||
modal.style.display = 'flex';
|
||||
|
||||
document.getElementById('sig-modal-title').textContent = id ? 'Modifica Firma' : 'Nuova Firma';
|
||||
document.getElementById('sig-name').value = sig ? sig.name : '';
|
||||
document.getElementById('sig-is-default').checked = sig ? !!sig.is_default : false;
|
||||
|
||||
// Init or reset Quill
|
||||
if (this.signatureQuill) {
|
||||
this.signatureQuill.root.innerHTML = sig ? (sig.body_html || '') : '';
|
||||
} else {
|
||||
this.signatureQuill = new Quill('#sig-quill-editor', {
|
||||
theme: 'snow',
|
||||
placeholder: 'Inserisci la tua firma...',
|
||||
modules: {
|
||||
toolbar: [
|
||||
['bold', 'italic', 'underline'],
|
||||
[{ 'color': [] }],
|
||||
['link', 'image'],
|
||||
['clean']
|
||||
]
|
||||
}
|
||||
});
|
||||
if (sig && sig.body_html) {
|
||||
this.signatureQuill.clipboard.dangerouslyPasteHTML(sig.body_html);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('sig-modal-close').onclick = () => this.closeEditor();
|
||||
document.getElementById('sig-cancel').onclick = () => this.closeEditor();
|
||||
document.getElementById('sig-save').onclick = () => this.saveSig();
|
||||
|
||||
modal.onclick = (e) => { if (e.target === modal) this.closeEditor(); };
|
||||
},
|
||||
|
||||
closeEditor() {
|
||||
const modal = document.getElementById('signature-editor-modal');
|
||||
if (modal) modal.style.display = 'none';
|
||||
this.editingId = null;
|
||||
},
|
||||
|
||||
async saveSig() {
|
||||
const name = document.getElementById('sig-name').value.trim();
|
||||
const body_html = this.signatureQuill ? this.signatureQuill.root.innerHTML : '';
|
||||
const is_default = document.getElementById('sig-is-default').checked ? 1 : 0;
|
||||
|
||||
if (!name) { Toast.warning('Inserisci un nome per la firma'); return; }
|
||||
|
||||
const saveBtn = document.getElementById('sig-save');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = 'Salvataggio...';
|
||||
|
||||
try {
|
||||
const payload = { agent_id: this.agentId, name, body_html, is_default };
|
||||
|
||||
if (this.editingId) {
|
||||
await App.api(`/api/email/signatures/${this.editingId}`, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
Toast.success('Firma aggiornata');
|
||||
} else {
|
||||
await App.api('/api/email/signatures', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
Toast.success('Firma creata');
|
||||
}
|
||||
|
||||
this.closeEditor();
|
||||
await this.loadSignatures();
|
||||
} catch (err) {
|
||||
Toast.error('Errore salvataggio: ' + err.message);
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = 'Salva Firma';
|
||||
}
|
||||
},
|
||||
|
||||
async deleteSig(id) {
|
||||
if (!confirm('Eliminare questa firma?')) return;
|
||||
try {
|
||||
await App.api(`/api/email/signatures/${id}`, { method: 'DELETE' });
|
||||
Toast.success('Firma eliminata');
|
||||
await this.loadSignatures();
|
||||
} catch (err) {
|
||||
Toast.error('Errore eliminazione: ' + err.message);
|
||||
}
|
||||
},
|
||||
|
||||
async setDefault(id) {
|
||||
try {
|
||||
await App.api(`/api/email/signatures/${id}/default`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ agent_id: this.agentId }),
|
||||
});
|
||||
Toast.success('Firma impostata come predefinita');
|
||||
await this.loadSignatures();
|
||||
} catch (err) {
|
||||
Toast.error('Errore: ' + err.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
window.SignaturesView = SignaturesView;
|
||||
@@ -52,10 +52,14 @@ const TicketDetailView = {
|
||||
return sum + (isNaN(val) ? 0 : val);
|
||||
}, 0);
|
||||
|
||||
// Display ticket number in the topbar
|
||||
// Display ticket number in the topbar with OTRS link if available
|
||||
const titleEl = document.getElementById('page-title');
|
||||
if (titleEl) {
|
||||
titleEl.innerHTML = `Ticket #${id}`;
|
||||
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 = {
|
||||
@@ -184,6 +188,11 @@ const TicketDetailView = {
|
||||
</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>
|
||||
Invia Email
|
||||
</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
|
||||
@@ -219,6 +228,7 @@ const TicketDetailView = {
|
||||
<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;">
|
||||
@@ -409,7 +419,7 @@ const TicketDetailView = {
|
||||
this.noteQuill = null;
|
||||
}
|
||||
|
||||
this.bindEvents();
|
||||
this.bindEvents(ticket, articles, container);
|
||||
|
||||
} catch (err) {
|
||||
container.innerHTML = `
|
||||
@@ -423,7 +433,7 @@ const TicketDetailView = {
|
||||
}
|
||||
},
|
||||
|
||||
bindEvents() {
|
||||
bindEvents(ticket, articles, container) {
|
||||
// Quick-edit change detection
|
||||
const fields = document.querySelectorAll('.quick-edit-select, #qe-customer-user-id, #qe-customer-id');
|
||||
const saveBtn = document.getElementById('qe-save');
|
||||
@@ -670,6 +680,63 @@ const TicketDetailView = {
|
||||
});
|
||||
}
|
||||
|
||||
// Email Compose Button
|
||||
const btnEmailCompose = document.getElementById('btn-open-email-compose');
|
||||
if (btnEmailCompose) {
|
||||
btnEmailCompose.addEventListener('click', () => {
|
||||
if (window.EmailCompose) {
|
||||
EmailCompose.open({
|
||||
ticketId: parseInt(btnEmailCompose.dataset.ticketId, 10),
|
||||
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>
|
||||
`;
|
||||
|
||||
// Extract sender email if possible for CC/To
|
||||
let customerEmail = ticket.customer_email || '';
|
||||
const matchEmail = (article.a_from || '').match(/<([^>]+)>/) || (article.a_from || '').match(/([a-zA-Z0-9._-]+@[a-zA-Z0-9._-]+\.[a-zA-Z0-9._-]+)/);
|
||||
if (matchEmail) {
|
||||
customerEmail = matchEmail[1];
|
||||
}
|
||||
|
||||
EmailCompose.open({
|
||||
ticketId: ticket.id,
|
||||
ticketTn: ticket.tn,
|
||||
ticketTitle: ticket.title,
|
||||
customerEmail: customerEmail,
|
||||
initialBodyHtml: initialBodyHtml,
|
||||
});
|
||||
} 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');
|
||||
|
||||
Reference in New Issue
Block a user