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

214 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* ActivityLogView — Storico Attività
* Displays the local SQLite activity log with filters, dual pagination,
* and an expandable JSON detail panel.
*/
const ActivityLogView = {
currentPage: 1,
perPage: 50,
filters: { esito: '', agente_id: '', da: '', a: '' },
async render() {
this.currentPage = 1;
const container = document.getElementById('view-container');
container.innerHTML = '<div class="loading-screen"><div class="spinner"></div><p>Caricamento storico...</p></div>';
await this._draw();
},
async _draw() {
const container = document.getElementById('view-container');
try {
const params = new URLSearchParams({
page: this.currentPage,
per_page: this.perPage,
});
if (this.filters.esito) params.set('esito', this.filters.esito);
if (this.filters.agente_id) params.set('agente_id', this.filters.agente_id);
if (this.filters.da) params.set('da', this.filters.da);
if (this.filters.a) params.set('a', this.filters.a);
const data = await App.api(`/api/attivita?${params}`);
const { rows, total, page, per_page, total_pages } = data;
container.innerHTML = this._buildHtml(rows, total, page, per_page, total_pages);
this._bind();
} catch (err) {
container.innerHTML = `<div class="empty-state"><p style="color:var(--danger)">Errore caricamento: ${App.escapeHtml(err.message)}</p></div>`;
}
},
_buildHtml(rows, total, page, per_page, total_pages) {
const filtersHtml = this._buildFilters();
const paginationHtml = this._buildPagination(total, page, per_page, total_pages);
const tableHtml = rows.length === 0
? `<div class="empty-state" style="padding:var(--space-2xl);text-align:center;color:var(--text-muted);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" style="width:48px;height:48px;margin:0 auto var(--space-md);display:block;opacity:0.4;"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
<p>Nessuna attività registrata.</p>
</div>`
: `<div class="table-wrapper" style="overflow-x:auto;">
<table class="tickets-table" id="activity-table" style="width:100%;">
<thead>
<tr>
<th style="width:160px;">Data/Ora</th>
<th style="width:160px;">Agente</th>
<th style="width:180px;">Azione</th>
<th style="min-width:60px;text-align:center;">Esito</th>
<th style="width:48px;text-align:center;">≡</th>
</tr>
</thead>
<tbody>
${rows.map(r => this._buildRow(r)).join('')}
</tbody>
</table>
</div>`;
return `
<div class="view-header" style="padding:var(--space-md) var(--space-xl);display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:var(--space-sm);border-bottom:1px solid var(--border-subtle);">
<div style="display:flex;align-items:center;gap:var(--space-sm);">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="width:20px;height:20px;color:var(--primary);"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
<span style="font-weight:600;font-size:1rem;">Storico Attività</span>
<span class="badge" style="background:var(--bg-tertiary);color:var(--text-secondary);font-size:0.75rem;padding:2px 8px;border-radius:12px;">${total} record</span>
</div>
</div>
${filtersHtml}
${paginationHtml}
${tableHtml}
${rows.length > 0 ? paginationHtml.replace(/id="pagination-top"/g,'id="pagination-bottom"') : ''}
`;
},
_buildRow(r) {
const dt = r.creato_il ? new Date(r.creato_il).toLocaleString('it-IT') : '—';
const esitoBadge = r.esito === 'successo'
? `<span style="display:inline-block;padding:2px 10px;border-radius:12px;background:rgba(34,197,94,0.15);color:#16a34a;font-size:0.75rem;font-weight:600;">✓ successo</span>`
: `<span style="display:inline-block;padding:2px 10px;border-radius:12px;background:rgba(239,68,68,0.15);color:#dc2626;font-size:0.75rem;font-weight:600;">✕ errore</span>`;
return `
<tr id="row-${r.id}" data-id="${r.id}">
<td style="font-size:0.8rem;color:var(--text-secondary);white-space:nowrap;">${dt}</td>
<td style="font-size:0.85rem;">${App.escapeHtml(r.agente_nome || '—')}</td>
<td style="font-size:0.85rem;font-weight:500;">${App.escapeHtml(r.titolo_azione)}</td>
<td style="text-align:center;">${esitoBadge}</td>
<td style="text-align:center;">
<button class="btn btn-ghost btn-sm act-detail-btn" data-id="${r.id}" title="Mostra dettaglio" style="padding:4px 8px;font-size:0.85rem;">≡</button>
</td>
</tr>
<tr id="detail-${r.id}" class="act-detail-row" style="display:none;">
<td colspan="5" style="padding:0 var(--space-md) var(--space-md);background:var(--bg-secondary);">
<pre style="margin:0;padding:var(--space-md);background:var(--bg-tertiary);border-radius:var(--radius-md);font-size:0.78rem;overflow-x:auto;white-space:pre-wrap;word-break:break-all;color:var(--text-primary);border:1px solid var(--border-subtle);">${App.escapeHtml(this._prettyJson(r.azione))}</pre>
</td>
</tr>
`;
},
_prettyJson(str) {
try {
return JSON.stringify(JSON.parse(str), null, 2);
} catch (_) {
return str || '';
}
},
_buildFilters() {
return `
<div id="activity-filters" style="display:flex;flex-wrap:wrap;gap:var(--space-sm);padding:var(--space-md) var(--space-xl);border-bottom:1px solid var(--border-subtle);background:var(--bg-secondary);align-items:flex-end;">
<div style="display:flex;flex-direction:column;gap:4px;">
<label style="font-size:0.75rem;color:var(--text-muted);font-weight:500;">Esito</label>
<select id="filter-esito" class="form-select" style="min-width:120px;height:36px;font-size:0.85rem;padding:6px 28px 6px 10px;">
<option value="">Tutti</option>
<option value="successo" ${this.filters.esito === 'successo' ? 'selected' : ''}>✓ Successo</option>
<option value="errore" ${this.filters.esito === 'errore' ? 'selected' : ''}>✕ Errore</option>
</select>
</div>
<div style="display:flex;flex-direction:column;gap:4px;">
<label style="font-size:0.75rem;color:var(--text-muted);font-weight:500;">Da data</label>
<input type="datetime-local" id="filter-da" class="form-input" value="${this.filters.da}" style="height:36px;font-size:0.85rem;padding:6px 10px;">
</div>
<div style="display:flex;flex-direction:column;gap:4px;">
<label style="font-size:0.75rem;color:var(--text-muted);font-weight:500;">A data</label>
<input type="datetime-local" id="filter-a" class="form-input" value="${this.filters.a}" style="height:36px;font-size:0.85rem;padding:6px 10px;">
</div>
<div style="display:flex;gap:var(--space-xs);align-self:flex-end;">
<button id="btn-apply-filters" class="btn btn-primary btn-sm" style="height:36px;">Applica</button>
<button id="btn-reset-filters" class="btn btn-ghost btn-sm" style="height:36px;">Reset</button>
</div>
</div>
`;
},
_buildPagination(total, page, per_page, total_pages) {
if (total_pages <= 1) return '';
const from = (page - 1) * per_page + 1;
const to = Math.min(page * per_page, total);
const pageBtn = (p, label, disabled, active) => {
const isDisabled = disabled || p === page;
return `<button class="btn btn-ghost btn-sm page-btn" data-page="${p}"
style="min-width:36px;height:32px;${active ? 'background:var(--primary);color:#fff;' : ''}${isDisabled ? 'opacity:0.4;pointer-events:none;' : ''}"
${disabled || active ? 'disabled' : ''}>${label}</button>`;
};
const pages = [];
pages.push(pageBtn(1, '«', page === 1, false));
pages.push(pageBtn(page - 1, '', page === 1, false));
const rangeStart = Math.max(1, page - 2);
const rangeEnd = Math.min(total_pages, page + 2);
for (let p = rangeStart; p <= rangeEnd; p++) {
pages.push(pageBtn(p, p, false, p === page));
}
pages.push(pageBtn(page + 1, '', page === total_pages, false));
pages.push(pageBtn(total_pages, '»', page === total_pages, false));
return `
<div id="pagination-top" style="display:flex;align-items:center;justify-content:space-between;padding:var(--space-sm) var(--space-xl);flex-wrap:wrap;gap:var(--space-sm);">
<span style="font-size:0.8rem;color:var(--text-muted);">Record ${from}${to} di ${total}</span>
<div style="display:flex;gap:4px;flex-wrap:wrap;">${pages.join('')}</div>
</div>
`;
},
_bind() {
// Filter apply
document.getElementById('btn-apply-filters')?.addEventListener('click', () => {
this.filters.esito = document.getElementById('filter-esito')?.value || '';
this.filters.da = document.getElementById('filter-da')?.value || '';
this.filters.a = document.getElementById('filter-a')?.value || '';
this.currentPage = 1;
this._draw();
});
// Filter reset
document.getElementById('btn-reset-filters')?.addEventListener('click', () => {
this.filters = { esito: '', agente_id: '', da: '', a: '' };
this.currentPage = 1;
this._draw();
});
// Detail expand/collapse
document.querySelectorAll('.act-detail-btn').forEach(btn => {
btn.addEventListener('click', () => {
const rowId = btn.dataset.id;
const detailRow = document.getElementById(`detail-${rowId}`);
if (!detailRow) return;
const isOpen = detailRow.style.display !== 'none';
detailRow.style.display = isOpen ? 'none' : 'table-row';
btn.textContent = isOpen ? '≡' : '✕';
});
});
// Pagination buttons (top and bottom share same .page-btn class)
document.querySelectorAll('.page-btn').forEach(btn => {
btn.addEventListener('click', () => {
const p = parseInt(btn.dataset.page, 10);
if (!isNaN(p)) {
this.currentPage = p;
this._draw();
document.getElementById('view-container')?.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
},
};