nordabiz/templates/admin/analytics_dashboard.html

770 lines
27 KiB
HTML

{% extends "base.html" %}
{% block title %}Panel Analityki - Norda Biznes Hub{% 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 %}