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
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:
parent
53491db06a
commit
0b5ffdcc76
@ -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],
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -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 -->
|
||||
|
||||
Loading…
Reference in New Issue
Block a user