feat(calendar): Show 3 upcoming events on homepage + WhatsApp data import
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

- Display up to 3 next events with RSVP status instead of just one
- Add import script for WhatsApp Norda group data (Feb 2026):
  events, company updates, Alter Energy, Croatia announcement

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-02-12 16:29:07 +01:00
parent 963f9ec5c8
commit 72ba8e05f1
3 changed files with 386 additions and 31 deletions

View File

@ -91,27 +91,29 @@ def index():
total_companies = len(companies)
total_categories = len([c for c in categories if db.query(Company).filter_by(category_id=c.id).count() > 0])
# Najbliższe wydarzenie (dla bannera "Kto weźmie udział?")
# Znajdź pierwsze wydarzenie, które użytkownik może ZOBACZYĆ
# Najbliższe wydarzenia (dla bannera "Kto weźmie udział?")
all_upcoming = db.query(NordaEvent).filter(
NordaEvent.event_date >= date.today()
).order_by(NordaEvent.event_date.asc()).all()
next_event = None
upcoming_events = []
for event in all_upcoming:
if event.can_user_view(current_user):
next_event = event
break
# Sprawdź czy użytkownik jest zapisany i czy MOŻE się zapisać
user_registered = False
user_can_attend = False
if next_event:
user_registered = db.query(EventAttendee).filter(
EventAttendee.event_id == next_event.id,
registered = db.query(EventAttendee).filter(
EventAttendee.event_id == event.id,
EventAttendee.user_id == current_user.id
).first() is not None
user_can_attend = next_event.can_user_attend(current_user)
can_attend = event.can_user_attend(current_user)
upcoming_events.append({
'event': event,
'user_registered': registered,
'user_can_attend': can_attend,
})
if len(upcoming_events) >= 3:
break
# Backward compat — next_event used by other parts
next_event = upcoming_events[0]['event'] if upcoming_events else None
# ZOPK Knowledge facts — admin only widget
zopk_facts = []
@ -140,8 +142,7 @@ def index():
total_companies=total_companies,
total_categories=total_categories,
next_event=next_event,
user_registered=user_registered,
user_can_attend=user_can_attend,
upcoming_events=upcoming_events,
pending_application=pending_application,
zopk_facts=zopk_facts
)

View File

@ -0,0 +1,349 @@
#!/usr/bin/env python3
"""
Import danych z WhatsApp grupy Norda Biznes (4-11.02.2026)
Zawiera:
- Nowe wydarzenia: Szkolenie KSeF 23.02, Chwila dla Biznesu 26.02 (jeśli brak)
- Update speaker info: Śniadanie 20.02 (ID=43)
- Uzupełnienie opisów firm: Fiume Studio (128), PG Construction (49), Green House Systems (24)
- Nowa firma: Alter Energy (child brand pod Fiume Studio)
- UserCompany: Irmina (user 67) Alter Energy
- Ogłoszenie: Chorwacja pod Żaglami
Użycie:
python3 scripts/import_whatsapp_feb2026.py # dry-run
python3 scripts/import_whatsapp_feb2026.py --apply # zapis do bazy
"""
import os
import sys
from pathlib import Path
from datetime import date, time, datetime
sys.path.insert(0, str(Path(__file__).parent.parent))
from sqlalchemy import create_engine, text
from sqlalchemy.orm import sessionmaker
def load_env():
"""Załaduj .env z katalogu projektu."""
env_path = Path(__file__).parent.parent / '.env'
if env_path.exists():
with open(env_path, 'r') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#') and '=' in line:
key, value = line.split('=', 1)
os.environ.setdefault(key.strip(), value.strip())
load_env()
DATABASE_URL = os.getenv('DATABASE_URL', 'postgresql://nordabiz_app:CHANGE_ME@127.0.0.1:5432/nordabiz')
SOURCE = 'whatsapp_norda_feb2026'
def main():
dry_run = '--apply' not in sys.argv
print("=" * 70)
print("IMPORT: WhatsApp Norda Biznes (4-11.02.2026)")
print(f"MODE: {'DRY-RUN (brak zapisu)' if dry_run else 'APPLY (zapis do bazy!)'}")
print("=" * 70)
engine = create_engine(DATABASE_URL)
Session = sessionmaker(bind=engine)
session = Session()
stats = {'added': 0, 'updated': 0, 'skipped': 0}
try:
# ============================================================
# 1. WYDARZENIA
# ============================================================
print("\n--- 1. WYDARZENIA ---\n")
# 1a. Szkolenie KSeF 23.02
existing = session.execute(
text("SELECT id FROM norda_events WHERE event_date = :d AND title ILIKE :t"),
{"d": date(2026, 2, 23), "t": "%KSeF%"}
).fetchone()
if existing:
print(f" SKIP: Szkolenie KSeF 23.02 juz istnieje (ID={existing[0]})")
stats['skipped'] += 1
else:
print(" + ADD: Szkolenie KSeF 23.02")
if not dry_run:
session.execute(text("""
INSERT INTO norda_events
(title, description, event_type, event_date, time_start, time_end,
location, source, source_note, access_level, created_at)
VALUES (:title, :desc, :etype, :edate, :tstart, :tend,
:loc, :src, :src_note, :access, NOW())
"""), {
"title": "Szkolenie — Zmiany podatkowe i KSeF 2026",
"desc": "Szkolenie z zakresu zmian podatkowych obowiazujacych od nowego roku oraz KSeF. "
"Dostepne dla czlonkow Izby NORDA, ich pracownikow i osob prowadzacych rozliczenia ksiegowe. "
"Firmy spoza Izby — koszt 250 zl/os. Kontakt: sekretariat Nordy (Magda).",
"etype": "meeting",
"edate": date(2026, 2, 23),
"tstart": time(9, 0),
"tend": time(14, 30),
"loc": "Urzad Miejski Wejherowo, ul. 12 Marca, ostatnie pietro",
"src": SOURCE,
"src_note": "WhatsApp grupa Norda, Roman, 8.02.2026",
"access": "members_only",
})
stats['added'] += 1
# 1b. Chwila dla Biznesu 26.02 — sprawdz czy istnieje
existing = session.execute(
text("SELECT id FROM norda_events WHERE event_date = :d AND title ILIKE :t"),
{"d": date(2026, 2, 26), "t": "%Chwila%"}
).fetchone()
if existing:
print(f" SKIP: Chwila dla Biznesu 26.02 juz istnieje (ID={existing[0]})")
stats['skipped'] += 1
else:
print(" + ADD: Chwila dla Biznesu 26.02")
if not dry_run:
session.execute(text("""
INSERT INTO norda_events
(title, description, event_type, event_date, time_start,
location, source, source_note, access_level, created_at)
VALUES (:title, :desc, :etype, :edate, :tstart,
:loc, :src, :src_note, :access, NOW())
"""), {
"title": "Chwila dla Biznesu — luty 2026",
"desc": "Comiesięczne spotkanie networkingowe członków Izby NORDA.",
"etype": "networking",
"edate": date(2026, 2, 26),
"tstart": time(19, 0),
"loc": "Hotel Olimp, V piętro, Wejherowo",
"src": SOURCE,
"src_note": "WhatsApp grupa Norda, Roman, 11.02.2026",
"access": "members_only",
})
stats['added'] += 1
# 1c. Śniadanie 20.02 (ID=43) — UPDATE speaker info
event43 = session.execute(
text("SELECT id, speaker_name, speaker_company_id FROM norda_events WHERE id = 43")
).fetchone()
if not event43:
print(" WARN: Wydarzenie ID=43 nie istnieje!")
elif event43[1] and event43[2]:
print(f" SKIP: Sniadanie ID=43 ma juz speaker: {event43[1]} (company_id={event43[2]})")
stats['skipped'] += 1
else:
print(f" ~ UPD: Sniadanie ID=43 — speaker: Alicja Domachowska, company_id=40 (Lean Idea)")
if not dry_run:
session.execute(text("""
UPDATE norda_events
SET speaker_name = :speaker, speaker_company_id = :cid
WHERE id = 43
"""), {"speaker": "Alicja Domachowska", "cid": 40})
stats['updated'] += 1
# ============================================================
# 2. UZUPEŁNIENIE OPISÓW FIRM
# ============================================================
print("\n--- 2. UZUPELNIENIE OPISOW FIRM ---\n")
company_updates = [
{
"id": 128,
"name": "Fiume Studio",
"description_short": "Fiume Studio — atelier kulinarne i sala eventowa w Redzie. Warsztaty kulinarne, eventy firmowe, catering.",
"services_offered": "Warsztaty kulinarne, sala bankietowa, eventy firmowe, warsztaty pizzy, catering",
},
{
"id": 49,
"name": "PG Construction",
"description_short": "Projektowanie, nadzór budowlany, kompleksowa obsługa inwestycji budowlanych, obsługa deweloperów.",
"services_offered": "Projektowanie budowlane, nadzór budowlany, kompleksowa obsługa inwestycji budowlanych, obsługa deweloperów",
},
{
"id": 24,
"name": "Green House Systems",
"description_short": "Firma instalacyjna — wod-kan, ogrzewanie podłogowe, rekuperacja, klimatyzacja, instalacje elektryczne z automatyką budynkową, fotowoltaika z magazynem energii.",
"services_offered": "Instalacje wod-kan, ogrzewanie podłogowe, rekuperacja, klimatyzacja, instalacje elektryczne, automatyka budynkowa, fotowoltaika, magazyny energii",
"website": "https://www.greenhousesystems.pl",
},
]
for cu in company_updates:
row = session.execute(
text("SELECT id, name, description_short, services_offered, website FROM companies WHERE id = :id"),
{"id": cu["id"]}
).fetchone()
if not row:
print(f" WARN: Firma ID={cu['id']} ({cu['name']}) nie istnieje!")
continue
updates = []
params = {"id": cu["id"]}
# description_short — update only if empty
if not row[2] or row[2].strip() == '':
updates.append("description_short = :desc_short")
params["desc_short"] = cu["description_short"]
else:
print(f" SKIP: {cu['name']} (ID={cu['id']}) — description_short juz wypelnione")
# services_offered — update only if empty
if not row[3] or row[3].strip() == '':
updates.append("services_offered = :services")
params["services"] = cu["services_offered"]
else:
print(f" SKIP: {cu['name']} (ID={cu['id']}) — services_offered juz wypelnione")
# website — update only if empty and provided
if "website" in cu:
if not row[4] or row[4].strip() == '':
updates.append("website = :website")
params["website"] = cu["website"]
else:
print(f" SKIP: {cu['name']} (ID={cu['id']}) — website juz wypelnione")
if updates:
set_clause = ", ".join(updates)
print(f" ~ UPD: {cu['name']} (ID={cu['id']}) — {', '.join(u.split(' =')[0] for u in updates)}")
if not dry_run:
session.execute(
text(f"UPDATE companies SET {set_clause}, last_updated = NOW() WHERE id = :id"),
params
)
stats['updated'] += 1
else:
stats['skipped'] += 1
# ============================================================
# 3. NOWA FIRMA: Alter Energy
# ============================================================
print("\n--- 3. NOWA FIRMA: Alter Energy ---\n")
existing_alter = session.execute(
text("SELECT id FROM companies WHERE slug = 'alter-energy'")
).fetchone()
alter_energy_id = None
if existing_alter:
alter_energy_id = existing_alter[0]
print(f" SKIP: Alter Energy juz istnieje (ID={alter_energy_id})")
stats['skipped'] += 1
else:
print(" + ADD: Alter Energy (child brand pod Fiume Studio, ID=128)")
if not dry_run:
result = session.execute(text("""
INSERT INTO companies
(name, slug, phone, parent_company_id, status, data_quality, data_source, created_at, last_updated)
VALUES (:name, :slug, :phone, :parent_id, :status, :dq, :ds, NOW(), NOW())
RETURNING id
"""), {
"name": "Alter Energy",
"slug": "alter-energy",
"phone": "508259086",
"parent_id": 128,
"status": "active",
"dq": "basic",
"ds": SOURCE,
})
alter_energy_id = result.fetchone()[0]
print(f" → ID={alter_energy_id}")
stats['added'] += 1
# 3b. UserCompany: Irmina (user 67) → Alter Energy
if alter_energy_id:
existing_uc = session.execute(
text("SELECT id FROM user_companies WHERE user_id = 67 AND company_id = :cid"),
{"cid": alter_energy_id}
).fetchone()
if existing_uc:
print(f" SKIP: UserCompany Irmina(67) → Alter Energy({alter_energy_id}) juz istnieje")
stats['skipped'] += 1
else:
print(f" + ADD: UserCompany Irmina(67) → Alter Energy({alter_energy_id}), role=MANAGER, is_primary=false")
if not dry_run:
session.execute(text("""
INSERT INTO user_companies (user_id, company_id, role, is_primary, created_at, updated_at)
VALUES (67, :cid, 'MANAGER', false, NOW(), NOW())
"""), {"cid": alter_energy_id})
stats['added'] += 1
else:
if dry_run:
print(" (dry-run) UserCompany Irmina → Alter Energy zostanie dodane przy --apply")
# ============================================================
# 4. OGŁOSZENIE: Chorwacja pod Żaglami
# ============================================================
print("\n--- 4. OGLOSZENIE: Chorwacja pod Zaglami ---\n")
existing_ann = session.execute(
text("SELECT id FROM announcements WHERE slug = 'chorwacja-pod-zaglami'")
).fetchone()
if existing_ann:
print(f" SKIP: Ogłoszenie 'Chorwacja pod Zaglami' juz istnieje (ID={existing_ann[0]})")
stats['skipped'] += 1
else:
# Find admin user for created_by
admin_user = session.execute(
text("SELECT id FROM users WHERE is_admin = true ORDER BY id LIMIT 1")
).fetchone()
admin_id = admin_user[0] if admin_user else 1
print(" + ADD: Ogloszenie 'Chorwacja pod Zaglami'")
if not dry_run:
session.execute(text("""
INSERT INTO announcements
(title, slug, excerpt, content, categories, status, published_at, created_by, created_at, updated_at)
VALUES (:title, :slug, :excerpt, :content, :cats, 'published', NOW(), :created_by, NOW(), NOW())
"""), {
"title": "Chorwacja pod Żaglami — wyjazd integracyjny",
"slug": "chorwacja-pod-zaglami",
"excerpt": "Pomysł Prezesa Leszka Glazy — wspólny wyjazd pod żaglami do Chorwacji. Kontakt: sekretariat Nordy (Magda) lub Prezes.",
"content": "<p>Zapraszamy na wyjazd integracyjny do Chorwacji pod żaglami! Pomysł naszego Prezesa Leszka Glazy.</p>"
"<p>Poprzednie wspólne wyjazdy (USA, Hawaje) były wielkim sukcesem. "
"Szczegóły do uzgodnienia — prosimy o kontakt z sekretariatem Nordy (Magda) lub bezpośrednio z Prezesem.</p>",
"cats": ["event"],
"created_by": admin_id,
})
stats['added'] += 1
# ============================================================
# COMMIT
# ============================================================
if not dry_run:
session.commit()
print("\n>>> COMMIT OK")
# ============================================================
# PODSUMOWANIE
# ============================================================
print("\n" + "=" * 70)
print(f"PODSUMOWANIE: dodano={stats['added']}, zaktualizowano={stats['updated']}, pominieto={stats['skipped']}")
if dry_run:
print("[DRY-RUN] Zadne dane NIE zostaly zapisane. Uzyj --apply aby zapisac.")
print("=" * 70)
except Exception as e:
session.rollback()
print(f"\nBLAD: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
finally:
session.close()
if __name__ == "__main__":
main()

View File

@ -863,36 +863,41 @@
</div>
{% endif %}
<!-- Event Banner - Ankieta "Kto weźmie udział?" -->
{% if next_event %}
<a href="{{ url_for('calendar.calendar_event', event_id=next_event.id) }}" class="event-banner" data-animate="fadeIn">
<!-- Event Banners - Najbliższe wydarzenia -->
{% if upcoming_events %}
{% for ue in upcoming_events %}
{% set ev = ue.event %}
<a href="{{ url_for('calendar.calendar_event', event_id=ev.id) }}" class="event-banner" data-animate="fadeIn"{% if not loop.first %} style="margin-top: -8px;"{% endif %}>
<div class="event-banner-icon">📅</div>
<div class="event-banner-content">
<div class="event-banner-label">Najbliższe wydarzenie Kto weźmie udział?</div>
<div class="event-banner-title">{{ next_event.title }} →</div>
<div class="event-banner-meta">
<span>📆 {{ next_event.event_date.strftime('%d.%m.%Y') }} ({{ ['Pon', 'Wt', 'Śr', 'Czw', 'Pt', 'Sob', 'Nd'][next_event.event_date.weekday()] }})</span>
{% if next_event.time_start %}
<span>🕕 {{ next_event.time_start.strftime('%H:%M') }}</span>
{% if loop.first %}
<div class="event-banner-label">Najbliższe wydarzenia Kto weźmie udział?</div>
{% endif %}
{% if next_event.location %}
<span>📍 {{ next_event.location[:30] }}{% if next_event.location|length > 30 %}...{% endif %}</span>
<div class="event-banner-title">{{ ev.title }} →</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 class="event-banner-attendees">
👥 Zapisanych: {{ next_event.attendee_count }} {% if next_event.attendee_count == 1 %}osoba{% elif next_event.attendee_count in [2,3,4] %}osoby{% else %}osób{% endif %}
👥 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>
<div class="event-banner-action">
{% if user_registered %}
{% if ue.user_registered %}
<span class="btn-light btn-registered">✓ Jesteś zapisany/a</span>
{% elif user_can_attend %}
<button type="button" class="btn-light" onclick="rsvpAndGo(event, {{ next_event.id }})">Zapisz się →</button>
{% elif next_event.access_level == 'rada_only' %}
{% 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>
{% endif %}
</div>
</a>
{% endfor %}
{% endif %}