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

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:
Maciej Pienczyn 2026-03-18 08:44:11 +01:00
parent 31eb21a84c
commit 9d775f75d4

View File

@ -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">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 %}