feat: enrich person card with board badge, activity, events, forum stats
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

- Rada Izby badge next to name
- Last active label (e.g. "Aktywny 2 dni temu")
- Forum stats (topics + replies count)
- Recent events attended (up to 5, linked)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-12 10:08:18 +01:00
parent 12eb506a93
commit da5f93368f
2 changed files with 110 additions and 3 deletions

View File

@ -396,10 +396,60 @@ def person_detail(person_id):
)) ))
portal_user = candidates[0] portal_user = candidates[0]
# Extra data if portal user exists
is_rada_member = False
last_active_label = None
attended_events = []
forum_topics_count = 0
forum_replies_count = 0
if portal_user:
is_rada_member = getattr(portal_user, 'is_rada_member', False)
# Last activity label
from datetime import datetime, timedelta
if portal_user.last_login:
diff = datetime.now() - portal_user.last_login
if diff < timedelta(hours=1):
last_active_label = 'Aktywny teraz'
elif diff < timedelta(days=1):
hours = int(diff.total_seconds() // 3600)
last_active_label = f'Aktywny {hours} godz. temu'
elif diff < timedelta(days=7):
days = diff.days
last_active_label = f'Aktywny {days} dni temu'
elif diff < timedelta(days=60):
weeks = diff.days // 7
last_active_label = f'Aktywny {weeks} tyg. temu'
else:
last_active_label = f'Ostatnio: {portal_user.last_login.strftime("%d.%m.%Y")}'
# Events attended
from database import EventAttendee, NordaEvent, ForumTopic, ForumReply
attended_events = db.query(NordaEvent).join(
EventAttendee, EventAttendee.event_id == NordaEvent.id
).filter(
EventAttendee.user_id == portal_user.id,
EventAttendee.status == 'confirmed',
).order_by(NordaEvent.event_date.desc()).limit(5).all()
# Forum stats
forum_topics_count = db.query(ForumTopic).filter_by(
author_id=portal_user.id, is_deleted=False
).count()
forum_replies_count = db.query(ForumReply).filter_by(
author_id=portal_user.id, is_deleted=False
).count()
return render_template('person_detail.html', return render_template('person_detail.html',
person=person, person=person,
company_roles=company_roles, company_roles=company_roles,
portal_user=portal_user portal_user=portal_user,
is_rada_member=is_rada_member,
last_active_label=last_active_label,
attended_events=attended_events,
forum_topics_count=forum_topics_count,
forum_replies_count=forum_replies_count,
) )
finally: finally:
db.close() db.close()

View File

@ -239,10 +239,18 @@
<div class="person-avatar"> <div class="person-avatar">
{{ person.imiona[0] }}{{ person.nazwisko[0] }} {{ person.imiona[0] }}{{ person.nazwisko[0] }}
</div> </div>
<h1 class="person-name">{{ person.full_name() }}</h1> <h1 class="person-name">
{{ person.full_name() }}
{% if is_rada_member %}
<span style="display:inline-block;background:#f59e0b;color:#92400e;font-size:12px;padding:3px 10px;border-radius:20px;font-weight:600;vertical-align:middle;margin-left:8px;">Rada Izby</span>
{% endif %}
</h1>
{% set unique_company_ids = company_roles|map(attribute='company_id')|list|unique|list %} {% set unique_company_ids = company_roles|map(attribute='company_id')|list|unique|list %}
<p class="person-subtitle"> <p class="person-subtitle">
Powiazany z {{ unique_company_ids|length }} firmami ({{ company_roles|length }} ról) w Norda Biznes Powiązany z {{ unique_company_ids|length }} firmami ({{ company_roles|length }} ról) w Norda Biznes
{% if last_active_label %}
<span style="display:inline-block;margin-left:12px;padding:2px 10px;background:#ecfdf5;color:#166534;border-radius:12px;font-size:var(--font-size-sm);">{{ last_active_label }}</span>
{% endif %}
</p> </p>
</div> </div>
@ -331,6 +339,55 @@
</div> </div>
{% endif %} {% endif %}
<!-- Activity on portal -->
{% if portal_user and (attended_events or forum_topics_count > 0 or forum_replies_count > 0) %}
<div class="person-section">
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
Aktywność na portalu
</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: var(--spacing-md); margin-bottom: var(--spacing-lg);">
{% if forum_topics_count > 0 %}
<div style="text-align:center; padding: var(--spacing-md); background: var(--background); border-radius: var(--radius-lg);">
<div style="font-size: var(--font-size-2xl); font-weight: 700; color: var(--primary);">{{ forum_topics_count }}</div>
<div style="font-size: var(--font-size-sm); color: var(--text-secondary);">{{ 'temat' if forum_topics_count == 1 else 'tematów' }} na forum</div>
</div>
{% endif %}
{% if forum_replies_count > 0 %}
<div style="text-align:center; padding: var(--spacing-md); background: var(--background); border-radius: var(--radius-lg);">
<div style="font-size: var(--font-size-2xl); font-weight: 700; color: var(--primary);">{{ forum_replies_count }}</div>
<div style="font-size: var(--font-size-sm); color: var(--text-secondary);">{{ 'odpowiedź' if forum_replies_count == 1 else 'odpowiedzi' }}</div>
</div>
{% endif %}
{% if attended_events %}
<div style="text-align:center; padding: var(--spacing-md); background: var(--background); border-radius: var(--radius-lg);">
<div style="font-size: var(--font-size-2xl); font-weight: 700; color: var(--primary);">{{ attended_events|length }}</div>
<div style="font-size: var(--font-size-sm); color: var(--text-secondary);">{{ 'wydarzenie' if attended_events|length == 1 else 'wydarzeń' }}</div>
</div>
{% endif %}
</div>
{% if attended_events %}
<div style="margin-top: var(--spacing-md);">
<div style="font-weight: 600; font-size: var(--font-size-sm); color: var(--text-secondary); margin-bottom: var(--spacing-sm);">Ostatnie wydarzenia:</div>
{% for event in attended_events %}
<a href="{{ url_for('calendar.calendar_event', event_id=event.id) }}" style="display:flex; align-items:center; gap: var(--spacing-sm); padding: 8px 12px; background: var(--background); border-radius: var(--radius); margin-bottom: 4px; text-decoration:none; color: var(--text-primary); font-size: var(--font-size-sm); transition: var(--transition);"
onmouseover="this.style.background='var(--border)'" onmouseout="this.style.background='var(--background)'">
<svg width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24" style="flex-shrink:0;color:var(--text-secondary);">
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/>
</svg>
<span>{{ event.title }}</span>
<span style="margin-left:auto; color:var(--text-secondary); white-space:nowrap;">{{ event.event_date.strftime('%d.%m.%Y') }}</span>
</a>
{% endfor %}
</div>
{% endif %}
</div>
{% endif %}
<!-- Company Roles --> <!-- Company Roles -->
<div class="person-section"> <div class="person-section">
<h2 class="section-title"> <h2 class="section-title">