nordabiz/templates/admin/user_insights.html
Maciej Pienczyn a583ae3386
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
feat: add detailed tooltip to engagement activity bar
Hovering over the activity bar shows breakdown: sessions, pages,
clicks, time, conversions, searches with weights and final score.
Column header explains what the metric measures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 21:03:12 +01:00

1418 lines
72 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %}
{% block title %}Analityka - Admin{% endblock %}
{% block head_extra %}
{% if tab == 'overview' %}
<script src="{{ url_for('static', filename='js/vendor/chart.min.js') }}"></script>
{% endif %}
{% endblock %}
{% block extra_css %}
<style>
.insights-header { display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: var(--spacing-md); margin-bottom: var(--spacing-lg); }
.insights-header h1 { font-size: var(--font-size-2xl); font-weight: 700; }
.insights-controls { display: flex; gap: var(--spacing-sm); align-items: center; flex-wrap: wrap; }
/* 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); text-decoration: none; }
.period-tab.active { background: var(--primary); color: white; font-weight: 500; }
.period-tab:hover:not(.active) { background: var(--background); }
/* Main tabs */
.tabs { display: flex; gap: var(--spacing-xs); margin-bottom: var(--spacing-lg); background: white; padding: var(--spacing-xs); border-radius: var(--radius); box-shadow: var(--shadow-sm); flex-wrap: wrap; }
.tab-link { padding: var(--spacing-sm) var(--spacing-md); border: none; background: transparent; border-radius: var(--radius-sm); cursor: pointer; font-size: var(--font-size-sm); font-weight: 500; color: var(--text-secondary); transition: var(--transition); text-decoration: none; white-space: nowrap; }
.tab-link.active { background: var(--primary); color: white; }
.tab-link:hover:not(.active) { background: var(--background); }
/* Stat cards */
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 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); }
.stat-card.error { border-left: 4px solid var(--error); }
.stat-card.warning { border-left: 4px solid #f59e0b; }
.stat-card.info { border-left: 4px solid #3b82f6; }
.stat-card.success { border-left: 4px solid var(--success); }
.stat-value { font-size: var(--font-size-2xl); font-weight: 700; color: var(--text-primary); }
.stat-label { color: var(--text-secondary); font-size: var(--font-size-sm); margin-top: var(--spacing-xs); }
/* Section cards */
.section-card { background: white; padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); margin-bottom: var(--spacing-xl); }
.section-card h2 { font-size: var(--font-size-lg); font-weight: 600; margin-bottom: var(--spacing-lg); display: flex; align-items: center; gap: var(--spacing-sm); }
/* Data table */
.data-table { width: 100%; border-collapse: collapse; }
.data-table th { text-align: left; padding: var(--spacing-sm) var(--spacing-md); background: var(--background); font-weight: 600; font-size: var(--font-size-xs); color: var(--text-secondary); border-bottom: 1px solid var(--border); text-transform: uppercase; letter-spacing: 0.5px; }
.data-table td { padding: var(--spacing-sm) var(--spacing-md); border-bottom: 1px solid var(--border); font-size: var(--font-size-sm); }
.data-table tr:last-child td { border-bottom: none; }
.data-table tr:hover td { background: var(--background); }
/* User cell */
.user-cell { display: flex; align-items: center; gap: var(--spacing-sm); }
.user-cell a { color: var(--primary); text-decoration: none; font-weight: 500; }
.user-cell a:hover { text-decoration: underline; }
.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-xs); font-weight: 600; flex-shrink: 0; }
/* Badges */
.badge { display: inline-block; padding: 2px 8px; border-radius: var(--radius); font-size: var(--font-size-xs); font-weight: 500; }
.badge-critical { background: #7f1d1d; color: white; }
.badge-high { background: #fee2e2; color: #991b1b; }
.badge-medium { background: #fef3c7; color: #92400e; }
.badge-low { background: #e0f2fe; color: #0369a1; }
.badge-ok { background: #dcfce7; color: #166534; }
.badge-active { background: #dcfce7; color: #166534; }
.badge-at-risk, .badge-at_risk { background: #fef3c7; color: #92400e; }
.badge-dormant { background: #fee2e2; color: #991b1b; }
/* Engagement bars */
.engagement-bar-wrap { height: 8px; background: var(--background); border-radius: 4px; overflow: hidden; }
.engagement-bar { height: 100%; border-radius: 4px; min-width: 2px; transition: width 0.3s ease; }
/* Horizontal bars */
.bar-container { height: 20px; background: var(--background); border-radius: var(--radius-sm); overflow: hidden; min-width: 100px; }
.bar-fill { height: 100%; border-radius: var(--radius-sm); transition: width 0.3s ease; }
.bar-fill.primary { background: var(--primary); }
.bar-fill.green { background: #22c55e; }
.bar-fill.blue { background: #3b82f6; }
/* Section heatmap grid */
.sections-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: var(--spacing-md); margin-bottom: var(--spacing-xl); }
.section-tile { padding: var(--spacing-lg); border-radius: var(--radius); text-align: center; transition: transform 0.2s; }
.section-tile:hover { transform: translateY(-2px); }
.section-tile h3 { font-size: var(--font-size-sm); font-weight: 600; margin-bottom: var(--spacing-sm); }
.section-tile .metric { font-size: var(--font-size-xs); color: rgba(0,0,0,0.6); }
/* Hourly heatmap */
.heatmap-table { border-collapse: collapse; width: 100%; }
.heatmap-table th { padding: 4px 6px; font-size: var(--font-size-xs); color: var(--text-secondary); font-weight: 500; }
.heatmap-cell { width: 28px; height: 28px; border-radius: 4px; margin: 1px; }
.heatmap-table td { padding: 1px; text-align: center; }
.heatmap-label { text-align: right; padding-right: 8px !important; font-weight: 500; font-size: var(--font-size-xs); color: var(--text-secondary); min-width: 30px; }
/* Two columns layout */
.two-columns { display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-xl); }
@media (max-width: 1024px) { .two-columns { grid-template-columns: 1fr; } }
/* Alert action buttons */
.btn-alert-action { padding: 4px 10px; font-size: var(--font-size-xs); border: 1px solid var(--primary); color: var(--primary); background: white; border-radius: var(--radius-sm); cursor: pointer; white-space: nowrap; transition: var(--transition); }
.btn-alert-action:hover { background: var(--primary); color: white; }
.btn-alert-action:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-alert-action.sent { border-color: var(--success); color: var(--success); }
/* Issue tags */
.issue-tag { display: inline-block; padding: 2px 8px; font-size: var(--font-size-xs); border-radius: var(--radius-sm); background: #fef3c7; color: #92400e; }
/* Path transitions */
.transition-row { display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm) 0; border-bottom: 1px solid var(--border); font-size: var(--font-size-sm); }
.transition-arrow { color: var(--text-muted); }
.transition-count { font-weight: 600; min-width: 40px; text-align: right; }
/* Session length bars */
.bar-chart-row { display: flex; align-items: center; gap: var(--spacing-md); margin-bottom: var(--spacing-sm); }
.bar-chart-label { width: 100px; min-width: 100px; font-size: var(--font-size-sm); color: var(--text-secondary); }
.bar-chart-bar { flex: 1; height: 28px; background: var(--background); border-radius: var(--radius-sm); overflow: hidden; }
.bar-chart-fill { height: 100%; background: var(--primary); border-radius: var(--radius-sm); display: flex; align-items: center; padding-left: var(--spacing-sm); }
.bar-chart-fill span { color: white; font-size: var(--font-size-xs); font-weight: 600; }
/* Overview charts */
.chart-container { position: relative; height: 300px; }
/* Filter buttons */
.filter-group { display: flex; gap: var(--spacing-xs); }
.filter-btn { padding: 4px 12px; border: 1px solid var(--border); background: white; border-radius: var(--radius-sm); font-size: var(--font-size-xs); cursor: pointer; text-decoration: none; color: var(--text-secondary); }
.filter-btn.active { background: var(--primary); color: white; border-color: var(--primary); }
/* Export button */
.btn-export { display: inline-flex; align-items: center; gap: var(--spacing-xs); padding: var(--spacing-sm) var(--spacing-md); background: white; border: 1px solid var(--border); border-radius: var(--radius); font-size: var(--font-size-sm); color: var(--text-secondary); text-decoration: none; cursor: pointer; }
.btn-export:hover { background: var(--background); }
/* Table scroll */
.table-scroll { max-height: 500px; overflow-y: auto; }
/* Alerts */
.alert-card { display: flex; align-items: flex-start; gap: var(--spacing-md); padding: var(--spacing-md) var(--spacing-lg); border-radius: var(--radius); margin-bottom: var(--spacing-sm); }
.alert-card.critical { background: #fef2f2; border-left: 4px solid #dc2626; }
.alert-card.high { background: #fffbeb; border-left: 4px solid #f59e0b; }
.alert-card.medium { background: #eff6ff; border-left: 4px solid #3b82f6; }
.alert-icon { width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 14px; }
.alert-card.critical .alert-icon { background: #fee2e2; }
.alert-card.high .alert-icon { background: #fef3c7; }
.alert-card.medium .alert-icon { background: #dbeafe; }
.alert-body { flex: 1; }
.alert-message { font-size: var(--font-size-sm); font-weight: 600; color: var(--text-primary); }
.alert-detail { font-size: var(--font-size-xs); color: var(--text-secondary); margin-top: 2px; }
.alert-action { flex-shrink: 0; }
.alert-action a { padding: 4px 12px; background: white; border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: var(--font-size-xs); color: var(--primary); text-decoration: none; }
.alert-action a:hover { background: var(--background); }
/* Resolved section toggle */
details[open] summary span:first-child { transform: rotate(90deg); }
details summary::-webkit-details-marker { display: none; }
/* Reset password button */
.btn-reset-pw { padding: 4px 10px; background: white; border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: var(--font-size-xs); cursor: pointer; white-space: nowrap; transition: var(--transition); }
.btn-reset-pw:hover { background: var(--background); border-color: var(--primary); }
.btn-reset-pw.sent { background: #dcfce7; color: #166534; border-color: #86efac; cursor: default; }
.btn-reset-pw.error { background: #fef2f2; color: #991b1b; border-color: #fca5a5; }
/* Responsive */
@media (max-width: 768px) {
.insights-header { flex-direction: column; align-items: flex-start; }
.tabs { overflow-x: auto; flex-wrap: nowrap; }
.sections-grid { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
.data-table { font-size: var(--font-size-xs); }
.data-table th, .data-table td { padding: var(--spacing-xs) var(--spacing-sm); }
}
</style>
{% endblock %}
{% block content %}
<div class="admin-container" style="max-width: 1400px; margin: 0 auto; padding: var(--spacing-lg);">
<!-- Header -->
<div class="insights-header">
<h1>Analityka</h1>
<div class="insights-controls">
<div class="period-tabs">
<a href="{{ url_for('admin.user_insights', tab=tab, period='day') }}" class="period-tab {% if period == 'day' %}active{% endif %}">Dziś</a>
<a href="{{ url_for('admin.user_insights', tab=tab, period='week') }}" class="period-tab {% if period == 'week' %}active{% endif %}">7 dni</a>
<a href="{{ url_for('admin.user_insights', tab=tab, period='month') }}" class="period-tab {% if period == 'month' %}active{% endif %}">30 dni</a>
</div>
{% if tab in ['problems', 'engagement', 'pages'] %}
<a href="{{ url_for('admin.user_insights_export', type=tab, period=period) }}" class="btn-export">
<svg width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>
CSV
</a>
{% endif %}
</div>
</div>
<!-- Tabs -->
<div class="tabs">
<a href="{{ url_for('admin.user_insights', tab='overview', period=period) }}" class="tab-link {% if tab == 'overview' %}active{% endif %}">Przegląd</a>
<a href="{{ url_for('admin.user_insights', tab='engagement', period=period) }}" class="tab-link {% if tab == 'engagement' %}active{% endif %}">Zaangażowanie</a>
<a href="{{ url_for('admin.user_insights', tab='pages', period=period) }}" class="tab-link {% if tab == 'pages' %}active{% endif %}">Strony</a>
<a href="{{ url_for('admin.user_insights', tab='paths', period=period) }}" class="tab-link {% if tab == 'paths' %}active{% endif %}">Ścieżki</a>
<a href="{{ url_for('admin.user_insights', tab='problems', period=period) }}" class="tab-link {% if tab == 'problems' %}active{% endif %}">Problemy</a>
<a href="{{ url_for('admin.user_insights', tab='chat', period=period) }}" class="tab-link {% if tab == 'chat' %}active{% endif %}">Chat & Konwersje</a>
</div>
<!-- ============================================================ -->
<!-- TAB: PROBLEMS -->
<!-- ============================================================ -->
{% if tab == 'problems' %}
<div class="stats-grid">
<div class="stat-card error">
<div class="stat-value">{{ data.locked_accounts }}</div>
<div class="stat-label">Zablokowane konta</div>
</div>
<div class="stat-card warning">
<div class="stat-value">{{ data.failed_logins }}</div>
<div class="stat-label">Nieudane logowania</div>
</div>
<div class="stat-card info">
<div class="stat-value">{{ data.password_resets }}</div>
<div class="stat-label">Resety hasła</div>
</div>
<div class="stat-card warning">
<div class="stat-value">{{ data.js_errors }}</div>
<div class="stat-label">Błędy JS</div>
</div>
<div class="stat-card warning">
<div class="stat-value">{{ data.security_alerts }}</div>
<div class="stat-label">Alerty bezpieczeństwa</div>
</div>
<div class="stat-card error">
<div class="stat-value">{{ data.never_logged_in }}</div>
<div class="stat-label">Nigdy nie zalogowani</div>
</div>
</div>
<!-- Proactive alerts -->
{% if data.alerts %}
<div class="section-card">
<h2>Alerty proaktywne
<span class="badge badge-high">{{ data.alerts|length }}</span>
</h2>
{% for alert in data.alerts %}
<div class="alert-card {{ alert.priority }}">
<div class="alert-icon">
{% if alert.type == 'never_logged_in' %}👤{% elif alert.type == 'locked' %}🔒{% elif alert.type == 'reset_no_effect' %}📧{% elif alert.type == 'repeat_resets' %}🔄{% else %}⚠{% endif %}
</div>
<div class="alert-body">
<div class="alert-message">{{ alert.user.name or alert.user.email }}: {{ alert.message }}</div>
<div class="alert-detail">{{ alert.detail }}</div>
</div>
<div class="alert-action" style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap;">
{% if alert.type == 'never_logged_in' %}
<button class="btn-alert-action" onclick="sendWelcome({{ alert.user.id }}, this)" title="Wyślij email aktywacyjny">📧 Wyślij zaproszenie</button>
{% elif alert.type == 'reset_no_effect' or alert.type == 'repeat_resets' %}
<button class="btn-alert-action" onclick="sendReset({{ alert.user.id }}, this)" title="Wyślij nowy reset hasła">🔑 Reset hasła</button>
{% endif %}
<a href="{{ url_for('admin.user_insights_profile', user_id=alert.user.id, ref_tab=tab, ref_period=period) }}" style="font-size: var(--font-size-sm); color: var(--primary);">Szczegóły →</a>
</div>
</div>
{% endfor %}
</div>
{% endif %}
<!-- Remediation effectiveness -->
{% if data.remediation and data.remediation.total > 0 %}
<div class="section-card">
<h2>Skuteczność działań <small style="font-weight: 400; color: var(--text-muted); font-size: var(--font-size-sm);">(30 dni)</small></h2>
<div class="stats-grid" style="margin-bottom: var(--spacing-lg);">
<div class="stat-card success">
<div class="stat-value">{{ data.remediation.success_rate }}%</div>
<div class="stat-label">Skuteczność</div>
</div>
<div class="stat-card success">
<div class="stat-value">{{ data.remediation.resolved }}</div>
<div class="stat-label">Zalogowali się</div>
</div>
<div class="stat-card warning">
<div class="stat-value">{{ data.remediation.pending }}</div>
<div class="stat-label">Oczekują (< 48h)</div>
</div>
<div class="stat-card error">
<div class="stat-value">{{ data.remediation.failed }}</div>
<div class="stat-label">Brak reakcji</div>
</div>
{% if data.remediation.avg_resolution_hours is not none %}
<div class="stat-card info">
<div class="stat-value">
{% if data.remediation.avg_resolution_hours < 1 %}
{{ (data.remediation.avg_resolution_hours * 60)|int }} min
{% elif data.remediation.avg_resolution_hours < 24 %}
{{ data.remediation.avg_resolution_hours }} godz.
{% else %}
{{ (data.remediation.avg_resolution_hours / 24)|round(1) }} dni
{% endif %}
</div>
<div class="stat-label">Śr. czas do loginu</div>
</div>
{% endif %}
</div>
<div class="table-scroll" style="max-height: 350px;">
<table class="data-table">
<thead>
<tr>
<th>Użytkownik</th>
<th>Akcja</th>
<th>Wysłano</th>
<th>Wynik</th>
</tr>
</thead>
<tbody>
{% for r in data.remediation.entries %}
<tr>
<td>
<div class="user-cell">
<div class="user-avatar">{{ r.user.name[0] if r.user.name else '?' }}</div>
<a href="{{ url_for('admin.user_insights_profile', user_id=r.user.id, ref_tab=tab, ref_period=period) }}">{{ r.user.name or r.user.email }}</a>
</div>
</td>
<td>{{ r.action }}</td>
<td>{{ r.sent_at.strftime('%d.%m %H:%M') }}</td>
<td>
{% if r.result == 'resolved' %}
<span class="badge badge-active">{{ r.result_label }}</span>
{% elif r.result == 'pending' %}
<span class="badge badge-medium">{{ r.result_label }}</span>
{% else %}
<span class="badge badge-high">{{ r.result_label }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- Active problems -->
<div class="section-card">
<h2>Aktywne problemy
<span class="badge {% if data.active_problems|length > 10 %}badge-critical{% elif data.active_problems|length > 0 %}badge-high{% else %}badge-ok{% endif %}">
{{ data.active_problems|length }}
</span>
</h2>
{% if data.active_problems %}
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th>Użytkownik</th>
<th>Problem Score</th>
<th>Nieudane logowania</th>
<th>Resety hasła</th>
<th>Alerty</th>
<th>Błędy JS</th>
<th>Wolne strony</th>
<th>🔒</th>
<th>Ostatni login</th>
<th>Akcja</th>
</tr>
</thead>
<tbody>
{% for p in data.active_problems %}
<tr>
<td>
<div class="user-cell">
<div class="user-avatar">{{ p.user.name[0] if p.user.name else '?' }}</div>
<a href="{{ url_for('admin.user_insights_profile', user_id=p.user.id, ref_tab=tab, ref_period=period) }}">{{ p.user.name or p.user.email }}</a>
</div>
</td>
<td>
<span class="badge {% if p.score >= 51 %}badge-critical{% elif p.score >= 21 %}badge-high{% elif p.score >= 1 %}badge-medium{% else %}badge-ok{% endif %}">
{{ p.score }}
</span>
</td>
<td>{{ p.failed_logins }}</td>
<td>{{ p.password_resets }}</td>
<td>{{ p.security_alerts }}</td>
<td>{{ p.js_errors }}</td>
<td>{{ p.slow_pages }}</td>
<td>{% if p.is_locked %}<span class="badge badge-critical">Tak</span>{% endif %}</td>
<td>{{ p.last_login.strftime('%d.%m %H:%M') if p.last_login else 'Nigdy' }}</td>
<td>
<button class="btn-reset-pw" data-uid="{{ p.user.id }}" data-name="{{ p.user.name or p.user.email }}" data-email="{{ p.user.email }}">📧 Reset</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div style="text-align: center; padding: var(--spacing-xl); color: var(--text-muted);">
<p>Brak aktywnych problemów w wybranym okresie.</p>
</div>
{% endif %}
</div>
<!-- Resolved problems (collapsible) -->
{% if data.resolved_problems %}
<div class="section-card" style="opacity: 0.8;">
<details>
<summary style="cursor: pointer; display: flex; align-items: center; gap: var(--spacing-sm); font-size: var(--font-size-lg); font-weight: 600; padding: var(--spacing-xs) 0; list-style: none;">
<span style="transition: transform 0.2s; display: inline-block;">&#9654;</span>
Rozwiązane problemy
<span class="badge badge-ok">{{ data.resolved_problems|length }}</span>
<span style="font-size: var(--font-size-xs); font-weight: 400; color: var(--text-muted);">— zalogowali się po problemach</span>
</summary>
<div class="table-scroll" style="margin-top: var(--spacing-md);">
<table class="data-table">
<thead>
<tr>
<th>Użytkownik</th>
<th>Było problemów</th>
<th>Nieudane logowania</th>
<th>Resety hasła</th>
<th>Alerty</th>
<th>Rozwiązano</th>
</tr>
</thead>
<tbody>
{% for p in data.resolved_problems %}
<tr>
<td>
<div class="user-cell">
<div class="user-avatar" style="background: var(--success);">{{ p.user.name[0] if p.user.name else '?' }}</div>
<a href="{{ url_for('admin.user_insights_profile', user_id=p.user.id, ref_tab=tab, ref_period=period) }}">{{ p.user.name or p.user.email }}</a>
</div>
</td>
<td><span class="badge badge-ok">{{ p.score }}</span></td>
<td>{{ p.failed_logins }}</td>
<td>{{ p.password_resets }}</td>
<td>{{ p.security_alerts }}</td>
<td><span class="badge badge-active">Zalogowano {{ p.resolved_at.strftime('%d.%m %H:%M') }}</span></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</details>
</div>
{% endif %}
<!-- ============================================================ -->
<!-- TAB: ENGAGEMENT -->
<!-- ============================================================ -->
{% elif tab == 'engagement' %}
<div class="stats-grid">
<div class="stat-card success">
<div class="stat-value">{{ data.active_7d }}</div>
<div class="stat-label">Aktywni (ostatnie 7 dni)</div>
</div>
<div class="stat-card info">
<div class="stat-value">{{ data.new_this_month }}</div>
<div class="stat-label">Nowi (ten miesiąc)</div>
</div>
<div class="stat-card warning">
<div class="stat-value">{{ data.at_risk }}</div>
<div class="stat-label">Nie logowali się 8-30 dni</div>
</div>
<div class="stat-card error">
<div class="stat-value">{{ data.dormant }}</div>
<div class="stat-label">Nie logowali się 30+ dni</div>
</div>
</div>
<div class="section-card">
<h2>Aktywność członków</h2>
{% if data.engagement_list %}
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th>#</th>
<th>Członek</th>
<th style="min-width: 140px; cursor: help;" title="Wskaźnik aktywności z ostatnich 30 dni.&#10;Uwzględnia: sesje, odsłony stron, kliknięcia, czas na stronie, konwersje (RSVP, kontakt), wyszukiwania.&#10;Najedź na pasek, żeby zobaczyć szczegóły.">Aktywność ⓘ</th>
<th>Sesje</th>
<th>Odsłony</th>
<th>Ostatnie logowanie</th>
</tr>
</thead>
<tbody>
{% for e in data.engagement_list %}
<tr{% if e.score == 0 %} style="opacity: 0.5;"{% endif %}>
<td style="font-weight: 600; color: var(--text-muted); width: 40px;">{{ loop.index }}</td>
<td>
<div class="user-cell">
<div class="user-avatar">{{ e.user.name[0] if e.user.name else '?' }}</div>
<a href="{{ url_for('admin.user_insights_profile', user_id=e.user.id, ref_tab=tab, ref_period=period) }}">{{ e.user.name or e.user.email }}</a>
</div>
</td>
<td title="Ostatnie 30 dni:&#10;{{ e.detail.sessions_30d }} sesji (×3) + {{ e.detail.pages_30d }} stron (×1) + {{ e.detail.clicks_30d }} kliknięć (×0.5) + {{ e.detail.minutes_30d }} min (×2) + {{ e.detail.conversions_30d }} konwersji (×10) + {{ e.detail.searches_30d }} wyszukiwań (×2)&#10;= {{ e.detail.raw }} pkt → skala log. {{ e.score }}/100">
<div class="engagement-bar-wrap">
{% set bar_color = '#16a34a' if e.score >= 40 else '#f59e0b' if e.score >= 10 else '#d1d5db' %}
<div class="engagement-bar" style="width: {{ [e.score, 100]|min }}%; background: {{ bar_color }};"></div>
</div>
</td>
<td>{{ e.sessions }}</td>
<td>{{ e.page_views }}</td>
<td style="white-space: nowrap; color: {% if e.last_login == 'Nigdy' %}var(--danger){% elif e.days_since is not none and e.days_since > 30 %}var(--text-muted){% else %}var(--text-secondary){% endif %};">
{{ e.last_login }}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div style="text-align: center; padding: var(--spacing-xl); color: var(--text-muted);">
<p>Brak danych o aktywności członków.</p>
</div>
{% endif %}
</div>
<!-- ============================================================ -->
<!-- TAB: PAGE MAP -->
<!-- ============================================================ -->
{% elif tab == 'pages' %}
<!-- Section heatmap -->
<div class="section-card">
<h2>Popularność sekcji</h2>
<div class="sections-grid">
{% for s in data.sections %}
<div class="section-tile" style="background: rgba(34, 197, 94, {{ s.intensity / 100 * 0.3 + 0.05 }});">
<h3>{{ s.name }}</h3>
<div class="stat-value" style="font-size: var(--font-size-lg);">{{ s.views }}</div>
<div class="metric">{{ s.unique_users }} unikalnych &middot; {{ s.avg_time_fmt }} śr.</div>
</div>
{% endfor %}
</div>
</div>
<!-- Top pages -->
<div class="section-card">
<h2>Top 50 stron</h2>
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th>Strona</th>
<th style="width: 200px;">Odsłony</th>
<th>Unikalni</th>
<th>Śr. czas</th>
<th>Śr. scroll</th>
<th>Śr. ładowanie</th>
</tr>
</thead>
<tbody>
{% for p in data.top_pages %}
<tr>
<td style="max-width: 300px; word-break: break-all; font-size: var(--font-size-xs);" title="{{ p.path }}">{{ p.label }}</td>
<td>
<div style="display: flex; align-items: center; gap: var(--spacing-sm);">
<div class="bar-container" style="flex: 1;">
<div class="bar-fill primary" style="width: {{ p.bar_pct }}%;"></div>
</div>
<span style="font-weight: 600; min-width: 40px; text-align: right;">{{ p.views }}</span>
</div>
</td>
<td>{{ p.unique_users }}</td>
<td>{{ p.avg_time_fmt }}</td>
<td>{{ p.avg_scroll }}%</td>
<td>
<span {% if p.avg_load > 3000 %}style="color: var(--error); font-weight: 600;"{% elif p.avg_load > 1500 %}style="color: #d97706;"{% endif %}>
{{ p.avg_load }}ms
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Ignored pages -->
{% if data.ignored_pages %}
<div class="section-card">
<h2>Nieużywane strony <span class="badge badge-low">< 5 odsłon/30d</span></h2>
<div style="display: flex; flex-wrap: wrap; gap: var(--spacing-sm);">
{% for p in data.ignored_pages %}
<span style="padding: 4px 10px; background: var(--background); border-radius: var(--radius-sm); font-size: var(--font-size-xs); font-family: monospace; color: var(--text-muted);">
{{ p.path }} ({{ p.views }})
</span>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Wyszukiwania -->
<div class="two-columns">
<div class="section-card">
<h2>Top wyszukiwania</h2>
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th>Fraza</th>
<th>Liczba</th>
<th>Śr. wyników</th>
</tr>
</thead>
<tbody>
{% for search in data.top_searches %}
<tr>
<td style="font-weight: 500;">{{ search.query_normalized }}</td>
<td>{{ search.count }}</td>
<td>{{ search.avg_results|round(1) if search.avg_results else 0 }}</td>
</tr>
{% else %}
<tr>
<td colspan="3" style="text-align: center; color: var(--text-muted); padding: var(--spacing-lg);">Brak danych wyszukiwań</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="section-card">
<h2>Wyszukiwania bez wyników</h2>
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th>Fraza</th>
<th>Liczba</th>
</tr>
</thead>
<tbody>
{% for search in data.searches_no_results %}
<tr>
<td style="color: var(--error); font-weight: 500;">{{ search.query_normalized }}</td>
<td>{{ search.count }}</td>
</tr>
{% else %}
<tr>
<td colspan="2" style="text-align: center; color: var(--text-muted); padding: var(--spacing-lg);">Wszystkie wyszukiwania miały wyniki</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<!-- Search Effectiveness -->
<div class="section-card">
<h2>Skuteczność wyszukiwarki</h2>
<div class="stats-grid" style="grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); margin-bottom: var(--spacing-lg);">
<div class="stat-card info">
<div class="stat-value" style="font-size: var(--font-size-xl);">{{ data.search_effectiveness.total }}</div>
<div class="stat-label">Wyszukiwań</div>
</div>
<div class="stat-card {% if data.search_effectiveness.ctr >= 40 %}success{% elif data.search_effectiveness.ctr >= 20 %}warning{% else %}error{% endif %}">
<div class="stat-value" style="font-size: var(--font-size-xl);">{{ data.search_effectiveness.ctr }}%</div>
<div class="stat-label">Klikalność (CTR)</div>
</div>
<div class="stat-card info">
<div class="stat-value" style="font-size: var(--font-size-xl);">{{ data.search_effectiveness.avg_position or '—' }}</div>
<div class="stat-label">Śr. pozycja kliknięcia</div>
</div>
<div class="stat-card {% if data.search_effectiveness.ctr < 20 %}error{% else %}success{% endif %}">
<div class="stat-value" style="font-size: var(--font-size-xl);">{{ data.search_effectiveness.with_click }}</div>
<div class="stat-label">Z kliknięciem</div>
</div>
</div>
{% if data.search_effectiveness.top_clicked %}
<h3 style="font-size: var(--font-size-sm); font-weight: 600; margin-bottom: var(--spacing-sm); color: var(--text-secondary);">Najczęściej wybierane firmy z wyszukiwarki</h3>
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
{% for c in data.search_effectiveness.top_clicked %}
<span style="padding: 4px 12px; background: var(--background); border-radius: var(--radius); font-size: var(--font-size-sm);">
<strong>{{ c.name }}</strong> <span style="color: var(--text-muted);">({{ c.clicks }}x)</span>
</span>
{% endfor %}
</div>
{% endif %}
</div>
<!-- Content Engagement (Open Rates) -->
<div class="section-card">
<h2>Zasięg treści <small style="font-weight: 400; color: var(--text-muted); font-size: var(--font-size-sm);">(30 dni)</small></h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--spacing-lg);">
{% set content_types = [
('Aktualności', data.content_engagement.announcements, '#3b82f6'),
('Forum', data.content_engagement.forum, '#8b5cf6'),
('Tablica B2B', data.content_engagement.classifieds, '#10b981')
] %}
{% for label, stats, color in content_types %}
<div style="background: var(--background); padding: var(--spacing-lg); border-radius: var(--radius-lg); border-left: 4px solid {{ color }};">
<h3 style="font-size: var(--font-size-base); font-weight: 600; margin-bottom: var(--spacing-md);">{{ label }}</h3>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-sm); margin-bottom: var(--spacing-md);">
<div>
<div style="font-size: var(--font-size-2xl); font-weight: 700;">{{ stats.published }}</div>
<div style="font-size: var(--font-size-xs); color: var(--text-muted);">nowych (30 dni)</div>
</div>
<div>
<div style="font-size: var(--font-size-2xl); font-weight: 700;">{{ stats.read_by }}</div>
<div style="font-size: var(--font-size-xs); color: var(--text-muted);">aktywnych czytelników</div>
</div>
</div>
<div style="margin-bottom: 4px; display: flex; justify-content: space-between;">
<span style="font-size: var(--font-size-xs); color: var(--text-secondary);" title="% aktywnych członków, którzy czytali">Zasięg</span>
<span style="font-size: var(--font-size-sm); font-weight: 600; color: {{ '#16a34a' if stats.open_rate >= 50 else ('#f59e0b' if stats.open_rate >= 25 else '#ef4444') }};">{{ stats.open_rate }}%</span>
</div>
<div style="height: 8px; background: #e2e8f0; border-radius: 4px; overflow: hidden;">
<div style="height: 100%; width: {{ stats.open_rate }}%; background: {{ color }}; border-radius: 4px;"></div>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- ============================================================ -->
<!-- TAB: PATHS -->
<!-- ============================================================ -->
{% elif tab == 'paths' %}
<div class="two-columns">
<!-- Entry pages -->
<div class="section-card">
<h2>Strony wejściowe (top 10)</h2>
{% for p in data.entry_pages %}
<div class="bar-chart-row">
<div class="bar-chart-label" style="width: 200px; min-width: 200px; font-size: var(--font-size-xs); word-break: break-all;" title="{{ p.path }}">{{ p.label }}</div>
<div class="bar-chart-bar">
<div class="bar-chart-fill" style="width: {{ p.bar_pct }}%; background: #22c55e;">
<span>{{ p.count }}</span>
</div>
</div>
</div>
{% endfor %}
</div>
<!-- Exit pages -->
<div class="section-card">
<h2>Strony wyjściowe (top 10)</h2>
{% for p in data.exit_pages %}
<div class="bar-chart-row">
<div class="bar-chart-label" style="width: 200px; min-width: 200px; font-size: var(--font-size-xs); word-break: break-all;" title="{{ p.path }}">{{ p.label }}</div>
<div class="bar-chart-bar">
<div class="bar-chart-fill" style="width: {{ p.bar_pct }}%; background: #ef4444;">
<span>{{ p.count }}</span>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- Top transitions -->
<div class="section-card">
<h2>Popularne przejścia (top 30)</h2>
<div class="table-scroll" style="max-height: 400px;">
<table class="data-table">
<thead>
<tr>
<th>Ze strony</th>
<th style="width: 40px;"></th>
<th>Na stronę</th>
<th style="width: 80px;">Liczba</th>
</tr>
</thead>
<tbody>
{% for t in data.transitions %}
<tr>
<td style="font-size: var(--font-size-xs);" title="{{ t.from }}">{{ t.from_label }}</td>
<td class="transition-arrow" style="text-align: center;"></td>
<td style="font-size: var(--font-size-xs);" title="{{ t.to }}">{{ t.to_label }}</td>
<td style="font-weight: 600; text-align: right;">{{ t.count }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="two-columns">
<!-- Drop-off pages -->
<div class="section-card">
<h2>Strony z odpływem</h2>
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th>Strona</th>
<th>Odsłony</th>
<th>Wyjścia</th>
<th>Wsp. wyjścia</th>
</tr>
</thead>
<tbody>
{% for d in data.dropoff %}
<tr>
<td style="font-size: var(--font-size-xs);" title="{{ d.path }}">{{ d.label }}</td>
<td>{{ d.views }}</td>
<td>{{ d.exits }}</td>
<td>
<span {% if d.exit_rate > 70 %}style="color: var(--error); font-weight: 600;"{% elif d.exit_rate > 50 %}style="color: #d97706;"{% endif %}>
{{ d.exit_rate }}%
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Session length distribution -->
<div class="section-card">
<h2>Rozkład długości sesji</h2>
{% for s in data.session_lengths %}
<div class="bar-chart-row">
<div class="bar-chart-label">{{ s.bucket }}</div>
<div class="bar-chart-bar">
<div class="bar-chart-fill" style="width: {{ s.bar_pct }}%;">
<span>{{ s.count }}</span>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
<!-- ============================================================ -->
<!-- TAB: OVERVIEW -->
<!-- ============================================================ -->
{% elif tab == 'overview' %}
<!-- KPI Stat Cards with Trends -->
<div class="stats-grid">
<!-- Active members with goal bar -->
<div class="stat-card success">
<div class="stat-value">{{ data.kpi.active_users }}<span style="font-size: var(--font-size-sm); color: var(--text-muted); font-weight: 400;">/{{ data.kpi.total_members }}</span>
{% if data.kpi.active_users_trend is not none %}
<span style="font-size: var(--font-size-sm); margin-left: 6px; color: {{ 'var(--success)' if data.kpi.active_users_trend >= 0 else 'var(--error)' }}">
{{ '▲' if data.kpi.active_users_trend > 0 else ('▼' if data.kpi.active_users_trend < 0 else '') }} {{ data.kpi.active_users_trend|abs }}%
</span>
{% endif %}
</div>
<div class="stat-label">Aktywni członkowie ({{ data.kpi.active_pct }}%)</div>
<div style="margin-top: 4px; height: 4px; background: #e2e8f0; border-radius: 2px;">
<div style="height: 100%; width: {{ data.kpi.active_pct }}%; background: var(--success); border-radius: 2px;"></div>
</div>
</div>
{% for kpi_item in [
('sessions', 'Sesje', 'info', false),
('pageviews', 'Odsłony', 'info', false),
('bounce_rate', 'Współczynnik odrzuceń', 'info', true),
] %}
{% set key = kpi_item[0] %}
{% set label = kpi_item[1] %}
{% set is_bounce = kpi_item[3] %}
{% set val = data.kpi[key] %}
{% set trend = data.kpi[key ~ '_trend'] %}
{% if is_bounce %}
<div class="stat-card {% if val > 85 %}warning{% else %}success{% endif %}" title="Na portalu członkowskim 70-85% jest normalne — użytkownicy sprawdzają jedną informację i wychodzą">
{% else %}
<div class="stat-card {{ kpi_item[2] }}">
{% endif %}
<div class="stat-value">{{ val }}{% if is_bounce %}%{% endif %}
{% if trend is not none %}
<span style="font-size: var(--font-size-sm); margin-left: 6px; {% if is_bounce %}color: {{ 'var(--success)' if trend <= 0 else 'var(--error)' }}{% else %}color: {{ 'var(--success)' if trend >= 0 else 'var(--error)' }}{% endif %}">
{{ '▲' if trend > 0 else ('▼' if trend < 0 else '') }} {{ trend|abs }}%
</span>
{% endif %}
</div>
<div class="stat-label">{{ label }}</div>
</div>
{% endfor %}
</div>
<!-- Filter -->
<div style="margin-bottom: var(--spacing-lg);">
<div class="filter-group">
<a href="{{ url_for('admin.user_insights', tab='overview', period=period, filter='all') }}" class="filter-btn {% if data.filter_type == 'all' %}active{% endif %}">Wszyscy</a>
<a href="{{ url_for('admin.user_insights', tab='overview', period=period, filter='logged') }}" class="filter-btn {% if data.filter_type == 'logged' %}active{% endif %}">Zalogowani</a>
<a href="{{ url_for('admin.user_insights', tab='overview', period=period, filter='anonymous') }}" class="filter-btn {% if data.filter_type == 'anonymous' %}active{% endif %}">Anonimowi</a>
</div>
</div>
<!-- Sessions + Page Views chart -->
<div class="section-card">
<h2>Sesje i odsłony <small style="font-weight: 400; color: var(--text-muted); font-size: var(--font-size-sm);">(zawsze ostatnie 30 dni)</small></h2>
<div class="chart-container">
<canvas id="sessionsChart"></canvas>
</div>
</div>
<div class="two-columns">
<!-- Logged vs Anonymous doughnut -->
<div class="section-card">
<h2>Zalogowani vs Anonimowi</h2>
<div class="chart-container" style="height: 250px;">
<canvas id="authChart"></canvas>
</div>
</div>
<!-- Devices stacked bar -->
<div class="section-card">
<h2>Urządzenia (tygodniowo)</h2>
<div class="chart-container" style="height: 250px;">
<canvas id="devicesChart"></canvas>
</div>
</div>
</div>
<!-- Hourly heatmap -->
<div class="section-card">
<h2>Aktywność godzinowa (30 dni)</h2>
<div style="overflow-x: auto;">
<table class="heatmap-table">
<thead>
<tr>
<th></th>
{% for h in range(24) %}
<th>{{ h }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for row in data.heatmap %}
<tr>
<td class="heatmap-label">{{ row.name }}</td>
{% for cell in row.hours %}
<td title="{{ row.name }} {{ loop.index0 }}:00 — {{ cell.count }} sesji">
<div class="heatmap-cell" style="background: rgba(34, 197, 94, {{ cell.intensity / 100 * 0.8 + 0.05 }}); display: inline-block;"></div>
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="two-columns">
<!-- Feature Adoption -->
<div class="section-card">
<h2>Adopcja funkcji <small style="font-weight: 400; color: var(--text-muted); font-size: var(--font-size-sm);">(30 dni, zalogowani)</small></h2>
{% for f in data.adoption %}
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 10px;">
<div style="width: 120px; font-size: var(--font-size-sm); font-weight: 500;">{{ f.name }}</div>
<div style="flex: 1; height: 20px; background: #e2e8f0; border-radius: 4px; overflow: hidden;">
<div style="height: 100%; width: {{ f.pct }}%; background: {{ '#10b981' if f.pct >= 30 else ('#f59e0b' if f.pct >= 10 else '#ef4444') }}; border-radius: 4px; display: flex; align-items: center; padding-left: 6px;">
{% if f.pct >= 8 %}<span style="font-size: 11px; color: white; font-weight: 600;">{{ f.pct }}%</span>{% endif %}
</div>
</div>
<div style="width: 50px; font-size: var(--font-size-xs); color: var(--text-muted); text-align: right;">{{ f.users }} os.</div>
</div>
{% endfor %}
</div>
<!-- Event Comparison -->
<div class="section-card">
<h2>Porównanie wydarzeń <small style="font-weight: 400; color: var(--text-muted); font-size: var(--font-size-sm);">(ostatnie 60 dni)</small></h2>
{% if data.events_comparison %}
<div class="table-scroll" style="max-height: 350px;">
<table class="data-table" style="font-size: var(--font-size-sm);">
<thead>
<tr>
<th>Wydarzenie</th>
<th style="text-align: center;">Data</th>
<th style="text-align: center;" title="Wyświetlenia strony wydarzenia">Odsłony</th>
<th style="text-align: center;" title="Unikalni odwiedzający">Osoby</th>
<th style="text-align: center;" title="Potwierdzone zapisy">RSVP</th>
</tr>
</thead>
<tbody>
{% for e in data.events_comparison %}
<tr>
<td style="max-width: 200px;">{{ e.title }}</td>
<td style="text-align: center; white-space: nowrap;">{{ e.date }}</td>
<td style="text-align: center;">{{ e.views }}</td>
<td style="text-align: center;">{{ e.unique_viewers }}</td>
<td style="text-align: center; font-weight: 600;">{{ e.rsvp }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p style="color: var(--text-muted); text-align: center; padding: var(--spacing-lg);">Brak wydarzeń w ostatnich 60 dniach</p>
{% endif %}
</div>
</div>
<!-- Referrer Sources + Company Popularity -->
<div class="two-columns">
<!-- Referrer Sources -->
<div class="section-card">
<h2>Skąd przychodzą użytkownicy <small style="font-weight: 400; color: var(--text-muted); font-size: var(--font-size-sm);">(30 dni)</small></h2>
{% if data.referrer_sources %}
{% for r in data.referrer_sources %}
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 8px;">
<div style="width: 110px; font-size: var(--font-size-sm); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;" title="{{ r.domain }}">{{ r.domain }}</div>
<div style="flex: 1; height: 18px; background: #e2e8f0; border-radius: 4px; overflow: hidden;">
<div style="height: 100%; width: {{ r.bar_pct }}%; background: #6366f1; border-radius: 4px;"></div>
</div>
<div style="width: 45px; font-size: var(--font-size-xs); color: var(--text-muted); text-align: right;">{{ r.count }}</div>
</div>
{% endfor %}
{% else %}
<p style="color: var(--text-muted); text-align: center; padding: var(--spacing-lg);">Brak danych o źródłach ruchu</p>
{% endif %}
</div>
<!-- Company Profile Popularity -->
<div class="section-card">
<h2>Popularność profili firm <small style="font-weight: 400; color: var(--text-muted); font-size: var(--font-size-sm);">(30 dni)</small></h2>
{% if data.company_popularity %}
<div class="table-scroll" style="max-height: 350px;">
<table class="data-table" style="font-size: var(--font-size-sm);">
<thead>
<tr>
<th>Firma</th>
<th style="text-align: center;">Odsłony</th>
<th style="text-align: center;">Osoby</th>
</tr>
</thead>
<tbody>
{% for c in data.company_popularity %}
<tr>
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">
<a href="{{ c.path }}" target="_blank" style="color: var(--primary); text-decoration: none;">{{ c.name }}</a>
</td>
<td style="text-align: center;">
<div style="display: flex; align-items: center; gap: 6px; justify-content: center;">
<div style="width: 60px; height: 12px; background: #e2e8f0; border-radius: 3px; overflow: hidden;">
<div style="height: 100%; width: {{ (c.views / data.max_company_views * 100)|int }}%; background: #10b981; border-radius: 3px;"></div>
</div>
{{ c.views }}
</div>
</td>
<td style="text-align: center;">{{ c.unique_viewers }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p style="color: var(--text-muted); text-align: center; padding: var(--spacing-lg);">Brak danych o profilach firm</p>
{% endif %}
</div>
</div>
<!-- Companies needing attention -->
{% if data.companies_attention %}
<div class="section-card">
<h2>Firmy wymagające uwagi
<span class="badge badge-high">{{ data.companies_attention_total }}</span>
</h2>
<div class="table-scroll" style="max-height: 400px;">
<table class="data-table" style="font-size: var(--font-size-sm);">
<thead>
<tr>
<th>Firma</th>
<th>Braki</th>
<th style="text-align: center;">Jakość</th>
<th></th>
</tr>
</thead>
<tbody>
{% for co in data.companies_attention %}
<tr>
<td style="font-weight: 500; max-width: 200px;">{{ co.name }}</td>
<td>
{% for issue in co.issues %}
<span class="issue-tag">{{ issue }}</span>
{% endfor %}
</td>
<td style="text-align: center;">
<span class="badge {% if co.quality == 'complete' %}badge-ok{% elif co.quality == 'enhanced' %}badge-medium{% else %}badge-high{% endif %}">{{ co.quality }}</span>
</td>
<td><a href="/company/{{ co.id }}/edit" style="font-size: var(--font-size-xs); color: var(--primary);">Edytuj →</a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endif %}
<!-- ============================================================ -->
<!-- TAB: CHAT & CONVERSIONS -->
<!-- ============================================================ -->
{% elif tab == 'chat' %}
<!-- Stats Grid -->
<div class="stats-grid">
<div class="stat-card success">
<div class="stat-value">{{ data.total_conversations }}</div>
<div class="stat-label">Rozmów ogółem</div>
</div>
<div class="stat-card info">
<div class="stat-value">{{ data.period_conversations }}</div>
<div class="stat-label">Rozmów (okres)</div>
</div>
<div class="stat-card success">
<div class="stat-value">{{ data.positive_feedback }}</div>
<div class="stat-label">Pozytywnych ocen</div>
</div>
<div class="stat-card error">
<div class="stat-value">{{ data.negative_feedback }}</div>
<div class="stat-label">Negatywnych ocen</div>
</div>
<div class="stat-card {% if data.satisfaction_rate >= 70 %}success{% elif data.satisfaction_rate >= 40 %}warning{% else %}error{% endif %}">
<div class="stat-value">{{ data.satisfaction_rate }}%</div>
<div class="stat-label">Satysfakcja</div>
</div>
</div>
<div class="two-columns">
<!-- Query Statistics (Anonymized) -->
<div class="section-card">
<h2>Statystyki zapytań <span style="font-size: var(--font-size-xs); color: var(--text-muted);">dane zanonimizowane</span></h2>
<div class="stats-grid" style="grid-template-columns: repeat(2, 1fr);">
<div class="stat-card">
<div class="stat-value" style="font-size: var(--font-size-xl);">{{ data.query_stats.total_today }}</div>
<div class="stat-label">Zapytań dzisiaj</div>
</div>
<div class="stat-card">
<div class="stat-value" style="font-size: var(--font-size-xl);">{{ data.query_stats.avg_length|int }}</div>
<div class="stat-label">Śr. długość (znaki)</div>
</div>
<div class="stat-card">
<div class="stat-value" style="font-size: var(--font-size-xl);">{{ data.query_stats.queries_with_company }}</div>
<div class="stat-label">O firmach</div>
</div>
<div class="stat-card">
<div class="stat-value" style="font-size: var(--font-size-xl);">{{ data.query_stats.queries_with_contact }}</div>
<div class="stat-label">O kontaktach</div>
</div>
</div>
</div>
<!-- Conversion Events -->
<div class="section-card">
<h2>Konwersje z chatu</h2>
{% if data.conversion_stats %}
<div class="stats-grid" style="grid-template-columns: repeat(2, 1fr);">
{% for event_type, count in data.conversion_stats.items() %}
<div style="background: var(--background); padding: var(--spacing-md); border-radius: var(--radius); text-align: center;">
<div style="font-size: var(--font-size-xl); font-weight: 700; color: var(--primary);">{{ count }}</div>
<div style="font-size: var(--font-size-sm); color: var(--text-secondary);">{{ event_type }}</div>
</div>
{% endfor %}
</div>
{% else %}
<div style="text-align: center; padding: var(--spacing-xl); color: var(--text-muted);">
<p>Brak konwersji z chatu w wybranym okresie</p>
</div>
{% endif %}
</div>
</div>
<!-- Recent Feedback -->
{% if data.recent_feedback %}
<div class="section-card">
<h2>Odpowiedzi z oceną</h2>
<div class="table-scroll" style="max-height: 400px;">
{% for msg in data.recent_feedback %}
<div style="padding: var(--spacing-md); border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: flex-start; gap: var(--spacing-md);">
<div style="flex: 1;">
<span class="badge {% if msg.feedback_rating == 2 %}badge-active{% else %}badge-high{% endif %}">
{% if msg.feedback_rating == 2 %}Pomocne{% else %}Do poprawy{% endif %}
</span>
<div style="margin-top: var(--spacing-sm); font-size: var(--font-size-sm); color: var(--text-secondary); max-height: 60px; overflow: hidden;">
{{ msg.content[:200] }}{% if msg.content|length > 200 %}...{% endif %}
</div>
{% if msg.feedback_comment %}
<div style="margin-top: var(--spacing-xs); font-size: var(--font-size-xs);"><strong>Komentarz:</strong> {{ msg.feedback_comment }}</div>
{% endif %}
</div>
<div style="font-size: var(--font-size-xs); color: var(--text-muted); white-space: nowrap;">
{{ msg.feedback_at.strftime('%d.%m %H:%M') if msg.feedback_at else '' }}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Recent Queries (Anonymized) -->
{% if data.recent_queries %}
<div class="section-card">
<h2>Ostatnia aktywność <span style="font-size: var(--font-size-xs); color: var(--text-muted);">zanonimizowana</span></h2>
<div class="table-scroll" style="max-height: 350px;">
{% for query in data.recent_queries %}
<div style="padding: var(--spacing-sm) var(--spacing-md); border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; align-items: center; font-size: var(--font-size-sm);">
<div>
Zapytanie ({{ query.length }} znaków)
{% if query.has_company_mention %}
<span class="badge badge-low" style="margin-left: 4px;">FIRMA</span>
{% endif %}
{% if query.has_contact_request %}
<span class="badge badge-active" style="margin-left: 4px;">KONTAKT</span>
{% endif %}
</div>
<div style="color: var(--text-muted); font-size: var(--font-size-xs);">
{{ query.created_at.strftime('%d.%m %H:%M') }}
</div>
</div>
{% endfor %}
</div>
<div style="margin-top: var(--spacing-md); padding: var(--spacing-md); background: var(--background); border-radius: var(--radius); font-size: var(--font-size-xs); color: var(--text-secondary);">
Treść zapytań użytkowników nie jest wyświetlana w panelu admina. Widoczne są tylko zanonimizowane statystyki.
</div>
</div>
{% endif %}
<!-- UTM Sources -->
{% if data.utm_sources %}
<div class="section-card">
<h2>Źródła ruchu (UTM)</h2>
<div style="display: flex; flex-direction: column; gap: var(--spacing-sm);">
{% for source, count in data.utm_sources.items() %}
<div style="display: flex; justify-content: space-between; align-items: center; padding: var(--spacing-xs) 0; border-bottom: 1px solid var(--border);">
<span style="font-weight: 500;">{{ source }}</span>
<span class="badge badge-low">{{ count }}</span>
</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endif %}
</div>
{% endblock %}
{% block extra_js %}
{% if tab == 'overview' %}
// Sessions + Page Views line chart
const chartData = {{ data.chart_data|tojson|safe }};
const sessionsCtx = document.getElementById('sessionsChart').getContext('2d');
new Chart(sessionsCtx, {
type: 'line',
data: {
labels: chartData.labels,
datasets: [{
label: 'Sesje',
data: chartData.sessions,
borderColor: '#6366f1',
backgroundColor: 'rgba(99, 102, 241, 0.1)',
fill: true,
tension: 0.4,
yAxisID: 'y'
}, {
label: 'Odsłony',
data: chartData.pageviews,
borderColor: '#22c55e',
backgroundColor: 'rgba(34, 197, 94, 0.1)',
fill: true,
tension: 0.4,
yAxisID: 'y1'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
interaction: { mode: 'index', intersect: false },
plugins: { legend: { position: 'top' } },
scales: {
x: { grid: { display: false } },
y: { type: 'linear', display: true, position: 'left', beginAtZero: true, title: { display: true, text: 'Sesje' } },
y1: { type: 'linear', display: true, position: 'right', beginAtZero: true, grid: { drawOnChartArea: false }, title: { display: true, text: 'Odsłony' } }
}
}
});
// Auth doughnut
const authData = {{ data.logged_vs_anon|tojson|safe }};
const authCtx = document.getElementById('authChart').getContext('2d');
const authTotal = authData.logged + authData.anonymous;
const authPctLogged = authTotal > 0 ? Math.round(authData.logged / authTotal * 100) : 0;
const authPctAnon = authTotal > 0 ? Math.round(authData.anonymous / authTotal * 100) : 0;
new Chart(authCtx, {
type: 'doughnut',
data: {
labels: ['Zalogowani (' + authPctLogged + '%)', 'Anonimowi (' + authPctAnon + '%)'],
datasets: [{
data: [authData.logged, authData.anonymous],
backgroundColor: ['#6366f1', '#d1d5db'],
borderWidth: 0
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'bottom' },
tooltip: {
callbacks: {
label: function(ctx) {
const total = ctx.dataset.data.reduce((a, b) => a + b, 0);
const pct = total > 0 ? Math.round(ctx.raw / total * 100) : 0;
return ctx.label + ': ' + ctx.raw + ' (' + pct + '%)';
}
}
}
}
}
});
// Devices stacked bar
const devData = {{ data.devices|tojson|safe }};
const devCtx = document.getElementById('devicesChart').getContext('2d');
new Chart(devCtx, {
type: 'bar',
data: {
labels: devData.labels,
datasets: [{
label: 'Desktop',
data: devData.desktop,
backgroundColor: '#6366f1'
}, {
label: 'Mobile',
data: devData.mobile,
backgroundColor: '#22c55e'
}, {
label: 'Tablet',
data: devData.tablet,
backgroundColor: '#f59e0b'
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { position: 'top' } },
scales: {
x: { stacked: true, grid: { display: false } },
y: { stacked: true, beginAtZero: true }
}
}
});
{% endif %}
// Password reset buttons
document.querySelectorAll('.btn-reset-pw').forEach(function(btn) {
btn.addEventListener('click', async function() {
const uid = this.dataset.uid;
const name = this.dataset.name;
const email = this.dataset.email;
if (!confirm('Wysłać reset hasła do ' + name + ' (' + email + ')?')) return;
this.disabled = true;
this.textContent = '⏳...';
try {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
const resp = await fetch('/admin/analytics/send-reset/' + uid, {
method: 'POST',
headers: {'X-CSRFToken': csrfToken}
});
const data = await resp.json();
if (data.success) {
this.textContent = '✓ Wysłano';
this.classList.add('sent');
} else {
this.textContent = '✗ ' + (data.error || 'Błąd');
this.classList.add('error');
this.disabled = false;
}
} catch (e) {
this.textContent = '✗ Błąd';
this.classList.add('error');
this.disabled = false;
}
});
});
// Alert action buttons
async function sendWelcome(userId, btn) {
btn.disabled = true;
btn.textContent = 'Wysyłanie...';
try {
const resp = await fetch('/admin/analytics/send-welcome/' + userId, {
method: 'POST',
headers: {'X-CSRFToken': document.querySelector('meta[name=csrf-token]')?.content || ''}
});
const data = await resp.json();
if (data.success) {
btn.textContent = '✓ Wysłano';
btn.classList.add('sent');
} else {
btn.textContent = '✗ ' + (data.error || 'Błąd');
btn.disabled = false;
}
} catch(e) {
btn.textContent = '✗ Błąd';
btn.disabled = false;
}
}
async function sendReset(userId, btn) {
btn.disabled = true;
btn.textContent = 'Wysyłanie...';
try {
const resp = await fetch('/admin/analytics/send-reset/' + userId, {
method: 'POST',
headers: {'X-CSRFToken': document.querySelector('meta[name=csrf-token]')?.content || ''}
});
const data = await resp.json();
if (data.success) {
btn.textContent = '✓ Wysłano';
btn.classList.add('sent');
} else {
btn.textContent = '✗ ' + (data.error || 'Błąd');
btn.disabled = false;
}
} catch(e) {
btn.textContent = '✗ Błąd';
btn.disabled = false;
}
}
{% endblock %}