feat: redesign engagement tab with 3 grouped sections and visual score bars
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

Replace single 50-row table with Active/At Risk/Dormant sections.
Remove noisy WoW% column and sparklines, add human-readable last activity.
Score displayed as colored bar instead of abstract number.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-10 20:47:11 +01:00
parent 53491db06a
commit 0b5ffdcc76
2 changed files with 104 additions and 74 deletions

View File

@ -658,25 +658,48 @@ def _tab_engagement(db, start_date, days):
user_spark = sparkline_map.get(uid, {})
sparkline = [user_spark.get(d, 0) for d in spark_days]
# Last activity label
if days_since_login is not None:
if days_since_login == 0:
last_activity = 'Dziś'
elif days_since_login == 1:
last_activity = 'Wczoraj'
elif days_since_login <= 7:
last_activity = f'{days_since_login} dni temu'
elif days_since_login <= 30:
weeks = days_since_login // 7
last_activity = f'{weeks} tyg. temu'
else:
last_activity = f'{days_since_login} dni temu'
else:
last_activity = 'Nigdy'
if sessions_cur > 0 or score > 0:
engagement_list.append({
'user': user,
'score': score,
'sessions': sessions_cur,
'page_views': pv_cur,
'wow': wow,
'status': status,
'sparkline': sparkline,
'last_activity': last_activity,
'days_since': days_since_login,
})
engagement_list.sort(key=lambda x: x['score'], reverse=True)
# Group by status
active_list = [e for e in engagement_list if e['status'] == 'active']
at_risk_list = [e for e in engagement_list if e['status'] == 'at_risk']
dormant_list = [e for e in engagement_list if e['status'] == 'dormant']
return {
'active_7d': active_7d,
'at_risk': at_risk,
'dormant': dormant,
'new_this_month': new_this_month,
'engagement_list': engagement_list[:50],
'active_list': active_list[:25],
'at_risk_list': at_risk_list[:25],
'dormant_list': dormant_list[:25],
}

View File

@ -64,14 +64,12 @@
.badge-at-risk, .badge-at_risk { background: #fef3c7; color: #92400e; }
.badge-dormant { background: #fee2e2; color: #991b1b; }
/* Sparkline */
.sparkline { display: inline-flex; align-items: flex-end; gap: 2px; height: 24px; }
.sparkline-bar { width: 6px; background: var(--primary); border-radius: 1px; min-height: 2px; opacity: 0.7; }
/* WoW arrow */
.wow-up { color: #16a34a; font-weight: 600; }
.wow-down { color: #dc2626; font-weight: 600; }
.wow-flat { color: var(--text-muted); }
/* Engagement bars */
.engagement-bar-wrap { height: 8px; background: var(--background); border-radius: 4px; overflow: hidden; }
.engagement-bar { height: 100%; border-radius: 4px; min-width: 4px; transition: width 0.3s ease; }
.engagement-bar-active { background: #16a34a; }
.engagement-bar-at_risk { background: #f59e0b; }
.engagement-bar-dormant { background: #ef4444; }
/* Horizontal bars */
.bar-container { height: 20px; background: var(--background); border-radius: var(--radius-sm); overflow: hidden; min-width: 100px; }
@ -466,74 +464,83 @@
</div>
</div>
<div class="section-card">
<h2>Ranking zaangażowania</h2>
{% macro engagement_table(users, empty_msg) %}
{% if users %}
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th>Użytkownik</th>
<th>Zaangażowanie</th>
<th>Sesje</th>
<th>Odsłony</th>
<th>Ostatnia aktywność</th>
</tr>
</thead>
<tbody>
{% for e in users %}
<tr>
<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 style="min-width: 120px;">
<div class="engagement-bar-wrap">
<div class="engagement-bar engagement-bar-{{ e.status }}" style="width: {{ [e.score, 100]|min }}%;"></div>
</div>
</td>
<td>{{ e.sessions }}</td>
<td>{{ e.page_views }}</td>
<td style="white-space: nowrap; color: var(--text-secondary);">{{ e.last_activity }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p style="text-align: center; padding: var(--spacing-md); color: var(--text-muted);">{{ empty_msg }}</p>
{% endif %}
{% endmacro %}
{% if data.engagement_list %}
<div class="table-scroll">
<table class="data-table">
<thead>
<tr>
<th>#</th>
<th>Użytkownik</th>
<th>Score</th>
<th>Sesje</th>
<th>Odsłony</th>
<th>Zmiana</th>
<th>Aktywność</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{% for e in data.engagement_list %}
<tr>
<td style="font-weight: 600; color: var(--text-muted);">{{ 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><strong>{{ e.score }}</strong></td>
<td>{{ e.sessions }}</td>
<td>{{ e.page_views }}</td>
<td>
{% if e.wow is not none %}
{% if e.wow > 0 %}
<span class="wow-up">▲ {{ e.wow }}%</span>
{% elif e.wow < 0 %}
<span class="wow-down">▼ {{ e.wow|abs }}%</span>
{% else %}
<span class="wow-flat">— 0%</span>
{% endif %}
{% else %}
<span class="wow-flat">N/A</span>
{% endif %}
</td>
<td>
<div class="sparkline">
{% set max_spark = e.sparkline|max if e.sparkline|max > 0 else 1 %}
{% for val in e.sparkline %}
<div class="sparkline-bar" style="height: {{ (val / max_spark * 22 + 2)|int }}px;"></div>
{% endfor %}
</div>
</td>
<td>
<span class="badge badge-{{ e.status }}">
{% if e.status == 'active' %}Aktywny{% elif e.status == 'at_risk' %}Zagrożony{% else %}Uśpiony{% endif %}
</span>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
{% if data.active_list %}
<div class="section-card">
<h2 style="color: #166534;">
<span style="display: inline-block; width: 10px; height: 10px; background: #16a34a; border-radius: 50%; margin-right: 8px;"></span>
Aktywni ({{ data.active_list|length }})
</h2>
{{ engagement_table(data.active_list, '') }}
</div>
{% endif %}
{% if data.at_risk_list %}
<div class="section-card">
<h2 style="color: #92400e;">
<span style="display: inline-block; width: 10px; height: 10px; background: #f59e0b; border-radius: 50%; margin-right: 8px;"></span>
Zagrożeni ({{ data.at_risk_list|length }})
</h2>
{{ engagement_table(data.at_risk_list, '') }}
</div>
{% endif %}
{% if data.dormant_list %}
<div class="section-card">
<h2 style="color: #991b1b;">
<span style="display: inline-block; width: 10px; height: 10px; background: #ef4444; border-radius: 50%; margin-right: 8px;"></span>
Uśpieni ({{ data.dormant_list|length }})
</h2>
{{ engagement_table(data.dormant_list, '') }}
</div>
{% endif %}
{% if not data.active_list and not data.at_risk_list and not data.dormant_list %}
<div class="section-card">
<div style="text-align: center; padding: var(--spacing-xl); color: var(--text-muted);">
<p>Brak danych o zaangażowaniu w wybranym okresie.</p>
</div>
{% endif %}
</div>
{% endif %}
<!-- ============================================================ -->
<!-- TAB: PAGE MAP -->