""" Migrate Rada Izby data to production. Creates/updates board members and creates meetings. Safe: checks for existing users by email before creating. """ import os import sys from datetime import datetime, date, time sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from database import SessionLocal, User, BoardMeeting, SystemRole from werkzeug.security import generate_password_hash db = SessionLocal() # --- Step 1: Create/update Rada members --- RADA_MEMBERS = [ {"name": "Andrzej Gorczycki", "email": "a.gorczycki@ekofabrykawejherowo.pl"}, {"name": "Angelika Piechocka", "email": "info@greenhousesystems.pl"}, {"name": "Artur Wiertel", "email": "artur.wiertel@waterm.pl"}, {"name": "Aureliusz Jędrzejewski", "email": "a.jedrzejewski@scrol.pl"}, {"name": "Dariusz Schmidtke", "email": "dariusz.schmidtke@tkchopin.pl"}, {"name": "Iwona Musiał", "email": "iwonamusial@cristap.pl"}, {"name": "Jacek Pomieczyński", "email": "jacek.pomieczynski@eura-tech.eu"}, {"name": "Jakub Bornowski", "email": "kuba@bormax.com.pl"}, {"name": "Janusz Masiak", "email": "jm@hebel-masiak.pl"}, {"name": "Krzysztof Kubis", "email": "krzysztof.kubis@sibuk.pl"}, {"name": "Leszek Glaza", "email": "leszek@rotor.pl"}, {"name": "Michał Węsierski", "email": "mjwesierski@gmail.com"}, {"name": "Paweł Kwidziński", "email": "pawel.kwidzinski@norda-biznes.info"}, {"name": "Paweł Piechota", "email": "pawel.piechota@norda-biznes.info"}, {"name": "Radosław Skwarło", "email": "radoslaw@skwarlo.pl"}, {"name": "Roman Wierciński", "email": "roman@sigmabudownictwo.pl"}, ] # Map: email -> prod user id (for attendance remapping) email_to_prod_id = {} for member in RADA_MEMBERS: user = db.query(User).filter(User.email == member["email"]).first() if user: # Existing user - just set rada flag if not user.is_rada_member: user.is_rada_member = True print(f" UPDATED: {user.name} ({user.email}) -> is_rada_member=True") else: print(f" OK: {user.name} ({user.email}) already rada member") email_to_prod_id[member["email"]] = user.id else: # Create new user with random password (they'll need to reset) import secrets temp_password = secrets.token_urlsafe(16) new_user = User( email=member["email"], name=member["name"], password_hash=generate_password_hash(temp_password), is_active=True, is_rada_member=True, role=SystemRole.MEMBER.name, is_verified=True, ) db.add(new_user) db.flush() # Get ID email_to_prod_id[member["email"]] = new_user.id print(f" CREATED: {new_user.name} ({new_user.email}) ID={new_user.id}") # Secretary (not a rada member, but needed for meeting reference) secretary_email = "magdalena.kloska@norda-biznes.info" sec_user = db.query(User).filter(User.email == secretary_email).first() if not sec_user: import secrets temp_password = secrets.token_urlsafe(16) sec_user = User( email=secretary_email, name="Magdalena Klóska", password_hash=generate_password_hash(temp_password), is_active=True, is_rada_member=False, role=SystemRole.MEMBER.name, is_verified=True, ) db.add(sec_user) db.flush() print(f" CREATED (secretary): {sec_user.name} ({sec_user.email}) ID={sec_user.id}") else: print(f" OK (secretary): {sec_user.name} ({sec_user.email})") # Deactivate corrupted duplicate account (ID=40) corrupted = db.query(User).filter(User.email == "a.gorczycki@ekofabrykawejherowo.plherowo.pl").first() if corrupted and corrupted.is_active: corrupted.is_active = False print(f" DEACTIVATED: ID={corrupted.id} ({corrupted.email}) - corrupted duplicate") db.commit() print(f"\nRada members: {len(email_to_prod_id)} processed") # --- Step 2: Staging user ID -> Prod user ID mapping --- # Staging attendance uses staging user IDs. We need to remap. # Staging ID -> email mapping (from staging export) STAGING_ID_TO_EMAIL = { "13": "jm@hebel-masiak.pl", "14": "jacek.pomieczynski@eura-tech.eu", "18": "info@greenhousesystems.pl", "35": "leszek@rotor.pl", "36": "iwonamusial@cristap.pl", "37": "a.gorczycki@ekofabrykawejherowo.pl", "38": "pawel.kwidzinski@norda-biznes.info", "39": "dariusz.schmidtke@tkchopin.pl", "40": "artur.wiertel@waterm.pl", "41": "a.jedrzejewski@scrol.pl", "42": "krzysztof.kubis@sibuk.pl", "43": "kuba@bormax.com.pl", "44": "pawel.piechota@norda-biznes.info", "45": "radoslaw@skwarlo.pl", "46": "roman@sigmabudownictwo.pl", "47": "mjwesierski@gmail.com", } def remap_attendance(staging_attendance): """Remap staging user IDs to production user IDs in attendance dict""" if not staging_attendance: return None remapped = {} for staging_id, data in staging_attendance.items(): email = STAGING_ID_TO_EMAIL.get(staging_id) if email and email in email_to_prod_id: prod_id = str(email_to_prod_id[email]) remapped[prod_id] = data else: print(f" WARNING: Could not remap staging ID {staging_id}") return remapped # --- Step 3: Create meetings --- # Get chairperson and secretary IDs on production chairperson = db.query(User).filter(User.email == "leszek@rotor.pl").first() secretary = db.query(User).filter(User.email == "magdalena.kloska@norda-biznes.info").first() # Get admin user as fallback for created_by admin = db.query(User).filter(User.role == 'ADMIN', User.is_active == True).first() created_by_id = admin.id if admin else 1 chairperson_id = chairperson.id if chairperson else None secretary_id = secretary.id if secretary else None # Check if meetings already exist existing = db.query(BoardMeeting).filter(BoardMeeting.year == 2026).all() if existing: print(f"\nWARNING: {len(existing)} meetings already exist for 2026. Skipping creation.") else: # Meeting 1/2026 (Jan 7) m1_attendance = { "13": {"present": False, "initials": "JM"}, "14": {"present": True, "initials": "JP"}, "18": {"present": True, "initials": "AP"}, "35": {"present": True, "initials": "LG"}, "36": {"present": True, "initials": "IM"}, "37": {"present": False, "initials": "AG"}, "38": {"present": True, "initials": "PK"}, "39": {"present": False, "initials": "DS"}, "40": {"present": True, "initials": "AW"}, "41": {"present": False, "initials": "AJ"}, "42": {"present": False, "initials": "KK"}, "43": {"present": True, "initials": "JB"}, "44": {"present": False, "initials": "PP"}, "45": {"present": True, "initials": "RS"}, "46": {"present": False, "initials": "RW"}, "47": {"present": False, "initials": "MW"}, } m1_proceedings = [ {"tasks": [], "title": "Akceptacja programu posiedzenia", "decisions": ["Program posiedzenia przyjęty."], "discussion": "Przyjęto program posiedzenia.", "agenda_item": 0}, {"tasks": [], "title": "Zbieranie kworum", "decisions": ["Kworum potwierdzone."], "discussion": "Stwierdzono kworum: 8 z 16 członków obecnych.", "agenda_item": 1}, {"tasks": ["AW – przekazanie członkom Rady materiałów: kalendarz wydarzeń + prezentacja strategii", "Członkowie Rady – przesłanie uwag do strategii marki i komunikacji – termin: do końca stycznia na adres Artur.wiertel@waterm.pl", "LG – koordynacja przygotowań do spotkania mentorskiego Akademii Biznesu NORDA (mentor: Wojciech Gębarowski, Orlex)", "IM – wstępna propozycja terminu i formuły kuligu (warianty + orientacyjne koszty)", "IM – sprawdzenie dostępności organizatora / miejsca kuligu i warunków realizacji", "AW – przygotowanie i przedstawienie na spotkaniu czwartkowym: (1) kalendarz 2026, (2) skrót strategii komunikacji, (3) projekt CRM – termin: 29/01/2026", "AW / MP (INPI) – doprecyzowanie funkcji CRM (forum i panel ogłoszeń) oraz propozycja etapów wdrożenia – termin do ustalenia z Radą", "Członkowie Rady – przygotowanie krótkiej propozycji/założeń dot. aktywizacji nowych członków i podniesienia prestiżu Rady (na potrzeby przygotowań do Walnego) – termin: luty", "LG i AW – wstępne rozeznanie organizacyjno-kosztowe dla: (1) Chorwacja – jachty, (2) misja Chiny – termin: luty", "RW – przypomnienie zasad korzystania z grupy WhatsApp (komunikaty informacyjne, minimum treści pobocznych)"], "title": "Planowanie wydarzeń dla Izby na rok 2026", "decisions": ["Członkowie Rady zgłoszą uwagi i propozycje do przedstawionej strategii marki i komunikacji.", "Spotkanie mentorskie Akademii Biznesu NORDA jest przygotowywane; temat kontynuowany organizacyjnie.", "Wątek Walnego Zgromadzenia wyborczego (czerwiec) wymaga przygotowania koncepcji aktywizowania nowych członków oraz działań wzmacniających prestiż Rady – do dalszego omówienia na kolejnym posiedzeniu.", "Projekt CRM (INPI) przyjęto do dalszych prac; zebrane zostaną uwagi funkcjonalne, w tym dotyczące forum i panelu ogłoszeń.", "Na czwartkowym spotkaniu networkingowym Chwila dla Biznesu członkom Izby zostaną przedstawione: kalendarz 2026, skrót strategii komunikacji oraz projekt CRM.", "Temat kuligu przyjęto do szybkiego rozpoznania pod kątem realności terminu i warunków organizacyjnych.", "Mniejsze wydarzenia integracyjne mogą mieć uproszczoną organizację, jednak komunikacja o nich ma docierać do wszystkich członków Izby.", "Propozycje wyjazdów integracyjnych (Chorwacja / Chiny) przyjęto do dalszego rozpoznania (zainteresowanie, koszty, terminy, organizacja).", "Grupa WhatsApp (RW) pozostaje kanałem krótkich komunikatów dla wszystkich, z ograniczeniem liczby wiadomości."], "discussion": "AW przedstawił propozycję kalendarza wydarzeń Izby na 2026 rok wraz ze wstępnym omówieniem planowanych aktywności. Podkreślono, że główny nacisk w 2026 roku powinien zostać położony na rozpoczęcie przygotowań do jubileuszu 30-lecia Izby NORDA.\n\nAW zaprezentował założenia strategii Izby NORDA w obszarze promocji marki i komunikacji, przygotowanej przez firmę Your Welcome (Agnieszka Kulinkowska) przy udziale przedstawicieli Zarządu: AW oraz PKw. Ustalono, że prezentacja ma charakter roboczy i służy doprecyzowaniu strategii, a członkowie Rady mają wnieść uwagi i propozycje.\n\nLG poinformował o prowadzonych przygotowaniach do spotkania mentorskiego w ramach Akademii Biznesu NORDA; w roli mentora ma wystąpić Pan Wojciech Gębarowski, właściciel firmy Orlex.\n\nOmówiono przygotowania do czerwcowego Walnego Zgromadzenia – wyborczego, na którym zostanie powołana nowa Rada. W dyskusji pojawił się pomysł aktywizowania nowych członków oraz podniesienia prestiżu Rady.\n\nAW przedstawił projekt platformy CRM przygotowywanej przez członka Izby – Macieja Pieńczyna (firma INPI). Platforma ma służyć komunikacji pomiędzy administracją a członkami oraz pomiędzy samymi członkami. Ustalono kierunek rozbudowy CRM jako pogłębionej platformy wymiany informacji (m.in. forum oraz panel ogłoszeń). Platforma ma stanowić zamkniętą część portalu www.nordabiznes.pl, dostępną wyłącznie dla członków Izby.\n\nIM zaproponowała inicjatywę integracyjną w formie kuligu, możliwą do realizacji jeszcze w styczniu (w zależności od warunków pogodowych). Uznano, że mniejsze wydarzenia integracyjne mogą mieć uproszczoną organizację, ale informacja o możliwości udziału powinna trafiać do wszystkich członków Izby.\n\nAW zaproponował, aby w ramach czwartkowych spotkań networkingowych przygotować prezentację dla członków Izby obejmującą: (1) kalendarz wydarzeń na 2026 rok, (2) skrót strategii komunikacji, (3) prezentację projektu CRM.\n\nPrzedstawiono propozycje wyjazdów integracyjnych: rejs jachtowy w Chorwacji (wrzesień 2026) oraz misja gospodarcza do Chin (październik–listopad 2026).\n\nUznano, że grupa WhatsApp prowadzona przez RW ma pełnić funkcję krótkiego komunikatora do wszystkich, z minimalną liczbą wiadomości niezwiązanych bezpośrednio z informacją dla członków.", "agenda_item": 2}, {"tasks": [], "title": "Prezentacja: 303 – Łukasz Mielewczyk (kandydat na członka Izby)", "decisions": ["Prezentacja przeniesiona na kolejny miesiąc."], "discussion": "Kandydat nie stawił się, wcześniej poinformował o nagłej sytuacji.", "agenda_item": 3}, {"tasks": ["MK – przedstawić na SM i stronie www nowego członka, możliwie jak najszybciej", "Otoczyć opieką nowego członka przez członka Izby (nie koniecznie z Rady), zaprosić na spotkanie 29/01/2026 Hotel Olimp"], "title": "Prezentacja: Iwona Speniak – coach/mentoring (kandydatka na członka Izby)", "decisions": ["Przyjęta jednogłośnie przez Radę."], "discussion": "Obszar działania: aktywizacja kobiet, warsztaty psychoedukacyjne.", "agenda_item": 4}, {"tasks": [], "title": "Głosowanie nad kandydaturami", "decisions": [], "discussion": "Głosowanie przeprowadzono w ramach poszczególnych prezentacji.", "agenda_item": 5}, {"tasks": [], "title": "Propozycja stworzenia ogólnodostępnej listy członków do kontaktów", "decisions": ["Temat włączony do projektu CRM."], "discussion": "Omówiono w ramach przygotowań do CRM.", "agenda_item": 6}, {"tasks": [], "title": "Budżet i planowanie wydatków na rok 2026 oraz planowanie wysokości składek 2026", "decisions": ["Temat przeniesiony na kolejne posiedzenie."], "discussion": "Punkt przeniesiony na kolejną Radę.", "agenda_item": 7}, {"tasks": [], "title": "Wolne wnioski", "decisions": ["Termin kolejnego spotkania: 04/02/2026."], "discussion": "Zaproponowano przenieść godzinę kolejnej Rady na 16:00.", "agenda_item": 8}, {"tasks": [], "title": "Ustalenie daty kolejnego posiedzenia", "decisions": ["Kolejne posiedzenie Rady: 04.02.2026 o godz. 16:00."], "discussion": "Ustalono datę 04/02/2026, godzina 16:00.", "agenda_item": 9}, {"tasks": [], "title": "Zakończenie posiedzenia", "decisions": [], "discussion": "Prowadzący zamknął posiedzenie o godz. 18:30.", "agenda_item": 10}, ] meeting1 = BoardMeeting( meeting_number=1, year=2026, meeting_date=date(2026, 1, 7), start_time=time(16, 0), end_time=time(18, 30), location="Siedziba Izby", chairperson_id=chairperson_id, secretary_id=secretary_id, guests="brak", agenda_items=[ {"title": "Akceptacja programu posiedzenia", "number": 1, "time_end": "16:10", "time_start": "16:00"}, {"title": "Zbieranie kworum", "number": 2, "time_end": "16:15", "time_start": "16:10"}, {"title": "Planowanie wydarzen dla Izby na rok 2026", "number": 3, "time_end": "16:40", "time_start": "16:15"}, {"title": "Prezentacja: 303 - Lukasz Mielewczyk (kandydat)", "number": 4, "time_end": "16:50", "time_start": "16:40"}, {"title": "Prezentacja: Iwona Speniak - coach/mentoring", "number": 5, "time_end": "17:15", "time_start": "17:00"}, {"title": "Glosowanie nad kandydaturami", "number": 6, "time_end": "17:20", "time_start": "17:15"}, {"title": "Propozycja listy czlonkow do kontaktow", "number": 7, "time_end": "17:30", "time_start": "17:20"}, {"title": "Budzet i planowanie wydatkow na rok 2026", "number": 8, "time_end": "17:50", "time_start": "17:30"}, {"title": "Wolne wnioski", "number": 9, "time_end": "18:00", "time_start": "17:50"}, {"title": "Ustalenie daty kolejnego posiedzenia", "number": 10}, {"title": "Zakonczenie posiedzenia", "number": 11}, ], attendance=remap_attendance(m1_attendance), quorum_count=8, quorum_confirmed=True, proceedings=m1_proceedings, status="protocol_published", created_by=created_by_id, agenda_published_at=datetime(2026, 1, 6, 12, 0), protocol_published_at=datetime(2026, 1, 15, 12, 0), ) db.add(meeting1) # Meeting 2/2026 (Feb 4) m2_attendance = { "13": {"present": None, "initials": "JM"}, "14": {"present": None, "initials": "JP"}, "18": {"present": None, "initials": "AP"}, "35": {"present": None, "initials": "LG"}, "36": {"present": None, "initials": "IM"}, "37": {"present": None, "initials": "AG"}, "38": {"present": None, "initials": "PK"}, "39": {"present": None, "initials": "DS"}, "40": {"present": None, "initials": "AW"}, "41": {"present": None, "initials": "AJ"}, "42": {"present": None, "initials": "KK"}, "43": {"present": None, "initials": "JB"}, "44": {"present": None, "initials": "PP"}, "45": {"present": None, "initials": "RS"}, "46": {"present": None, "initials": "RW"}, "47": {"present": None, "initials": "MW"}, } meeting2 = BoardMeeting( meeting_number=2, year=2026, meeting_date=date(2026, 2, 4), start_time=time(16, 0), end_time=time(19, 0), location="Siedziba Izby", chairperson_id=chairperson_id, secretary_id=secretary_id, guests=None, agenda_items=[ {"title": "Akceptacja programu posiedzenia", "number": 1, "time_end": "16:10", "time_start": "16:00"}, {"title": "Zbieranie kworum", "number": 2, "time_end": "16:15", "time_start": "16:10"}, {"title": "Prezentacja firmy Konkol - kandydat na czlonka Izby", "number": 3, "time_end": "16:40", "time_start": "16:30"}, {"title": "Prezentacja firmy Ibet - kandydat na czlonka Izby", "number": 4, "time_end": "17:00", "time_start": "16:45"}, {"title": "Prezentacja firmy Audioline Sp. z o.o. - kandydat na czlonka Izby", "number": 5, "time_end": "17:15", "time_start": "17:00"}, {"title": "Prezentacja firmy Pc Invest - kandydat na czlonka Izby", "number": 6, "time_end": "17:30", "time_start": "17:15"}, {"title": "Prezentacja firmy Pacific Sun - kandydat na czlonka Izby", "number": 7, "time_end": "17:40", "time_start": "17:30"}, {"title": "Glosowanie nad kandydaturami", "number": 8, "time_end": "17:50", "time_start": "17:40"}, {"title": "Omowienie strategii i dzialan marketingowych", "number": 9, "time_end": "18:05", "time_start": "17:50"}, {"title": "Wysokosc skladek na rok 2026 - plany", "number": 10, "time_end": "18:20", "time_start": "18:05"}, {"title": "Planowane wydarzenia integracyjne - grill wiosenny", "number": 11, "time_end": "18:30", "time_start": "18:20"}, {"title": "Wybor komitetu organizacyjnego na 30-lecie Izby", "number": 12, "time_end": "18:45", "time_start": "18:30"}, {"title": "Sprawozdanie z realizacji zadan ze stycznia oraz wolne wnioski", "number": 13, "time_end": "19:00", "time_start": "18:45"}, {"title": "Ustalenie daty kolejnego posiedzenia (04.03)", "number": 14}, {"title": "Zakonczenie posiedzenia", "number": 15}, ], attendance=remap_attendance(m2_attendance), quorum_count=None, quorum_confirmed=None, proceedings=None, status="agenda_published", created_by=created_by_id, agenda_published_at=datetime(2026, 2, 3, 12, 0), protocol_published_at=None, ) db.add(meeting2) db.commit() print("\nMeetings created: 2") # --- Verify --- meetings = db.query(BoardMeeting).filter(BoardMeeting.year == 2026).all() members = db.query(User).filter(User.is_rada_member == True, User.is_active == True).all() print(f"\n=== VERIFICATION ===") print(f"Meetings 2026: {len(meetings)}") for m in meetings: print(f" {m.meeting_identifier} | {m.meeting_date} | status={m.status}") print(f"Rada members: {len(members)}") for u in sorted(members, key=lambda x: x.name or ''): print(f" {u.name} ({u.email})") db.close() print("\nDone!")