feat: simplify engagement tab - single sorted list with activity 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 3-section active/at-risk/dormant design with one clear table
showing all members sorted by activity level. Green/yellow/gray bars,
human-readable last login dates, inactive users dimmed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-10 20:53:57 +01:00
parent 0b5ffdcc76
commit cfcee7af73
2 changed files with 78 additions and 136 deletions

View File

@ -633,73 +633,47 @@ def _tab_engagement(db, start_date, days):
int(dur30) / 60 * 2 + conv30 * 10 + search30 * 2)
score = _log_engagement_score(raw)
# WoW change
wow = None
if pv_prev > 0:
wow = round((pv_cur - pv_prev) / pv_prev * 100)
elif pv_cur > 0:
wow = 100
# Engagement status (improved logic)
# Last login label
days_since_login = None
if user.last_login:
days_since_login = (date.today() - user.last_login.date()).days
if days_since_login is None:
status = 'dormant'
elif days_since_login <= 7:
status = 'active' if score >= 10 else 'at_risk'
elif days_since_login <= 30:
status = 'at_risk'
else:
status = 'dormant'
# Sparkline from batch data
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ś'
last_login = 'Dziś'
elif days_since_login == 1:
last_activity = 'Wczoraj'
last_login = 'Wczoraj'
elif days_since_login <= 7:
last_activity = f'{days_since_login} dni temu'
last_login = f'{days_since_login} dni temu'
elif days_since_login <= 30:
weeks = days_since_login // 7
last_activity = f'{weeks} tyg. temu'
last_login = f'{weeks} tyg. temu'
else:
last_activity = f'{days_since_login} dni temu'
months = days_since_login // 30
if months > 0:
last_login = f'{months} mies. temu'
else:
last_login = f'{days_since_login} dni temu'
else:
last_activity = 'Nigdy'
last_login = 'Nigdy'
if sessions_cur > 0 or score > 0:
engagement_list.append({
'user': user,
'score': score,
'sessions': sessions_cur,
'page_views': pv_cur,
'status': status,
'last_activity': last_activity,
'days_since': days_since_login,
})
engagement_list.append({
'user': user,
'score': score,
'sessions': sessions_cur,
'page_views': pv_cur,
'last_login': last_login,
'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,
'active_list': active_list[:25],
'at_risk_list': at_risk_list[:25],
'dormant_list': dormant_list[:25],
'engagement_list': engagement_list,
}

View File

@ -66,10 +66,7 @@
/* 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; }
.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; }
@ -448,99 +445,70 @@
<div class="stats-grid">
<div class="stat-card success">
<div class="stat-value">{{ data.active_7d }}</div>
<div class="stat-label">Aktywni ({% if period == 'day' %}dziś{% elif period == 'week' %}7 dni{% else %}30 dni{% endif %})</div>
</div>
<div class="stat-card warning">
<div class="stat-value">{{ data.at_risk }}</div>
<div class="stat-label">Zagrożeni (8-30d)</div>
</div>
<div class="stat-card error">
<div class="stat-value">{{ data.dormant }}</div>
<div class="stat-label">Uśpieni (30d+)</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>
{% 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.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 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>
{% endif %}
<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;">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>
<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 -->