From 72ba8e05f1c463174144bb8a3910562a1efea541 Mon Sep 17 00:00:00 2001 From: Maciej Pienczyn Date: Thu, 12 Feb 2026 16:29:07 +0100 Subject: [PATCH] feat(calendar): Show 3 upcoming events on homepage + WhatsApp data import - 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 --- blueprints/public/routes.py | 33 +-- scripts/import_whatsapp_feb2026.py | 349 +++++++++++++++++++++++++++++ templates/index.html | 35 +-- 3 files changed, 386 insertions(+), 31 deletions(-) create mode 100644 scripts/import_whatsapp_feb2026.py diff --git a/blueprints/public/routes.py b/blueprints/public/routes.py index 7302603..4c43473 100644 --- a/blueprints/public/routes.py +++ b/blueprints/public/routes.py @@ -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 + registered = db.query(EventAttendee).filter( + EventAttendee.event_id == event.id, + EventAttendee.user_id == current_user.id + ).first() is not None + 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 - # 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, - EventAttendee.user_id == current_user.id - ).first() is not None - user_can_attend = next_event.can_user_attend(current_user) + # 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 ) diff --git a/scripts/import_whatsapp_feb2026.py b/scripts/import_whatsapp_feb2026.py new file mode 100644 index 0000000..d4e3fd9 --- /dev/null +++ b/scripts/import_whatsapp_feb2026.py @@ -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": "

Zapraszamy na wyjazd integracyjny do Chorwacji pod żaglami! Pomysł naszego Prezesa Leszka Glazy.

" + "

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.

", + "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() diff --git a/templates/index.html b/templates/index.html index 438a10d..88ca7a5 100755 --- a/templates/index.html +++ b/templates/index.html @@ -863,36 +863,41 @@ {% endif %} - -{% if next_event %} - + +{% if upcoming_events %} +{% for ue in upcoming_events %} +{% set ev = ue.event %} +
📅
-
Najbliższe wydarzenie – Kto weźmie udział?
-
{{ next_event.title }} →
+ {% if loop.first %} +
Najbliższe wydarzenia – Kto weźmie udział?
+ {% endif %} +
{{ ev.title }} →
- 📆 {{ next_event.event_date.strftime('%d.%m.%Y') }} ({{ ['Pon', 'Wt', 'Śr', 'Czw', 'Pt', 'Sob', 'Nd'][next_event.event_date.weekday()] }}) - {% if next_event.time_start %} - 🕕 {{ next_event.time_start.strftime('%H:%M') }} + 📆 {{ ev.event_date.strftime('%d.%m.%Y') }} ({{ ['Pon', 'Wt', 'Śr', 'Czw', 'Pt', 'Sob', 'Nd'][ev.event_date.weekday()] }}) + {% if ev.time_start %} + 🕕 {{ ev.time_start.strftime('%H:%M') }} {% endif %} - {% if next_event.location %} - 📍 {{ next_event.location[:30] }}{% if next_event.location|length > 30 %}...{% endif %} + {% if ev.location %} + 📍 {{ ev.location[:30] }}{% if ev.location|length > 30 %}...{% endif %} {% endif %}
- 👥 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 %}
- {% if user_registered %} + {% if ue.user_registered %} ✓ Jesteś zapisany/a - {% elif user_can_attend %} - - {% elif next_event.access_level == 'rada_only' %} + {% elif ue.user_can_attend %} + + {% elif ev.access_level == 'rada_only' %} 🔒 Rada Izby {% endif %}
+{% endfor %} {% endif %}