feat(admin): sortable columns + group-by tabs for recent logins table
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
Clickable column headers with sort arrows (▲▼) for all 6 columns. Group tabs: "Wg użytkownika", "Wg urządzenia", "Wg przeglądarki" show summary with session count, total duration and total pageviews per group. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
31eb21a84c
commit
9d775f75d4
@ -104,6 +104,33 @@
|
||||
.device-tablet { background: #FEF3C7; color: #D97706; }
|
||||
.device-other { background: #F3F4F6; color: #6B7280; }
|
||||
|
||||
/* ---- Group tabs ---- */
|
||||
.group-tabs {
|
||||
display: flex;
|
||||
gap: var(--spacing-xs);
|
||||
margin-bottom: var(--spacing-md);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.group-tab {
|
||||
padding: 6px 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface);
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-sm);
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
.group-tab:hover { border-color: var(--primary); color: var(--primary); }
|
||||
.group-tab.active { background: var(--primary); color: white; border-color: var(--primary); }
|
||||
|
||||
/* ---- Sortable table headers ---- */
|
||||
.sortable { cursor: pointer; user-select: none; white-space: nowrap; }
|
||||
.sortable::after { content: ' ⇅'; opacity: 0.3; font-size: 0.8em; }
|
||||
.sortable.sort-asc::after { content: ' ▲'; opacity: 0.7; }
|
||||
.sortable.sort-desc::after { content: ' ▼'; opacity: 0.7; }
|
||||
.sortable:hover { color: var(--primary); }
|
||||
|
||||
/* ---- DAU Chart (CSS-only bars) ---- */
|
||||
.chart-container {
|
||||
overflow-x: auto;
|
||||
@ -243,23 +270,31 @@
|
||||
<div class="section">
|
||||
<h2>Ostatnie logowania</h2>
|
||||
{% if recent_sessions %}
|
||||
<div class="table-scroll">
|
||||
<table class="data-table">
|
||||
<div class="group-tabs">
|
||||
<button class="group-tab active" data-group="all">Wszystkie</button>
|
||||
<button class="group-tab" data-group="user">Wg użytkownika</button>
|
||||
<button class="group-tab" data-group="device">Wg urządzenia</button>
|
||||
<button class="group-tab" data-group="browser">Wg przeglądarki</button>
|
||||
</div>
|
||||
|
||||
<!-- Detail table -->
|
||||
<div id="logins-detail" class="table-scroll">
|
||||
<table class="data-table" id="logins-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Uzytkownik</th>
|
||||
<th>Data</th>
|
||||
<th>Urzadzenie</th>
|
||||
<th>Przegladarka</th>
|
||||
<th>Czas (min)</th>
|
||||
<th>Odslony</th>
|
||||
<th class="sortable" data-col="0" data-type="string">Użytkownik</th>
|
||||
<th class="sortable sort-desc" data-col="1" data-type="date">Data</th>
|
||||
<th class="sortable" data-col="2" data-type="string">Urządzenie</th>
|
||||
<th class="sortable" data-col="3" data-type="string">Przeglądarka</th>
|
||||
<th class="sortable" data-col="4" data-type="number">Czas (min)</th>
|
||||
<th class="sortable" data-col="5" data-type="number">Odsłony</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for s in recent_sessions %}
|
||||
<tr>
|
||||
<tr data-user="{{ s.user_name }}" data-device="{{ s.device_type }}" data-browser="{{ s.browser }}">
|
||||
<td>{{ s.user_name }}</td>
|
||||
<td>{{ s.started_at.strftime('%d.%m.%Y %H:%M') if s.started_at else '-' }}</td>
|
||||
<td data-sort-value="{{ s.started_at.strftime('%Y%m%d%H%M') if s.started_at else '0' }}">{{ s.started_at.strftime('%d.%m.%Y %H:%M') if s.started_at else '-' }}</td>
|
||||
<td>
|
||||
{% set dt = s.device_type|lower %}
|
||||
<span class="device-badge device-{{ dt if dt in ['desktop','mobile','tablet'] else 'other' }}">
|
||||
@ -274,6 +309,14 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Summary table (shown when grouped) -->
|
||||
<div id="logins-summary" class="table-scroll" style="display:none;">
|
||||
<table class="data-table" id="summary-table">
|
||||
<thead><tr id="summary-header"></tr></thead>
|
||||
<tbody id="summary-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-muted">Brak danych o sesjach w ostatnich 30 dniach.</p>
|
||||
{% endif %}
|
||||
@ -316,3 +359,96 @@
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
/* --- Logins table: sorting + grouping --- */
|
||||
(function() {
|
||||
var table = document.getElementById('logins-table');
|
||||
if (!table) return;
|
||||
var tbody = table.querySelector('tbody');
|
||||
var rows = Array.from(tbody.querySelectorAll('tr'));
|
||||
|
||||
/* Column sorting */
|
||||
table.querySelectorAll('th.sortable').forEach(function(th) {
|
||||
th.addEventListener('click', function() {
|
||||
var col = parseInt(th.dataset.col);
|
||||
var type = th.dataset.type;
|
||||
var isDesc = th.classList.contains('sort-desc');
|
||||
var dir = isDesc ? 1 : -1;
|
||||
|
||||
table.querySelectorAll('th.sortable').forEach(function(h) {
|
||||
h.classList.remove('sort-asc', 'sort-desc');
|
||||
});
|
||||
th.classList.add(isDesc ? 'sort-asc' : 'sort-desc');
|
||||
|
||||
rows.sort(function(a, b) {
|
||||
var aCell = a.cells[col], bCell = b.cells[col];
|
||||
var aVal, bVal;
|
||||
if (type === 'date') {
|
||||
aVal = aCell.dataset.sortValue || '0';
|
||||
bVal = bCell.dataset.sortValue || '0';
|
||||
} else if (type === 'number') {
|
||||
aVal = parseFloat(aCell.textContent) || 0;
|
||||
bVal = parseFloat(bCell.textContent) || 0;
|
||||
return (aVal - bVal) * dir;
|
||||
} else {
|
||||
aVal = aCell.textContent.trim().toLowerCase();
|
||||
bVal = bCell.textContent.trim().toLowerCase();
|
||||
}
|
||||
return aVal < bVal ? dir : aVal > bVal ? -dir : 0;
|
||||
});
|
||||
rows.forEach(function(r) { tbody.appendChild(r); });
|
||||
});
|
||||
});
|
||||
|
||||
/* Group tabs */
|
||||
var detailDiv = document.getElementById('logins-detail');
|
||||
var summaryDiv = document.getElementById('logins-summary');
|
||||
var summaryHeader = document.getElementById('summary-header');
|
||||
var summaryBody = document.getElementById('summary-body');
|
||||
|
||||
document.querySelectorAll('.group-tab').forEach(function(tab) {
|
||||
tab.addEventListener('click', function() {
|
||||
document.querySelectorAll('.group-tab').forEach(function(t) { t.classList.remove('active'); });
|
||||
tab.classList.add('active');
|
||||
var group = tab.dataset.group;
|
||||
|
||||
if (group === 'all') {
|
||||
detailDiv.style.display = '';
|
||||
summaryDiv.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
detailDiv.style.display = 'none';
|
||||
summaryDiv.style.display = '';
|
||||
|
||||
var labels = { user: 'Użytkownik', device: 'Urządzenie', browser: 'Przeglądarka' };
|
||||
summaryHeader.innerHTML = '<th>' + labels[group] + '</th><th>Sesje</th><th>Łączny czas (min)</th><th>Łączne odsłony</th>';
|
||||
|
||||
var groups = {};
|
||||
rows.forEach(function(r) {
|
||||
var key = r.dataset[group] || '-';
|
||||
if (!groups[key]) groups[key] = { count: 0, duration: 0, views: 0 };
|
||||
groups[key].count++;
|
||||
groups[key].duration += parseFloat(r.cells[4].textContent) || 0;
|
||||
groups[key].views += parseInt(r.cells[5].textContent) || 0;
|
||||
});
|
||||
|
||||
var sorted = Object.keys(groups).sort(function(a, b) {
|
||||
return groups[b].count - groups[a].count;
|
||||
});
|
||||
|
||||
summaryBody.innerHTML = '';
|
||||
sorted.forEach(function(key) {
|
||||
var g = groups[key];
|
||||
var tr = document.createElement('tr');
|
||||
tr.innerHTML = '<td><strong>' + key + '</strong></td>'
|
||||
+ '<td class="num">' + g.count + '</td>'
|
||||
+ '<td class="num">' + g.duration.toFixed(1) + '</td>'
|
||||
+ '<td class="num">' + g.views + '</td>';
|
||||
summaryBody.appendChild(tr);
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
{% endblock %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user