feat(homepage): redesign events section with 2-column grid, forum topic and new members
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

Add admitted_at_meeting_id to Company model linking firms to board meetings.
Homepage now shows 2 events (left column) + latest forum topic and new members
admitted at the last board meeting (right column). Responsive single-column on mobile.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-04-06 13:50:56 +02:00
parent 110d971dca
commit 4ec8e15c56
4 changed files with 154 additions and 49 deletions

View File

@ -139,7 +139,7 @@ def index():
'user_registered': registered,
'user_can_attend': can_attend,
})
if len(upcoming_events) >= 6:
if len(upcoming_events) >= 2:
break
# Backward compat — next_event used by other parts
@ -182,6 +182,29 @@ def index():
all_releases = _get_releases()
latest_release = all_releases[0] if all_releases else None
# Latest forum topic for homepage
latest_forum_topic = None
try:
from database import ForumTopic
latest_forum_topic = db.query(ForumTopic).filter(
ForumTopic.is_deleted == False
).order_by(ForumTopic.created_at.desc()).first()
except Exception:
pass
# New members admitted at last board meeting
latest_admitted = []
last_meeting = None
try:
from database import BoardMeeting
last_meeting = db.query(BoardMeeting).order_by(BoardMeeting.meeting_date.desc()).first()
if last_meeting:
latest_admitted = db.query(Company).filter(
Company.admitted_at_meeting_id == last_meeting.id
).order_by(Company.name).all()
except Exception:
pass
return render_template(
'index.html',
companies=companies,
@ -195,7 +218,10 @@ def index():
zopk_facts=zopk_facts,
latest_release=latest_release,
company_children=company_children,
company_parent=company_parent
company_parent=company_parent,
latest_forum_topic=latest_forum_topic,
latest_admitted=latest_admitted,
last_meeting=last_meeting
)
finally:
db.close()

View File

@ -848,6 +848,7 @@ class Company(Base):
norda_biznes_url = Column(String(500))
norda_biznes_member_id = Column(String(50))
member_since = Column(Date) # Data przystąpienia do Izby NORDA
admitted_at_meeting_id = Column(Integer, ForeignKey('board_meetings.id'), nullable=True)
previous_years_debt = Column(Numeric(10, 2), default=0) # Zaległości z lat poprzednich (ręcznie wpisane)
# Metadata
@ -887,6 +888,9 @@ class Company(Base):
krs_last_audit_at = Column(DateTime) # Data ostatniego audytu KRS
krs_pdf_path = Column(Text) # Ścieżka do pliku PDF
# Board meeting where company was admitted
admitted_at_meeting = relationship('BoardMeeting', foreign_keys=[admitted_at_meeting_id])
# Relationships
category = relationship('Category', back_populates='companies')
services = relationship('CompanyService', back_populates='company', cascade='all, delete-orphan')

View File

@ -0,0 +1,5 @@
-- Migration 098: Add admitted_at_meeting_id to companies
-- Links companies to the board meeting where they were admitted as members
ALTER TABLE companies ADD COLUMN IF NOT EXISTS admitted_at_meeting_id INTEGER REFERENCES board_meetings(id);
GRANT ALL ON TABLE companies TO nordabiz_app;

View File

@ -193,7 +193,19 @@
display: none;
}
/* Homepage 2-column grid */
.homepage-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
@media (max-width: 768px) {
.homepage-grid {
grid-template-columns: 1fr;
}
.events-row {
grid-template-columns: 1fr;
}
@ -1080,62 +1092,120 @@
</div>
{% endif %}
<!-- Event Banners - Najbliższe wydarzenia -->
{% if upcoming_events %}
<div class="events-filter" data-animate="fadeIn">
<span class="events-filter-label">Pokaż:</span>
<button class="events-filter-btn active" data-filter="all" onclick="filterEvents('all', this)">Wszystkie</button>
<button class="events-filter-btn" data-filter="norda" onclick="filterEvents('norda', this)">🏢 Norda Biznes</button>
<button class="events-filter-btn" data-filter="external" onclick="filterEvents('external', this)">🌐 Zewnętrzne</button>
</div>
<div class="events-row" data-animate="fadeIn">
{% for ue in upcoming_events %}
{% set ev = ue.event %}
<a href="{{ url_for('calendar.calendar_event', event_id=ev.id) }}" class="event-banner" data-event-type="{{ 'external' if ev.is_external else 'norda' }}">
<div class="event-banner-top">
<div class="event-banner-icon">📅</div>
<div class="event-banner-content">
{% if loop.first %}
<div class="events-row-label">Najbliższe wydarzenia Kto weźmie udział?</div>
{% endif %}
<div class="event-banner-title">
{{ ev.title }} →
{% if ev.is_external and ev.external_source %}
<span style="display:inline-block; background:rgba(255,255,255,0.2); color:#fff; font-size:10px; padding:2px 6px; border-radius:4px; font-weight:600; vertical-align:middle; margin-left:6px;">🌐 {{ ev.external_source }}</span>
{% endif %}
{% if ev.access_level == 'admin_only' %}
<span style="display:inline-block; background:#ef4444; color:#fff; font-size:10px; padding:2px 6px; border-radius:4px; font-weight:600; vertical-align:middle; margin-left:6px;">UKRYTE</span>
{% elif ev.access_level == 'rada_only' %}
<span style="display:inline-block; background:#f59e0b; color:#92400e; font-size:10px; padding:2px 6px; border-radius:4px; font-weight:600; vertical-align:middle; margin-left:6px;">IZBA</span>
<!-- Homepage Grid: Events + Forum + New Members -->
{% if upcoming_events or latest_forum_topic or latest_admitted %}
<div class="homepage-grid" data-animate="fadeIn">
<!-- LEFT COLUMN: 2 Events -->
<div style="display: flex; flex-direction: column; gap: var(--spacing-md);">
{% if upcoming_events %}
{% for ue in upcoming_events[:2] %}
{% set ev = ue.event %}
<a href="{{ url_for('calendar.calendar_event', event_id=ev.id) }}" class="event-banner" data-event-type="{{ 'external' if ev.is_external else 'norda' }}" style="margin: 0;">
<div class="event-banner-top">
<div class="event-banner-icon">📅</div>
<div class="event-banner-content">
{% if loop.first %}
<div class="events-row-label">Najbliższe wydarzenia Kto weźmie udział?</div>
{% endif %}
<div class="event-banner-title">
{{ ev.title }} →
{% if ev.is_external and ev.external_source %}
<span style="display:inline-block; background:rgba(255,255,255,0.2); color:#fff; font-size:10px; padding:2px 6px; border-radius:4px; font-weight:600; vertical-align:middle; margin-left:6px;">🌐 {{ ev.external_source }}</span>
{% endif %}
</div>
<div class="event-banner-meta">
<span>📆 {{ ev.event_date.strftime('%d.%m.%Y') }} ({{ ['Pon', 'Wt', 'Śr', 'Czw', 'Pt', 'Sob', 'Nd'][ev.event_date.weekday()] }})</span>
{% if ev.time_start %}
<span>🕕 {{ ev.time_start.strftime('%H:%M') }}</span>
{% endif %}
{% if ev.location %}
<span>📍 {{ ev.location[:30] }}{% if ev.location|length > 30 %}...{% endif %}</span>
{% endif %}
</div>
</div>
<div class="event-banner-meta">
<span>📆 {{ ev.event_date.strftime('%d.%m.%Y') }} ({{ ['Pon', 'Wt', 'Śr', 'Czw', 'Pt', 'Sob', 'Nd'][ev.event_date.weekday()] }})</span>
{% if ev.time_start %}
<span>🕕 {{ ev.time_start.strftime('%H:%M') }}</span>
{% endif %}
{% if ev.location %}
<span>📍 {{ ev.location[:30] }}{% if ev.location|length > 30 %}...{% endif %}</span>
</div>
<div class="event-banner-bottom">
<div class="event-banner-attendees">
👥 Zapisanych: {{ ev.attendee_count }} {% if ev.attendee_count == 1 %}osoba{% elif ev.attendee_count in [2,3,4] %}osoby{% else %}osób{% endif %}
</div>
<div class="event-banner-action">
{% if ue.user_registered %}
<span class="btn-light btn-registered">✓ Jesteś zapisany/a</span>
{% elif ue.user_can_attend %}
<button type="button" class="btn-light" onclick="rsvpAndGo(event, {{ ev.id }})">Zapisz się →</button>
{% endif %}
</div>
</div>
</div>
<div class="event-banner-bottom">
<div class="event-banner-attendees">
👥 Zapisanych: {{ ev.attendee_count }} {% if ev.attendee_count == 1 %}osoba{% elif ev.attendee_count in [2,3,4] %}osoby{% else %}osób{% endif %}
</a>
{% endfor %}
{% endif %}
</div>
<!-- RIGHT COLUMN: Forum + New Members -->
<div style="display: flex; flex-direction: column; gap: var(--spacing-md);">
<!-- Latest Forum Topic -->
{% if latest_forum_topic %}
<a href="{{ url_for('forum.forum_topic', topic_id=latest_forum_topic.id) }}" style="text-decoration: none; display: block; background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--spacing-md); transition: all 0.2s; min-height: 120px;" onmouseover="this.style.borderColor='var(--primary)';this.style.boxShadow='0 2px 8px rgba(0,0,0,0.08)'" onmouseout="this.style.borderColor='var(--border)';this.style.boxShadow='none'">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
<span style="font-size: 1.2rem;">💬</span>
<span style="font-size: var(--font-size-xs); font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px;">Najnowszy wpis na forum</span>
</div>
<div class="event-banner-action">
{% if ue.user_registered %}
<span class="btn-light btn-registered">✓ Jesteś zapisany/a</span>
{% elif ue.user_can_attend %}
<button type="button" class="btn-light" onclick="rsvpAndGo(event, {{ ev.id }})">Zapisz się →</button>
{% elif ev.access_level == 'rada_only' %}
<span class="btn-light" style="background: #fef3c7; color: #92400e; border: 1px solid #fde68a;">🔒 Rada Izby</span>
<div style="font-weight: 600; color: var(--text-primary); font-size: var(--font-size-base); line-height: 1.4; margin-bottom: 8px;">
{{ latest_forum_topic.title }}
</div>
<div style="font-size: var(--font-size-sm); color: var(--text-secondary);">
{{ latest_forum_topic.author.name if latest_forum_topic.author else 'Anonim' }} · {{ latest_forum_topic.created_at|local_time('%d.%m.%Y %H:%M') }}
{% if latest_forum_topic.reply_count is defined and latest_forum_topic.reply_count > 0 %}
· {{ latest_forum_topic.reply_count }} {{ 'odpowiedź' if latest_forum_topic.reply_count == 1 else ('odpowiedzi' if latest_forum_topic.reply_count < 5 else 'odpowiedzi') }}
{% endif %}
</div>
</a>
{% endif %}
<!-- New Members -->
{% if latest_admitted %}
<a href="{{ url_for('public.new_members') }}" style="text-decoration: none; display: block; background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--spacing-md); transition: all 0.2s; min-height: 120px;" onmouseover="this.style.borderColor='var(--primary)';this.style.boxShadow='0 2px 8px rgba(0,0,0,0.08)'" onmouseout="this.style.borderColor='var(--border)';this.style.boxShadow='none'">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
<span style="font-size: 1.2rem;">🏢</span>
<span style="font-size: var(--font-size-xs); font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px;">Nowi członkowie Izby</span>
{% if last_meeting %}
<span style="font-size: var(--font-size-xs); color: var(--text-muted);">· Rada {{ last_meeting.meeting_date.strftime('%d.%m.%Y') }}</span>
{% endif %}
</div>
<div style="display: flex; flex-direction: column; gap: 6px;">
{% for company in latest_admitted[:4] %}
<div style="display: flex; align-items: center; gap: 8px;">
<span style="color: var(--success); font-size: 14px;"></span>
<span style="font-weight: 500; color: var(--text-primary); font-size: var(--font-size-sm);">
{{ company.name }}
</span>
{% if company.status != 'active' %}
<span style="font-size: var(--font-size-xs); color: var(--text-muted); font-style: italic;">· profil w trakcie uzupełniania</span>
{% endif %}
</div>
{% endfor %}
{% if latest_admitted|length > 4 %}
<div style="font-size: var(--font-size-sm); color: var(--text-muted);">+ {{ latest_admitted|length - 4 }} więcej</div>
{% endif %}
</div>
<div style="margin-top: 8px; font-size: var(--font-size-sm); color: var(--primary); font-weight: 500;">
Zobacz wszystkich nowych członków →
</div>
</a>
{% elif last_meeting %}
<!-- No companies admitted yet at last meeting, show placeholder -->
<div style="background: var(--card-bg); border: 1px solid var(--border); border-radius: var(--radius); padding: var(--spacing-md); min-height: 120px;">
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
<span style="font-size: 1.2rem;">🏢</span>
<span style="font-size: var(--font-size-xs); font-weight: 600; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px;">Nowi członkowie Izby</span>
</div>
<div style="color: var(--text-muted); font-size: var(--font-size-sm);">Brak nowych firm przyjętych na ostatnim posiedzeniu Rady.</div>
</div>
</a>
{% endfor %}
{% endif %}
</div>
</div>
{% endif %}