aggiunta gestione html/testo e allegati
This commit is contained in:
@@ -1930,3 +1930,57 @@ body {
|
||||
.retrodate-btn-action.cancel {
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
/* HTML switch & attachments styles */
|
||||
.article-header-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: var(--space-md);
|
||||
}
|
||||
|
||||
.html-toggle-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
font-size: 0.82rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.html-toggle-input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.article-attachments {
|
||||
margin-top: var(--space-md);
|
||||
padding-top: var(--space-sm);
|
||||
border-top: 1px dashed var(--border-subtle);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.attachment-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-light);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 4px 10px;
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-primary);
|
||||
text-decoration: none;
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.attachment-badge:hover {
|
||||
background: var(--bg-card-hover);
|
||||
border-color: var(--accent-primary);
|
||||
color: var(--accent-primary-hover);
|
||||
}
|
||||
|
||||
.attachment-size {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
@@ -4,17 +4,19 @@
|
||||
*/
|
||||
const TicketDetailView = {
|
||||
ticketId: null,
|
||||
htmlMode: false,
|
||||
originalValues: {},
|
||||
|
||||
async render(id) {
|
||||
this.ticketId = id;
|
||||
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 } = data;
|
||||
const { ticket, articles, attachments } = data;
|
||||
const localISO = (dateStr) => {
|
||||
if (!dateStr) return '';
|
||||
const d = new Date(dateStr);
|
||||
@@ -130,37 +132,64 @@ const TicketDetailView = {
|
||||
|
||||
<!-- Articles Timeline -->
|
||||
<div>
|
||||
<div class="card-title" style="margin-bottom:var(--space-md);">Articoli & Note (${articles.length})</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 => `
|
||||
<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>` : ''}
|
||||
</div>
|
||||
<div style="display:flex; gap: var(--space-sm); align-items:center;">
|
||||
${a.time_unit ? `<span class="badge" style="background:var(--info-bg);color:var(--info);font-size:0.75rem;padding:2px 8px;border-radius:4px;">⏱ ${parseFloat(a.time_unit)} min</span>` : ''}
|
||||
${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>`;
|
||||
|
||||
<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>
|
||||
const articleAttachments = (attachments || []).filter(att => att.article_id === a.article_id);
|
||||
|
||||
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>` : ''}
|
||||
</div>
|
||||
<div style="display:flex; gap: var(--space-sm); align-items:center;">
|
||||
${a.time_unit ? `<span class="badge" style="background:var(--info-bg);color:var(--info);font-size:0.75rem;padding:2px 8px;border-radius:4px;">⏱ ${parseFloat(a.time_unit)} min</span>` : ''}
|
||||
|
||||
<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>
|
||||
${a.a_subject ? `<div class="article-subject">${App.escapeHtml(a.a_subject)}</div>` : ''}
|
||||
<div class="article-body">${App.escapeHtml(a.a_body || '')}</div>
|
||||
</div>
|
||||
`).join('') : `
|
||||
`;
|
||||
}).join('') : `
|
||||
<div class="empty-state" style="padding:var(--space-lg);">
|
||||
<div class="empty-state-icon">💬</div>
|
||||
<div class="empty-state-text">Nessun articolo</div>
|
||||
@@ -457,5 +486,15 @@ const TicketDetailView = {
|
||||
});
|
||||
}
|
||||
});
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
+37
-2
@@ -210,13 +210,24 @@ router.get('/:id', async (req, res) => {
|
||||
LEFT JOIN users creator ON a.create_by = creator.id
|
||||
LEFT JOIN time_accounting ta ON a.id = ta.article_id
|
||||
WHERE a.ticket_id = $1
|
||||
ORDER BY a.create_time ASC`,
|
||||
ORDER BY a.create_time DESC, a.id DESC`,
|
||||
[id]
|
||||
);
|
||||
|
||||
// Fetch attachments metadata for all articles in the ticket
|
||||
const attachmentsResult = await pool.query(
|
||||
`SELECT id, article_id, filename, content_size, content_type, disposition
|
||||
FROM article_data_mime_attachment
|
||||
WHERE article_id IN (
|
||||
SELECT id FROM article WHERE ticket_id = $1
|
||||
)`,
|
||||
[id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
ticket: ticketResult.rows[0],
|
||||
articles: articlesResult.rows,
|
||||
attachments: attachmentsResult.rows,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error fetching ticket detail:', err);
|
||||
@@ -578,7 +589,7 @@ router.get('/:id/articles', async (req, res) => {
|
||||
LEFT JOIN users creator ON a.create_by = creator.id
|
||||
LEFT JOIN time_accounting ta ON a.id = ta.article_id
|
||||
WHERE a.ticket_id = $1
|
||||
ORDER BY a.create_time ASC`,
|
||||
ORDER BY a.create_time DESC, a.id DESC`,
|
||||
[id]
|
||||
);
|
||||
res.json(result.rows);
|
||||
@@ -917,4 +928,28 @@ router.post('/articles/:articleId/retrodata-article', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/tickets/attachments/:id — Download/View attachment
|
||||
router.get('/attachments/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const result = await pool.query(
|
||||
`SELECT filename, content_type, content
|
||||
FROM article_data_mime_attachment
|
||||
WHERE id = $1`,
|
||||
[id]
|
||||
);
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).send('Allegato non trovato.');
|
||||
}
|
||||
const attachment = result.rows[0];
|
||||
|
||||
res.setHeader('Content-Type', attachment.content_type || 'application/octet-stream');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${attachment.filename}"`);
|
||||
res.send(attachment.content);
|
||||
} catch (err) {
|
||||
console.error('Errore download allegato:', err);
|
||||
res.status(500).send(err.message);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
Reference in New Issue
Block a user