- Zmiana nazwy: "Norda Biznes Hub" → "Norda Biznes Partner" - Aktualizacja modelu AI: Gemini 2.0 Flash → Gemini 3 Flash - Zachowano historyczne odniesienia w timeline i dokumentacji Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
770 lines
27 KiB
HTML
770 lines
27 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Panel Analityki - Norda Biznes Partner{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.admin-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--spacing-xl);
|
|
flex-wrap: wrap;
|
|
gap: var(--spacing-md);
|
|
}
|
|
|
|
.admin-header h1 {
|
|
font-size: var(--font-size-3xl);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.header-actions {
|
|
display: flex;
|
|
gap: var(--spacing-sm);
|
|
align-items: center;
|
|
}
|
|
|
|
/* Period Tabs */
|
|
.period-tabs {
|
|
display: flex;
|
|
gap: var(--spacing-xs);
|
|
background: white;
|
|
padding: var(--spacing-xs);
|
|
border-radius: var(--radius);
|
|
box-shadow: var(--shadow-sm);
|
|
}
|
|
|
|
.period-tab {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
border: none;
|
|
background: transparent;
|
|
border-radius: var(--radius-sm);
|
|
cursor: pointer;
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
transition: var(--transition);
|
|
}
|
|
|
|
.period-tab:hover {
|
|
background: var(--background);
|
|
}
|
|
|
|
.period-tab.active {
|
|
background: var(--primary);
|
|
color: white;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Stats Grid */
|
|
.stats-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.stat-card {
|
|
background: white;
|
|
padding: var(--spacing-lg);
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-sm);
|
|
text-align: center;
|
|
}
|
|
|
|
.stat-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
margin: 0 auto var(--spacing-md);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.stat-icon.blue { background: #e0f2fe; color: #0284c7; }
|
|
.stat-icon.green { background: #d1fae5; color: #059669; }
|
|
.stat-icon.purple { background: #ede9fe; color: #7c3aed; }
|
|
.stat-icon.orange { background: #ffedd5; color: #ea580c; }
|
|
.stat-icon.gray { background: #f1f5f9; color: #64748b; }
|
|
|
|
.stat-number {
|
|
font-size: var(--font-size-2xl);
|
|
font-weight: 700;
|
|
display: block;
|
|
margin-bottom: var(--spacing-xs);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.stat-label {
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
/* Two Column Layout */
|
|
.two-columns {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: var(--spacing-xl);
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
@media (max-width: 1024px) {
|
|
.two-columns {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
|
|
/* Section Card */
|
|
.section-card {
|
|
background: white;
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-sm);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.section-header {
|
|
padding: var(--spacing-md) var(--spacing-lg);
|
|
border-bottom: 1px solid var(--border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.section-header h3 {
|
|
font-size: var(--font-size-lg);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.section-body {
|
|
padding: var(--spacing-lg);
|
|
}
|
|
|
|
/* Device Chart */
|
|
.device-chart {
|
|
display: flex;
|
|
justify-content: space-around;
|
|
align-items: center;
|
|
padding: var(--spacing-lg) 0;
|
|
}
|
|
|
|
.device-item {
|
|
text-align: center;
|
|
}
|
|
|
|
.device-icon {
|
|
width: 48px;
|
|
height: 48px;
|
|
margin: 0 auto var(--spacing-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.device-count {
|
|
font-size: var(--font-size-xl);
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.device-label {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.device-percent {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--primary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* Tables */
|
|
.analytics-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
}
|
|
|
|
.analytics-table th,
|
|
.analytics-table td {
|
|
padding: var(--spacing-sm) var(--spacing-md);
|
|
text-align: left;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
/* Kolumna użytkownika - szersza */
|
|
.analytics-table td:first-child {
|
|
min-width: 250px;
|
|
max-width: 350px;
|
|
}
|
|
|
|
.analytics-table th {
|
|
background: var(--background);
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
font-size: var(--font-size-sm);
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.analytics-table tbody tr:hover {
|
|
background: var(--background);
|
|
}
|
|
|
|
.analytics-table tbody tr:last-child td {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.user-cell {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
.user-avatar {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 50%;
|
|
background: var(--primary);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.user-info {
|
|
flex: 1;
|
|
}
|
|
|
|
.analytics-user-name {
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.analytics-user-email {
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.engagement-badge {
|
|
display: inline-block;
|
|
padding: var(--spacing-xs) var(--spacing-sm);
|
|
border-radius: var(--radius);
|
|
font-size: var(--font-size-sm);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.engagement-high { background: #d1fae5; color: #059669; }
|
|
.engagement-medium { background: #fef3c7; color: #d97706; }
|
|
.engagement-low { background: #fee2e2; color: #dc2626; }
|
|
|
|
/* Page path */
|
|
.page-path {
|
|
font-family: monospace;
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-primary);
|
|
max-width: 300px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Recent Sessions */
|
|
.session-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
padding: var(--spacing-md);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.session-item:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.session-user {
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
.session-user-name {
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.session-anonymous {
|
|
color: var(--text-secondary);
|
|
font-style: italic;
|
|
}
|
|
|
|
.session-meta {
|
|
display: flex;
|
|
gap: var(--spacing-md);
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.session-time {
|
|
text-align: right;
|
|
font-size: var(--font-size-sm);
|
|
}
|
|
|
|
.session-duration {
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.session-pages {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.session-device {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* Empty State */
|
|
.empty-state {
|
|
text-align: center;
|
|
padding: var(--spacing-2xl);
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.empty-state svg {
|
|
width: 64px;
|
|
height: 64px;
|
|
margin-bottom: var(--spacing-md);
|
|
opacity: 0.5;
|
|
}
|
|
|
|
/* Scrollable table */
|
|
.table-scroll {
|
|
max-height: 400px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* Back Link */
|
|
.back-link {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--spacing-xs);
|
|
color: var(--text-secondary);
|
|
text-decoration: none;
|
|
font-size: var(--font-size-sm);
|
|
margin-bottom: var(--spacing-md);
|
|
}
|
|
|
|
.back-link:hover {
|
|
color: var(--primary);
|
|
}
|
|
|
|
/* User Detail Modal */
|
|
.user-detail-section {
|
|
background: white;
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-sm);
|
|
padding: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
|
|
.user-detail-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-lg);
|
|
padding-bottom: var(--spacing-lg);
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
|
|
.user-detail-avatar {
|
|
width: 64px;
|
|
height: 64px;
|
|
border-radius: 50%;
|
|
background: var(--primary);
|
|
color: white;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: var(--font-size-xl);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.user-detail-info h2 {
|
|
font-size: var(--font-size-xl);
|
|
margin-bottom: var(--spacing-xs);
|
|
}
|
|
|
|
.user-detail-info p {
|
|
color: var(--text-secondary);
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<main>
|
|
<div class="container">
|
|
<a href="{{ url_for('admin_users') }}" class="back-link">
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M10 12L6 8l4-4"/>
|
|
</svg>
|
|
Panel Admina
|
|
</a>
|
|
|
|
<div class="admin-header">
|
|
<div>
|
|
<h1>Analityka Użytkowników</h1>
|
|
</div>
|
|
<div class="header-actions">
|
|
<div class="period-tabs">
|
|
<a href="{{ url_for('admin_analytics', period='day') }}"
|
|
class="period-tab {% if current_period == 'day' %}active{% endif %}">Dzis</a>
|
|
<a href="{{ url_for('admin_analytics', period='week') }}"
|
|
class="period-tab {% if current_period == 'week' %}active{% endif %}">7 dni</a>
|
|
<a href="{{ url_for('admin_analytics', period='month') }}"
|
|
class="period-tab {% if current_period == 'month' %}active{% endif %}">30 dni</a>
|
|
<a href="{{ url_for('admin_analytics', period='all') }}"
|
|
class="period-tab {% if current_period == 'all' %}active{% endif %}">Wszystko</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% if user_detail %}
|
|
<!-- User Detail Section -->
|
|
<div class="user-detail-section">
|
|
<div class="user-detail-header">
|
|
<div class="user-detail-avatar">
|
|
{{ user_detail.user.name[0]|upper if user_detail.user and user_detail.user.name else '?' }}
|
|
</div>
|
|
<div class="user-detail-info">
|
|
<h2>{{ user_detail.user.name if user_detail.user else 'Nieznany' }}</h2>
|
|
<p>{{ user_detail.user.email if user_detail.user else '' }}</p>
|
|
</div>
|
|
<a href="{{ url_for('admin_analytics', period=current_period) }}" class="btn btn-secondary">
|
|
Zamknij
|
|
</a>
|
|
</div>
|
|
|
|
<h3 style="margin-bottom: var(--spacing-md);">Ostatnie sesje</h3>
|
|
<div class="table-scroll">
|
|
<table class="analytics-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Data</th>
|
|
<th>Czas trwania</th>
|
|
<th>Strony</th>
|
|
<th>Kliknięcia</th>
|
|
<th>Urzadzenie</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for s in user_detail.sessions %}
|
|
<tr>
|
|
<td>{{ s.started_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
|
<td>{{ ((s.duration_seconds or 0) // 60) }} min</td>
|
|
<td>{{ s.page_views_count|default(0) }}</td>
|
|
<td>{{ s.clicks_count|default(0) }}</td>
|
|
<td>{{ s.device_type|default('?') }} / {{ s.browser|default('?') }}</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="5" class="empty-state">Brak sesji</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<h3 style="margin: var(--spacing-lg) 0 var(--spacing-md);">Ostatnie odwiedzone strony</h3>
|
|
<div class="table-scroll">
|
|
<table class="analytics-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Ścieżka</th>
|
|
<th>Czas</th>
|
|
<th>Data</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for p in user_detail.pages %}
|
|
<tr>
|
|
<td class="page-path">{{ p.path }}</td>
|
|
<td>{% if p.time_on_page_seconds %}{{ (p.time_on_page_seconds // 60) }} min{% else %}-{% endif %}</td>
|
|
<td>{{ p.viewed_at.strftime('%Y-%m-%d %H:%M') }}</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="3" class="empty-state">Brak danych</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- Stats Cards -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-icon blue">
|
|
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
|
|
<circle cx="9" cy="7" r="4"/>
|
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
|
|
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
|
|
</svg>
|
|
</div>
|
|
<span class="stat-number">{{ stats.total_sessions }}</span>
|
|
<span class="stat-label">Sesje</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon green">
|
|
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
|
<circle cx="12" cy="7" r="4"/>
|
|
</svg>
|
|
</div>
|
|
<span class="stat-number">{{ stats.unique_users }}</span>
|
|
<span class="stat-label">Unikalni uzytkownicy</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon purple">
|
|
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/>
|
|
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/>
|
|
</svg>
|
|
</div>
|
|
<span class="stat-number">{{ stats.total_page_views }}</span>
|
|
<span class="stat-label">Wysw. stron</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon orange">
|
|
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5"/>
|
|
</svg>
|
|
</div>
|
|
<span class="stat-number">{{ stats.total_clicks }}</span>
|
|
<span class="stat-label">Kliknięcia</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-icon gray">
|
|
<svg width="24" height="24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<circle cx="12" cy="12" r="10"/>
|
|
<polyline points="12 6 12 12 16 14"/>
|
|
</svg>
|
|
</div>
|
|
<span class="stat-number">{{ (stats.avg_duration / 60)|round(1) if stats.avg_duration else 0 }} min</span>
|
|
<span class="stat-label">Śr. czas sesji</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="two-columns">
|
|
<!-- Device Breakdown -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<h3>Urządzenia</h3>
|
|
</div>
|
|
<div class="section-body">
|
|
<div class="device-chart">
|
|
<div class="device-item">
|
|
<div class="device-icon">
|
|
<svg width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
<rect x="4" y="6" width="40" height="28" rx="2"/>
|
|
<line x1="4" y1="38" x2="44" y2="38"/>
|
|
<line x1="18" y1="34" x2="30" y2="34"/>
|
|
</svg>
|
|
</div>
|
|
<div class="device-count">{{ device_stats.get('desktop', 0) }}</div>
|
|
<div class="device-label">Desktop</div>
|
|
{% set total_devices = device_stats.get('desktop', 0) + device_stats.get('mobile', 0) + device_stats.get('tablet', 0) %}
|
|
<div class="device-percent">
|
|
{{ ((device_stats.get('desktop', 0) / total_devices * 100)|round(0)|int if total_devices > 0 else 0) }}%
|
|
</div>
|
|
</div>
|
|
<div class="device-item">
|
|
<div class="device-icon">
|
|
<svg width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
<rect x="14" y="4" width="20" height="40" rx="2"/>
|
|
<line x1="20" y1="40" x2="28" y2="40"/>
|
|
</svg>
|
|
</div>
|
|
<div class="device-count">{{ device_stats.get('mobile', 0) }}</div>
|
|
<div class="device-label">Mobile</div>
|
|
<div class="device-percent">
|
|
{{ ((device_stats.get('mobile', 0) / total_devices * 100)|round(0)|int if total_devices > 0 else 0) }}%
|
|
</div>
|
|
</div>
|
|
<div class="device-item">
|
|
<div class="device-icon">
|
|
<svg width="48" height="48" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
<rect x="6" y="8" width="36" height="28" rx="2"/>
|
|
<line x1="18" y1="40" x2="30" y2="40"/>
|
|
</svg>
|
|
</div>
|
|
<div class="device-count">{{ device_stats.get('tablet', 0) }}</div>
|
|
<div class="device-label">Tablet</div>
|
|
<div class="device-percent">
|
|
{{ ((device_stats.get('tablet', 0) / total_devices * 100)|round(0)|int if total_devices > 0 else 0) }}%
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Popular Pages -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<h3>Popularne strony</h3>
|
|
</div>
|
|
<div class="table-scroll">
|
|
<table class="analytics-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Ścieżka</th>
|
|
<th>Wysw.</th>
|
|
<th>Unik.</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for page in popular_pages[:10] %}
|
|
<tr>
|
|
<td class="page-path" title="{{ page.path }}">{{ page.path }}</td>
|
|
<td>{{ page.views }}</td>
|
|
<td>{{ page.unique_users }}</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="3" class="empty-state">Brak danych</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- User Rankings -->
|
|
<div class="section-card" style="margin-bottom: var(--spacing-xl);">
|
|
<div class="section-header">
|
|
<h3>Ranking użytkowników wg aktywności</h3>
|
|
</div>
|
|
<div class="table-scroll">
|
|
<table class="analytics-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Użytkownik</th>
|
|
<th>Sesje</th>
|
|
<th>Strony</th>
|
|
<th>Kliknięcia</th>
|
|
<th>Czas</th>
|
|
<th>Zaangażowanie</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for user in user_rankings %}
|
|
<tr>
|
|
<td>
|
|
<div class="user-cell">
|
|
<div class="user-avatar">{{ user.name[0]|upper if user.name else '?' }}</div>
|
|
<div class="user-info">
|
|
<div class="analytics-user-name">{{ user.name }}</div>
|
|
<div class="analytics-user-email">{{ user.email }}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>{{ user.sessions }}</td>
|
|
<td>{{ user.page_views|default(0) }}</td>
|
|
<td>{{ user.clicks|default(0) }}</td>
|
|
<td>{{ ((user.total_time or 0) / 60)|round(0)|int }} min</td>
|
|
<td>
|
|
{% set engagement = (user.page_views|default(0)) + (user.clicks|default(0)) * 2 + ((user.total_time or 0) / 120)|int %}
|
|
{% if engagement >= 50 %}
|
|
<span class="engagement-badge engagement-high">Wysoki ({{ engagement }})</span>
|
|
{% elif engagement >= 20 %}
|
|
<span class="engagement-badge engagement-medium">Średni ({{ engagement }})</span>
|
|
{% else %}
|
|
<span class="engagement-badge engagement-low">Niski ({{ engagement }})</span>
|
|
{% endif %}
|
|
</td>
|
|
<td>
|
|
<a href="{{ url_for('admin_analytics', period=current_period, user_id=user.id) }}"
|
|
style="color: var(--primary); text-decoration: none;">
|
|
Szczegóły
|
|
</a>
|
|
</td>
|
|
</tr>
|
|
{% else %}
|
|
<tr>
|
|
<td colspan="7" class="empty-state">
|
|
<svg width="64" height="64" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
<circle cx="32" cy="32" r="28"/>
|
|
<path d="M24 28h16"/>
|
|
<path d="M24 36h8"/>
|
|
</svg>
|
|
<p>Brak danych o użytkownikach w wybranym okresie</p>
|
|
</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Recent Sessions -->
|
|
<div class="section-card">
|
|
<div class="section-header">
|
|
<h3>Ostatnie sesje</h3>
|
|
</div>
|
|
<div class="table-scroll" style="max-height: 500px;">
|
|
{% for s in recent_sessions %}
|
|
<div class="session-item">
|
|
<div class="session-user">
|
|
{% if s.user %}
|
|
<div class="session-user-name">{{ s.user.name }}</div>
|
|
<div style="font-size: var(--font-size-sm); color: var(--text-secondary);">{{ s.user.email }}</div>
|
|
{% else %}
|
|
<div class="session-anonymous">Niezalogowany</div>
|
|
{% endif %}
|
|
</div>
|
|
<div class="session-device">
|
|
{% if s.device_type == 'mobile' %}
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="4" y="1" width="8" height="14" rx="1"/>
|
|
</svg>
|
|
{% elif s.device_type == 'tablet' %}
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="2" y="2" width="12" height="12" rx="1"/>
|
|
</svg>
|
|
{% else %}
|
|
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="1" y="2" width="14" height="10" rx="1"/>
|
|
<line x1="4" y1="14" x2="12" y2="14"/>
|
|
</svg>
|
|
{% endif %}
|
|
{{ s.browser|default('?') }}
|
|
</div>
|
|
<div class="session-time">
|
|
<div class="session-duration">{{ ((s.duration_seconds or 0) // 60) }} min</div>
|
|
<div class="session-pages">{{ s.page_views_count|default(0) }} stron</div>
|
|
</div>
|
|
<div style="font-size: var(--font-size-sm); color: var(--text-secondary); min-width: 100px; text-align: right;">
|
|
{{ s.started_at.strftime('%d.%m %H:%M') }}
|
|
</div>
|
|
</div>
|
|
{% else %}
|
|
<div class="empty-state">
|
|
<svg width="64" height="64" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
<circle cx="32" cy="32" r="28"/>
|
|
<path d="M32 20v12"/>
|
|
<path d="M32 40h.01"/>
|
|
</svg>
|
|
<p>Brak sesji w wybranym okresie</p>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
{% endblock %}
|