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
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>
1418 lines
72 KiB
HTML
1418 lines
72 KiB
HTML
{% 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;">▶</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. Uwzględnia: sesje, odsłony stron, kliknięcia, czas na stronie, konwersje (RSVP, kontakt), wyszukiwania. 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: {{ 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) = {{ 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 · {{ 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 %}
|