feat: aggiunta tracciatura delle modifiche effettuate
This commit is contained in:
@@ -79,6 +79,15 @@
|
||||
<span>Apertura Massiva</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#/activity" class="nav-link" data-view="activity" id="nav-activity">
|
||||
<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<polyline points="12 6 12 12 16 14" />
|
||||
</svg>
|
||||
<span>Storico Attività</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
@@ -158,6 +167,7 @@
|
||||
<script src="/js/views/ticketDetail.js"></script>
|
||||
<script src="/js/views/ticketCreate.js"></script>
|
||||
<script src="/js/views/ticketBulk.js"></script>
|
||||
<script src="/js/views/activityLog.js"></script>
|
||||
<script src="/js/app.js"></script>
|
||||
</body>
|
||||
|
||||
|
||||
@@ -138,6 +138,11 @@ const App = {
|
||||
titleEl.textContent = 'Apertura Massiva Ticket';
|
||||
TicketBulkView.render();
|
||||
|
||||
} else if (hash === '#/activity') {
|
||||
document.getElementById('nav-activity')?.classList.add('active');
|
||||
titleEl.textContent = 'Storico Attività';
|
||||
ActivityLogView.render();
|
||||
|
||||
} else if (hash.match(/^#\/tickets\/(\d+)$/)) {
|
||||
const id = hash.match(/^#\/tickets\/(\d+)$/)[1];
|
||||
document.getElementById('nav-tickets')?.classList.add('active');
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
/**
|
||||
* 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' });
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user