aggiunta gestione html/testo e allegati
This commit is contained in:
@@ -1930,3 +1930,57 @@ body {
|
|||||||
.retrodate-btn-action.cancel {
|
.retrodate-btn-action.cancel {
|
||||||
color: var(--error);
|
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 = {
|
const TicketDetailView = {
|
||||||
ticketId: null,
|
ticketId: null,
|
||||||
|
htmlMode: false,
|
||||||
originalValues: {},
|
originalValues: {},
|
||||||
|
|
||||||
async render(id) {
|
async render(id) {
|
||||||
this.ticketId = id;
|
this.ticketId = id;
|
||||||
|
this.htmlMode = localStorage.getItem('otrs_turbo_html_mode') === 'true';
|
||||||
const container = document.getElementById('view-container');
|
const container = document.getElementById('view-container');
|
||||||
container.innerHTML = '<div class="loading-screen"><div class="spinner"></div><p>Caricamento ticket...</p></div>';
|
container.innerHTML = '<div class="loading-screen"><div class="spinner"></div><p>Caricamento ticket...</p></div>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await App.ensureLookups();
|
await App.ensureLookups();
|
||||||
const data = await App.api(`/api/tickets/${id}`);
|
const data = await App.api(`/api/tickets/${id}`);
|
||||||
const { ticket, articles } = data;
|
const { ticket, articles, attachments } = data;
|
||||||
const localISO = (dateStr) => {
|
const localISO = (dateStr) => {
|
||||||
if (!dateStr) return '';
|
if (!dateStr) return '';
|
||||||
const d = new Date(dateStr);
|
const d = new Date(dateStr);
|
||||||
@@ -130,37 +132,64 @@ const TicketDetailView = {
|
|||||||
|
|
||||||
<!-- Articles Timeline -->
|
<!-- Articles Timeline -->
|
||||||
<div>
|
<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">
|
<div class="articles-timeline">
|
||||||
${articles.length > 0 ? articles.map(a => `
|
${articles.length > 0 ? articles.map(a => {
|
||||||
<div class="article-card sender-${(a.sender_type || 'system').toLowerCase()}">
|
const hasHtml = (a.a_content_type || '').toLowerCase().includes('html') || a.a_body.includes('</') || a.a_body.includes('/>');
|
||||||
<div class="article-header">
|
const displayBody = this.htmlMode && hasHtml
|
||||||
<div class="article-sender">
|
? `<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>`
|
||||||
<span class="article-sender-badge ${(a.sender_type || 'system').toLowerCase()}">${a.sender_type || 'System'}</span>
|
: `<div class="article-body">${App.escapeHtml(a.a_body || '')}</div>`;
|
||||||
<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}">
|
const articleAttachments = (attachments || []).filter(att => att.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>
|
return `
|
||||||
<div class="retrodate-editor" style="display:none;">
|
<div class="article-card sender-${(a.sender_type || 'system').toLowerCase()}">
|
||||||
<input type="datetime-local" class="retrodate-input input-article-date" value="${localISO(a.create_time)}" />
|
<div class="article-header">
|
||||||
<div class="retrodate-actions">
|
<div class="article-sender">
|
||||||
<button class="retrodate-btn-action save btn-save-article-date" title="Salva">✓</button>
|
<span class="article-sender-badge ${(a.sender_type || 'system').toLowerCase()}">${a.sender_type || 'System'}</span>
|
||||||
<button class="retrodate-btn-action cancel btn-cancel-article-date" title="Annulla">✗</button>
|
<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>
|
||||||
</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>
|
</div>
|
||||||
${a.a_subject ? `<div class="article-subject">${App.escapeHtml(a.a_subject)}</div>` : ''}
|
`;
|
||||||
<div class="article-body">${App.escapeHtml(a.a_body || '')}</div>
|
}).join('') : `
|
||||||
</div>
|
|
||||||
`).join('') : `
|
|
||||||
<div class="empty-state" style="padding:var(--space-lg);">
|
<div class="empty-state" style="padding:var(--space-lg);">
|
||||||
<div class="empty-state-icon">💬</div>
|
<div class="empty-state-icon">💬</div>
|
||||||
<div class="empty-state-text">Nessun articolo</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 users creator ON a.create_by = creator.id
|
||||||
LEFT JOIN time_accounting ta ON a.id = ta.article_id
|
LEFT JOIN time_accounting ta ON a.id = ta.article_id
|
||||||
WHERE a.ticket_id = $1
|
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]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
ticket: ticketResult.rows[0],
|
ticket: ticketResult.rows[0],
|
||||||
articles: articlesResult.rows,
|
articles: articlesResult.rows,
|
||||||
|
attachments: attachmentsResult.rows,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching ticket detail:', 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 users creator ON a.create_by = creator.id
|
||||||
LEFT JOIN time_accounting ta ON a.id = ta.article_id
|
LEFT JOIN time_accounting ta ON a.id = ta.article_id
|
||||||
WHERE a.ticket_id = $1
|
WHERE a.ticket_id = $1
|
||||||
ORDER BY a.create_time ASC`,
|
ORDER BY a.create_time DESC, a.id DESC`,
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
res.json(result.rows);
|
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;
|
module.exports = router;
|
||||||
|
|||||||
Reference in New Issue
Block a user